{"id":48,"date":"2026-09-06T11:24:13","date_gmt":"2026-09-06T11:24:13","guid":{"rendered":"https:\/\/nodeguard.io\/blog\/?p=48"},"modified":"2026-09-06T17:18:21","modified_gmt":"2026-09-06T15:18:21","slug":"how-to-secure-a-linux-vps","status":"publish","type":"post","link":"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/","title":{"rendered":"How to Secure a Linux VPS: Essential Ubuntu 26.04 Hardening Guide"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Your VPS is not anonymous. The moment an IPv4 address is assigned to your server, it becomes part of a public range that automated scanners sweep continuously. Log in to a brand-new box, run <code>journalctl -u ssh --since \"10 minutes ago\"<\/code>, and you will usually find failed root logins already stacking up \u2014 from hosts you have never heard of, in countries you have never visited.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"540\" src=\"https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/09\/secure-a-linux-vps-failed-ssh-logins.webp\" alt=\"How to secure a Linux VPS \u2014 failed SSH login attempts on a fresh server\" class=\"wp-image-70\" srcset=\"https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/09\/secure-a-linux-vps-failed-ssh-logins.webp 1200w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/09\/secure-a-linux-vps-failed-ssh-logins-300x135.webp 300w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/09\/secure-a-linux-vps-failed-ssh-logins-767x345.webp 767w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/09\/secure-a-linux-vps-failed-ssh-logins-1024x461.webp 1024w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><figcaption class=\"wp-element-caption\">Eleven minutes after provisioning: 1,284 failed login attempts, none of them personal.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">None of that is personal. It is background radiation. Bots enumerate address space, knock on port 22, try <code>root:admin<\/code>, <code>root:123456<\/code>, <code>ubuntu:ubuntu<\/code>, and move on. The overwhelming majority of compromised servers are not victims of clever attacks; they are victims of defaults left untouched.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The good news is that the fix is boring and finite. You can secure a Linux VPS to a genuinely respectable baseline in about thirty minutes, and most of that time is spent waiting for <code>apt<\/code> to finish. This guide walks through the sequence we would follow on any new Ubuntu or Debian box, with the reasoning behind each step, because a checklist you understand is one you will actually maintain.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Tested on Ubuntu 26.04 LTS. Every command below also works unchanged on Ubuntu 24.04 LTS and Debian 13 except where noted.<\/em><\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_86 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">In This Guide<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Start_From_a_Clean_Current_Image\" >Start From a Clean, Current Image<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Patch_Before_You_Do_Anything_Else\" >Patch Before You Do Anything Else<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Create_a_Non-Root_User_With_sudo\" >Create a Non-Root User With sudo<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Lock_Down_SSH\" >Lock Down SSH<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Why_key-only_authentication_is_non-negotiable\" >Why key-only authentication is non-negotiable<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Changing_the_port_and_one_modern_gotcha\" >Changing the port, and one modern gotcha<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Put_a_Default-Deny_Firewall_in_Front\" >Put a Default-Deny Firewall in Front<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Slow_Down_Brute_Force_With_Fail2Ban\" >Slow Down Brute Force With Fail2Ban<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Turn_On_Automatic_Security_Updates\" >Turn On Automatic Security Updates<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Reduce_the_Attack_Surface\" >Reduce the Attack Surface<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Harden_the_Kernel_and_Shared_Memory\" >Harden the Kernel and Shared Memory<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Secure_the_Application_Layer\" >Secure the Application Layer<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Back_Up_as_Though_You_Will_Need_It\" >Back Up as Though You Will Need It<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Watch_What_You_Have_Built\" >Watch What You Have Built<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#How_to_Secure_a_Linux_VPS_Your_30-Minute_Checklist\" >How to Secure a Linux VPS: Your 30-Minute Checklist<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#How_do_I_secure_a_Linux_VPS\" >How do I secure a Linux VPS?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#How_long_does_it_take_to_harden_a_new_VPS\" >How long does it take to harden a new VPS?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Is_changing_the_SSH_port_worth_it\" >Is changing the SSH port worth it?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/nodeguard.io\/blog\/how-to-secure-a-linux-vps\/#Do_I_still_need_Fail2Ban_if_I_use_SSH_keys\" >Do I still need Fail2Ban if I use SSH keys?<\/a><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Start_From_a_Clean_Current_Image\"><\/span>Start From a Clean, Current Image<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Security begins before your first SSH session. When you deploy, choose a current LTS release rather than whatever is familiar \u2014 Ubuntu 26.04 LTS or Debian 13 both carry years of security support ahead of them, which means patches keep arriving without a distribution upgrade in the middle of your busiest quarter. An OS approaching end of life is a slow-motion vulnerability.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If your provider offers SSH key injection at deploy time, use it. Every NodeGuard <a href=\"\/cloud-vps\" data-type=\"link\" data-id=\"\/cloud-vps\">Cloud VPS<\/a> can be provisioned with your public key already in place, which means the server never has a password-authenticated root account exposed to the internet, not even for the ten minutes it takes you to configure one. That closes the single most common window of exposure entirely.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Patch_Before_You_Do_Anything_Else\"><\/span>Patch Before You Do Anything Else<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Images are snapshots. Even a freshly published one was built days or weeks ago, and the packages inside it are frozen at that moment. Your first command should always be:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update &amp;&amp; sudo apt full-upgrade -y\nsudo reboot\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>full-upgrade<\/code> rather than <code>upgrade<\/code> matters here: it allows packages to be added or removed when dependencies changed, which is exactly what happens with kernel and library updates. Reboot afterward if the kernel was replaced \u2014 a patched kernel sitting on disk while the vulnerable one is still running protects nobody.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Create_a_Non-Root_User_With_sudo\"><\/span>Create a Non-Root User With sudo<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Working as root is a habit worth breaking. Not because root is inherently dangerous, but because it removes the pause between intent and consequence. A mistyped <code>rm -rf<\/code> under a normal account throws a permission error; the same command as root does exactly what you asked.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>adduser alex\nusermod -aG sudo alex\nrsync --archive --chown=alex:alex ~\/.ssh \/home\/alex\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That last line copies your existing authorized key to the new user before you lock root out \u2014 skip it and you will lock yourself out instead. Open a <strong>second terminal<\/strong> and confirm you can log in as the new user and run <code>sudo -v<\/code> successfully. Do not close your original session until that works. This is the single most common way people strand themselves on a fresh server.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Lock_Down_SSH\"><\/span>Lock Down SSH<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SSH is the front door, and it deserves the most attention. Edit <code>\/etc\/ssh\/sshd_config<\/code> or, better, drop a file into <code>\/etc\/ssh\/sshd_config.d\/99-hardening.conf<\/code> so your changes survive package upgrades cleanly. Every directive here is documented in the <a href=\"https:\/\/man.openbsd.org\/sshd_config\" data-type=\"link\" data-id=\"https:\/\/man.openbsd.org\/sshd_config\" target=\"_blank\" rel=\"noreferrer noopener\">OpenSSH manual<\/a>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\nMaxAuthTries 3\nX11Forwarding no\nAllowUsers alex<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you have a static IP or connect through a <a href=\"\/secure-vpn\" data-type=\"link\" data-id=\"\/secure-vpn\">VPN<\/a>, tighten it further with <code>AllowUsers alex@your.static.ip<\/code> so SSH only answers from an address you control.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Why_key-only_authentication_is_non-negotiable\"><\/span>Why key-only authentication is non-negotiable<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A strong password is guessable given enough attempts; a 4096-bit RSA or Ed25519 key is not, in any timeframe that matters. Turning off password authentication does not merely make brute-force attacks harder \u2014 it makes them structurally impossible. The bots will keep knocking, and every attempt will fail at the protocol level before a password is ever evaluated.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Generate your key locally, never on the server:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh-keygen -t ed25519 -C \"workstation-2026\"\nssh-copy-id alex@your-server-ip\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Changing_the_port_and_one_modern_gotcha\"><\/span>Changing the port, and one modern gotcha<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Moving SSH off port 22 is not real security \u2014 a port scan finds it in seconds \u2014 but it does cut log noise by an enormous margin, which makes genuine anomalies visible. If you change it, be aware of a change in recent releases: <strong>Ubuntu 24.04 and 26.04 use socket activation for SSH.<\/strong> The <code>Port<\/code> directive in <code>sshd_config<\/code> is ignored, because <code>systemd<\/code> owns the listening socket. Setting a new port and reloading will appear to work and then refuse connections.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The most reliable fix is to bypass socket activation entirely:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl disable --now ssh.socket\nsudo systemctl enable --now ssh.service\nsudo ss -tulpn | grep sshd\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That final verification step is the important one. Always confirm the daemon is listening where you think it is, from a second session, before you disconnect.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Put_a_Default-Deny_Firewall_in_Front\"><\/span>Put a Default-Deny Firewall in Front<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A firewall&#8217;s job is not to block known-bad traffic. It is to permit only known-good traffic, so that a service you forgot you installed cannot quietly accept connections from the world.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw default deny incoming\nsudo ufw default allow outgoing\nsudo ufw allow 2222\/tcp    # your SSH port \u2014 add this FIRST\nsudo ufw allow 80,443\/tcp\nsudo ufw enable\nsudo ufw status verbose\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add the SSH rule before enabling, every single time. UFW will warn you, and the warning is not decorative.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two things worth knowing. First, databases should almost never appear in this list \u2014 MariaDB, PostgreSQL, and Redis belong bound to <code>127.0.0.1<\/code>, reachable by your application and nothing else. Second, if you sit behind a CDN or proxy such as Cloudflare, your firewall sees the proxy&#8217;s IP rather than the visitor&#8217;s. Restore the real client IP at the web-server layer, or your logs and rate limits will be measuring the wrong thing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Slow_Down_Brute_Force_With_Fail2Ban\"><\/span>Slow Down Brute Force With Fail2Ban<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Even with key-only SSH, repeated failed attempts consume resources and bury real events in noise. <a href=\"https:\/\/github.com\/fail2ban\/fail2ban\" target=\"_blank\" data-type=\"link\" data-id=\"https:\/\/github.com\/fail2ban\/fail2ban\" rel=\"noreferrer noopener\">Fail2Ban<\/a> reads your logs and firewalls off repeat offenders automatically.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install fail2ban -y\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Create <code>\/etc\/fail2ban\/jail.local<\/code> \u2014 never edit <code>jail.conf<\/code>, which package updates overwrite:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;DEFAULT]\nbantime  = 1h\nfindtime = 10m\nmaxretry = 5\nbackend  = systemd<\/code><\/pre>\n\n\n<p>[sshd]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">enabled = true port = 2222<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On current Ubuntu and Debian releases the <code>systemd<\/code> backend matters, because journald has largely replaced flat log files. Check your work with <code>sudo fail2ban-client status sshd<\/code>. Also whitelist your own static IP under <code>ignoreip<\/code> if you have one \u2014 Fail2Ban does not distinguish between an attacker and a sysadmin who fat-fingered a passphrase five times.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Turn_On_Automatic_Security_Updates\"><\/span>Turn On Automatic Security Updates<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The gap between a vulnerability being <a href=\"https:\/\/ubuntu.com\/security\/notices\" target=\"_blank\" data-type=\"link\" data-id=\"https:\/\/ubuntu.com\/security\/notices\" rel=\"noreferrer noopener\">disclosed<\/a> and being exploited at scale is now measured in hours. Manual patching cannot win that race, and most breaches exploit something that was fixed upstream weeks earlier.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install unattended-upgrades -y\nsudo dpkg-reconfigure --priority=low unattended-upgrades\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The default configuration applies security updates only, which is the right balance \u2014 you get patched without feature updates surprising you at 3 a.m. If you want unattended reboots for kernel updates, enable <code>Unattended-Upgrade::Automatic-Reboot<\/code> with a scheduled time, and make sure your services start cleanly on boot before you trust it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Reduce_the_Attack_Surface\"><\/span>Reduce the Attack Surface<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every listening service is a potential entry point. Audit what your server is actually exposing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ss -tulpn\nsudo systemctl list-units --type=service --state=running\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Look for anything you did not deliberately install. Stock images frequently ship with services you will never use. Remove them rather than merely stopping them \u2014 an uninstalled package cannot be started by a dependency later. Where a service must run but only serves local traffic, bind it to <code>127.0.0.1<\/code> instead of <code>0.0.0.0<\/code>. That one change is worth more than most firewall rules, because it holds even if the firewall is misconfigured.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Harden_the_Kernel_and_Shared_Memory\"><\/span>Harden the Kernel and Shared Memory<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A small set of <code>sysctl<\/code> values closes off common network-level tricks. Add them to <code>\/etc\/sysctl.d\/99-hardening.conf<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>net.ipv4.conf.all.rp_filter = 1\nnet.ipv4.conf.all.accept_redirects = 0\nnet.ipv4.conf.all.send_redirects = 0\nnet.ipv4.conf.all.accept_source_route = 0\nnet.ipv4.icmp_echo_ignore_broadcasts = 1\nnet.ipv4.tcp_syncookies = 1\nkernel.dmesg_restrict = 1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apply with <code>sudo sysctl --system<\/code>. These are conservative, widely deployed settings; they will not break a normal web or application server, and they remove several classes of spoofing and reconnaissance outright.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Secure_the_Application_Layer\"><\/span>Secure the Application Layer<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Server hardening is necessary but insufficient. In practice, the majority of real-world compromises on hosting infrastructure arrive through the application, not the operating system.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Serve everything over TLS.<\/strong> Certbot issues free <a href=\"https:\/\/letsencrypt.org\/\" target=\"_blank\" data-type=\"link\" data-id=\"https:\/\/letsencrypt.org\/\" rel=\"noreferrer noopener\">Let&#8217;s Encrypt<\/a> certificates and renews them automatically. Redirect HTTP to HTTPS at the web server or CDN, and enable HSTS once you are confident the site works without exception.<\/li>\n\n\n\n<li><strong>Use per-application database users.<\/strong> One database, one user, only the privileges that application needs. A SQL injection in a marketing site should never expose your billing data.<\/li>\n\n\n\n<li><strong>Isolate PHP applications.<\/strong> Separate PHP-FPM pools per site, each running as its own system user, mean a compromised plugin in one site cannot read files belonging to another.<\/li>\n\n\n\n<li><strong>Keep <a href=\"\/wordpress-hosting\" data-type=\"link\" data-id=\"\/wordpress-hosting\">WordPress<\/a> lean.<\/strong> Most WordPress breaches trace back to an abandoned plugin or theme. Delete what you do not use, enable automatic updates for the core and for plugins you trust, and enforce strong credentials with two-factor authentication on admin accounts.<\/li>\n\n\n\n<li><strong>Restrict file permissions.<\/strong> Web roots should be <code>755<\/code> for directories and <code>644<\/code> for files, owned by a user the web server can read but generally cannot write.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Back_Up_as_Though_You_Will_Need_It\"><\/span>Back Up as Though You Will Need It<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Assume, calmly, that something will eventually go wrong \u2014 a bad deploy, a failed disk, a successful attack. The question is not whether you have backups, but whether you have <em>restores<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A workable pattern: a nightly <code>mysqldump<\/code> plus a compressed archive of your web roots, retained locally for a week, and copied off-server to separate storage. Backups that live only on the machine they protect are not backups. And schedule a restore drill \u2014 pull last night&#8217;s archive onto a scratch server and bring the site up. The first time you attempt a restore should never be during an incident.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Watch_What_You_Have_Built\"><\/span>Watch What You Have Built<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Hardening is a state, not an event. A light monitoring rhythm keeps that state from decaying:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Review authentication failures and Fail2Ban bans weekly: <code>sudo lastb | head -20<\/code><\/li>\n\n\n\n<li>Confirm unattended upgrades are actually applying: <code>cat \/var\/log\/unattended-upgrades\/unattended-upgrades.log<\/code><\/li>\n\n\n\n<li>Track disk, memory, and load, so that a resource anomaly reaches you before your visitors do<\/li>\n\n\n\n<li>Watch for unexpected outbound connections \u2014 compromised servers usually phone home<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_to_Secure_a_Linux_VPS_Your_30-Minute_Checklist\"><\/span>How to Secure a Linux VPS: Your 30-Minute Checklist<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Deploy a current LTS image with your SSH key injected<\/li>\n\n\n\n<li><code>apt update &amp;&amp; apt full-upgrade<\/code>, then reboot<\/li>\n\n\n\n<li>Create a sudo user, copy your key, verify in a second terminal<\/li>\n\n\n\n<li>Disable root login and password authentication in <code>sshd_config.d<\/code><\/li>\n\n\n\n<li>Change the SSH port, working around socket activation, and verify with <code>ss -tulpn<\/code><\/li>\n\n\n\n<li>Enable UFW with default-deny, allowing SSH first<\/li>\n\n\n\n<li>Install Fail2Ban with a <code>jail.local<\/code> using the systemd backend<\/li>\n\n\n\n<li>Enable <code>unattended-upgrades<\/code><\/li>\n\n\n\n<li>Audit listening services; bind local-only services to <code>127.0.0.1<\/code><\/li>\n\n\n\n<li>Apply <code>sysctl<\/code> hardening<\/li>\n\n\n\n<li>Issue TLS certificates and lock down the application layer<\/li>\n\n\n\n<li>Configure off-server backups \u2014 and test a restore<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">None of this is exotic. It is the same sequence experienced administrators run on autopilot, and it eliminates the overwhelming majority of opportunistic attacks, because opportunistic attacks depend entirely on defaults.<\/p>\n\n\n<div id=\"rank-math-faq\" class=\"rank-math-block\">\n<div class=\"rank-math-list \">\n<div id=\"faq-question-1788699979152\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><span class=\"ez-toc-section\" id=\"How_do_I_secure_a_Linux_VPS\"><\/span>How do I secure a Linux VPS?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Secure a Linux VPS by working through a fixed sequence: patch the system fully, create a non-root sudo user, switch SSH to key-only authentication and disable root login, enable a default-deny firewall, install Fail2Ban, and turn on automatic security updates. Those six steps stop the overwhelming majority of automated attacks, because automated attacks depend entirely on defaults being left in place. Everything after that \u2014 kernel tuning, application hardening, backups \u2014 reduces the damage of the attacks that get past the first layer.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1788700027300\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><span class=\"ez-toc-section\" id=\"How_long_does_it_take_to_harden_a_new_VPS\"><\/span>How long does it take to harden a new VPS?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>About thirty minutes for the core baseline, and most of that is spent waiting for <code>apt<\/code> to finish rather than typing. The steps that matter most \u2014 key-only SSH, a default-deny firewall, and unattended upgrades \u2014 take roughly ten minutes combined. Backups and application-layer hardening take longer and are worth scheduling separately, but the server is no longer low-hanging fruit once the first ten minutes are done.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1788700040961\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><span class=\"ez-toc-section\" id=\"Is_changing_the_SSH_port_worth_it\"><\/span>Is changing the SSH port worth it?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Changing the SSH port is not a security measure, because a port scan finds the new port in seconds. It is a noise-reduction measure, and that is still valuable: moving off port 22 cuts automated login attempts by an enormous margin, which makes genuine anomalies visible in your logs instead of buried under thousands of bot entries. Change it if you want readable logs. Do not change it and consider the server secured.<\/p>\n\n<\/div>\n<\/div>\n<div id=\"faq-question-1788700064217\" class=\"rank-math-list-item\">\n<h3 class=\"rank-math-question \"><span class=\"ez-toc-section\" id=\"Do_I_still_need_Fail2Ban_if_I_use_SSH_keys\"><\/span>Do I still need Fail2Ban if I use SSH keys?<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<div class=\"rank-math-answer \">\n\n<p>Yes, though for a different reason than most people assume. With password authentication disabled, brute-force attempts cannot succeed \u2014 but they still consume CPU, bandwidth and log space, and they bury real events in noise. Fail2Ban also protects services beyond SSH, including web application logins and mail. Treat it as noise control and multi-service coverage rather than as your primary SSH defence.<\/p>\n\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">At NodeGuard, our <a href=\"\/cloud-vps\" data-type=\"link\" data-id=\"\/cloud-vps\">Cloud VPS<\/a> instances ship with key-based access from first boot, automated monitoring, and clean current images across Ubuntu, Debian, AlmaLinux, Rocky, and Fedora \u2014 so the baseline above starts closer to done. Plans begin at \u20ac7\/month with full root access, which means the server is genuinely yours to configure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Harden it once, properly, and then get back to building the thing you actually wanted the server for.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n","protected":false},"excerpt":{"rendered":"<p>Your VPS is not anonymous. The moment an IPv4 address is assigned to your server, it becomes part of a public range\u2026<\/p>\n","protected":false},"author":1,"featured_media":62,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18,24],"tags":[44,40,36,6,33,35,43,37,5,38,41,39,42,34,45],"class_list":["post-48","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux","category-security","tag-cloud-vps","tag-debian-13","tag-fail2ban","tag-linux-server","tag-linux-vps","tag-server-hardening","tag-ssh-keys","tag-ssh-security","tag-sysadmin","tag-ubuntu-26-04","tag-ubuntu-server","tag-ufw-firewall","tag-unattended-upgrades","tag-vps-security","tag-web-hosting-security"],"_links":{"self":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts\/48","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/comments?post=48"}],"version-history":[{"count":13,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts\/48\/revisions"}],"predecessor-version":[{"id":87,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts\/48\/revisions\/87"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/media\/62"}],"wp:attachment":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/media?parent=48"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/categories?post=48"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/tags?post=48"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}