{"id":166223,"date":"2026-04-14T23:24:23","date_gmt":"2026-04-14T20:24:23","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=166223"},"modified":"2026-05-12T18:31:23","modified_gmt":"2026-05-12T15:31:23","slug":"harden-ubuntu-2604-server","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/harden-ubuntu-2604-server\/","title":{"rendered":"How To Harden Ubuntu 26.04 LTS Server (Complete Security Guide)"},"content":{"rendered":"<p>A freshly installed Ubuntu 26.04 server is reasonable out of the box, but it is not hardened. Default SSH settings, no firewall rules, no intrusion monitoring, no kernel tuning. Stick it on a public IP and you will see brute-force SSH attempts within minutes.<\/p>\n\n<p>This guide walks through a practical hardening baseline for Ubuntu 26.04 LTS. You will create a sudo user, lock down SSH (OpenSSH 10.2 with post-quantum ML-KEM is the new default), enable UFW with rate limiting, install Fail2ban, apply kernel sysctl protections, verify AppArmor, set up auditd, and run a Lynis audit to measure the result. Every command was tested on a real VM. Pair this with our <a href=\"https:\/\/computingforgeeks.com\/ubuntu-2604-initial-server-setup\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ubuntu 26.04 initial server setup<\/a> guide for the first 10 minutes of a new box.<\/p>\n\n<p><em>Tested <strong>April 2026<\/strong> on Ubuntu 26.04 LTS, OpenSSH 10.2p1, UFW 0.36.2, Fail2ban 1.1.0, AppArmor 5.0.0 beta1, auditd 4.1.2, Lynis 3.1.6<\/em><\/p>\n\n<h2>Prerequisites<\/h2>\n\n<ul>\n<li>Ubuntu 26.04 LTS server with root or sudo access<\/li>\n<li>SSH access from your workstation (console fallback if you lock yourself out)<\/li>\n<li>An SSH keypair on your local machine (run <code>ssh-keygen -t ed25519<\/code> if you don&#8217;t have one)<\/li>\n<li>Server package sources already configured and reachable<\/li>\n<\/ul>\n\n<p>If you are running a version older than 26.04, our <a href=\"https:\/\/computingforgeeks.com\/upgrade-ubuntu-24-04-to-26-04\/\" target=\"_blank\" rel=\"noreferrer noopener\">upgrade guide from 24.04 to 26.04<\/a> covers the release-upgrade path. The hardening steps below assume a fresh 26.04 system.<\/p>\n\n<h2>Step 1: Patch the System First<\/h2>\n\n<p>Hardening a box with outdated packages is pointless. Update everything before touching any security config.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt update &amp;&amp; sudo apt -y full-upgrade<\/code><\/pre>\n\n\n<p>Reboot if the kernel was updated:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>[ -f \/var\/run\/reboot-required ] &amp;&amp; sudo reboot<\/code><\/pre>\n\n\n<p>Now install unattended-upgrades so future security patches land automatically without anyone remembering to run <code>apt<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y unattended-upgrades apt-listchanges\nsudo dpkg-reconfigure -plow unattended-upgrades<\/code><\/pre>\n\n\n<p>That writes the periodic config. If the interactive prompt skips, drop this file in place:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/apt\/apt.conf.d\/20auto-upgrades &gt;\/dev\/null &lt;&lt;'EOF'\nAPT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Download-Upgradeable-Packages \"1\";\nAPT::Periodic::AutocleanInterval \"7\";\nAPT::Periodic::Unattended-Upgrade \"1\";\nEOF<\/code><\/pre>\n\n\n<p>Test that unattended-upgrades parses the config without errors:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo unattended-upgrades --dry-run --debug 2&gt;&amp;1 | head -10<\/code><\/pre>\n\n\n<p>You should see it enumerate allowed origins (security, updates) and list any pending upgrades. No errors means the periodic job will run cleanly.<\/p>\n\n<h2>Step 2: Create a Non-Root Sudo User With SSH Keys<\/h2>\n\n<p>Logging in as root over SSH is a liability. Create a regular user, give it sudo, and push your public key.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo adduser devops\nsudo usermod -aG sudo devops<\/code><\/pre>\n\n\n<p>Set a strong password when prompted (pick something like <code>StrongPass123<\/code> only for a lab, use a proper generated password in production).<\/p>\n\n<p>From your <strong>local workstation<\/strong>, copy your SSH public key to the new user:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ssh-copy-id devops@10.0.1.50<\/code><\/pre>\n\n\n<p>Test the key login before you touch SSH config. If this fails, fix it before moving on, because the next step disables password auth.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ssh devops@10.0.1.50 id<\/code><\/pre>\n\n\n<p>You should see your UID and group list, including <code>sudo<\/code>. If you get a password prompt instead of a key login, your key was not accepted. Check <code>~\/.ssh\/authorized_keys<\/code> permissions on the server (must be 600 owned by the user).<\/p>\n\n<h2>Step 3: Harden SSH<\/h2>\n\n<p>Ubuntu 26.04 ships OpenSSH 10.2, which enables the ML-KEM post-quantum key exchange by default alongside classical curve25519. The modern defaults are already strong. Your job is to lock down authentication, limit who can log in, and move away from noisy defaults.<\/p>\n\n<p>Check the version first so you know what you are working with:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ssh -V<\/code><\/pre>\n\n\n<p>On a freshly patched Ubuntu 26.04 you will see something like this:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>OpenSSH_10.2p1 Ubuntu-2ubuntu3, OpenSSL 3.5.5 27 Jan 2026<\/code><\/pre>\n\n\n<p>Rather than editing the main <code>\/etc\/ssh\/sshd_config<\/code>, use a drop-in file. It survives package upgrades cleanly and makes your changes obvious to the next person to log in.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/ssh\/sshd_config.d\/99-hardening.conf &gt;\/dev\/null &lt;&lt;'EOF'\nPort 2202\nPermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nPubkeyAuthentication yes\nMaxAuthTries 3\nLoginGraceTime 20\nAllowUsers devops\nX11Forwarding no\nAllowAgentForwarding no\nAllowTcpForwarding no\nClientAliveInterval 300\nClientAliveCountMax 2\nEOF<\/code><\/pre>\n\n\n<p>Each line matters. Changing the port does not provide real security, but it cuts automated bot noise by 99% and keeps your logs readable. <code>MaxAuthTries 3<\/code> with <code>LoginGraceTime 20<\/code> gives attackers 20 seconds and three attempts per connection, then the socket closes. <code>AllowUsers<\/code> is a whitelist. Even if a new account is created later, SSH will refuse logins from it unless you update this file.<\/p>\n\n<p>Validate the syntax before reloading, because a broken config will kill sshd and kick you out.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo sshd -t &amp;&amp; echo \"config OK\"<\/code><\/pre>\n\n\n<p>If the output is just <code>config OK<\/code>, apply the changes. Ubuntu 26.04 uses socket activation for SSH, so you have to stop and disable the socket unit, then explicitly enable <code>ssh.service<\/code> for boot (it ships disabled by default \u2014 without this, SSH will not start after a reboot):<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl stop ssh.socket 2&gt;\/dev\/null\nsudo systemctl disable ssh.socket 2&gt;\/dev\/null\nsudo systemctl enable ssh\nsudo systemctl restart ssh<\/code><\/pre>\n\n\n<p>Confirm sshd is listening on the new port:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ss -tlnp | grep sshd<\/code><\/pre>\n\n\n<p>You should see port 2202 in the listen column. Open a second terminal and verify you can still log in over the new port (keep the original session alive until you are sure):<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>ssh -p 2202 devops@10.0.1.50<\/code><\/pre>\n\n\n<h2>Step 4: Enable UFW With Rate Limiting<\/h2>\n\n<p>Ubuntu ships <code>ufw<\/code> but leaves it disabled. Turn it on with a deny-by-default incoming policy. The <code>limit<\/code> verb adds connection rate limiting on top of the accept rule, which throttles brute-force attempts before Fail2ban even wakes up.<\/p>\n\n<p>Install if missing, then set defaults:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y ufw\nsudo ufw default deny incoming\nsudo ufw default allow outgoing<\/code><\/pre>\n\n\n<p>Allow your new SSH port with rate limiting. This caps new connections at 6 per 30 seconds from a single IP.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ufw limit 2202\/tcp comment \"SSH\"<\/code><\/pre>\n\n\n<p>If you run a web server, add 80 and 443. Open only what you actually need.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ufw allow 80\/tcp comment \"HTTP\"\nsudo ufw allow 443\/tcp comment \"HTTPS\"<\/code><\/pre>\n\n\n<p>Enable the firewall:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ufw enable<\/code><\/pre>\n\n\n<p>Verify the active rule set:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ufw status verbose<\/code><\/pre>\n\n\n<p>The output shows the policy and every rule with its IPv4 and IPv6 counterpart:<\/p>\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-ubuntu-2604-ufw-status-verbose.png\" alt=\"Ubuntu 26.04 UFW firewall status verbose output showing deny incoming and rate limited SSH rules\" title=\"\"><\/figure>\n\n\n<p>If you need to add a rule later, <code>ufw allow from 10.0.1.0\/24 to any port 5432<\/code> restricts a port to a specific subnet. That is far better than opening ports to the world.<\/p>\n\n<h2>Step 5: Install Fail2ban for SSH<\/h2>\n\n<p>UFW rate limits connections, but Fail2ban reads journald and bans IPs that trip authentication failures. The two work together well.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y fail2ban<\/code><\/pre>\n\n\n<p>Ubuntu&#8217;s Fail2ban defaults point at <code>\/var\/log\/auth.log<\/code>, but journald is the source of truth on modern Ubuntu. Create a jail override that uses the systemd backend and watches the hardened SSH port:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/fail2ban\/jail.d\/sshd.local &gt;\/dev\/null &lt;&lt;'EOF'\n[sshd]\nenabled = true\nport    = 2202\nmaxretry = 3\nfindtime = 10m\nbantime  = 1h\nbackend  = systemd\nEOF<\/code><\/pre>\n\n\n<p>Enable and start the service:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl enable --now fail2ban<\/code><\/pre>\n\n\n<p>Confirm the jail is active:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo fail2ban-client status sshd<\/code><\/pre>\n\n\n<p>The output confirms the filter is reading journald and that no IPs are currently banned:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>Status for the jail: sshd\n|- Filter\n|  |- Currently failed: 0\n|  |- Total failed:     0\n|  `- Journal matches:  _SYSTEMD_UNIT=ssh.service + _COMM=sshd\n`- Actions\n   |- Currently banned: 0\n   |- Total banned:     0\n   `- Banned IP list:<\/code><\/pre>\n\n\n<p>For more aggressive jails (web apps, mail, API endpoints), read our dedicated guides. The SSH jail alone stops 99% of drive-by attempts.<\/p>\n\n<h2>Step 6: Apply Kernel Sysctl Hardening<\/h2>\n\n<p>The Linux kernel has dozens of toggles that improve network and process security. Ubuntu sets some of them by default (ASLR is on, for example) but leaves many legacy options permissive for compatibility. Override them with a single drop-in file.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/sysctl.d\/99-hardening.conf &gt;\/dev\/null &lt;&lt;'EOF'\n# IP spoofing and routing protections\nnet.ipv4.conf.default.rp_filter = 1\nnet.ipv4.conf.all.rp_filter = 1\nnet.ipv4.conf.all.accept_source_route = 0\nnet.ipv6.conf.all.accept_source_route = 0\nnet.ipv4.conf.all.send_redirects = 0\nnet.ipv4.conf.default.send_redirects = 0\nnet.ipv4.conf.all.accept_redirects = 0\nnet.ipv6.conf.all.accept_redirects = 0\n\n# ICMP and SYN flood protection\nnet.ipv4.icmp_echo_ignore_broadcasts = 1\nnet.ipv4.tcp_syncookies = 1\nnet.ipv4.tcp_max_syn_backlog = 2048\nnet.ipv4.tcp_synack_retries = 2\nnet.ipv4.tcp_syn_retries = 5\nnet.ipv4.conf.all.log_martians = 1\n\n# Kernel and process hardening\nkernel.randomize_va_space = 2\nkernel.kptr_restrict = 2\nkernel.dmesg_restrict = 1\nkernel.yama.ptrace_scope = 1\nfs.protected_hardlinks = 1\nfs.protected_symlinks = 1\nfs.suid_dumpable = 0\nEOF<\/code><\/pre>\n\n\n<p>Apply it without rebooting:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo sysctl --system<\/code><\/pre>\n\n\n<p>The output lists every sysctl file loaded and each key applied. Scan it for any <code>Invalid argument<\/code> errors, which indicate a typo or an option that does not exist on your kernel.<\/p>\n\n<p>A quick spot-check on key values:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sysctl kernel.randomize_va_space kernel.kptr_restrict fs.protected_symlinks<\/code><\/pre>\n\n\n<p>Each must return its expected value (2, 2, 1 respectively). <code>kernel.randomize_va_space = 2<\/code> is full ASLR for both stack and heap. <code>kptr_restrict = 2<\/code> hides kernel pointers from <code>\/proc<\/code> even for root, which blunts several kernel exploits. <code>ptrace_scope = 1<\/code> means a process can only be attached to by its parent, which stops cross-session credential theft via <code>gdb<\/code>.<\/p>\n\n<h2>Step 7: Verify AppArmor Is Enforcing<\/h2>\n\n<p>AppArmor is the mandatory access control layer on Ubuntu. It ships enabled by default with a broad set of profiles for common services. Confirm it is loaded and check what is enforced:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo aa-status | head -8<\/code><\/pre>\n\n\n<p>A freshly patched Ubuntu 26.04 shows roughly 180 loaded profiles, with the majority in enforce mode:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>apparmor module is loaded.\n184 profiles are loaded.\n108 profiles are in enforce mode.\n76 profiles are in complain mode.\n0 profiles are in prompt mode.\n0 profiles are in kill mode.\n0 profiles are in unconfined mode.<\/code><\/pre>\n\n\n<p>Profiles in complain mode log violations but do not block. That is fine for profiles that are still being developed. If you install software that ships its own profile, check its mode and flip it to enforce once you have confirmed the service works:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo aa-enforce \/etc\/apparmor.d\/usr.sbin.nginx<\/code><\/pre>\n\n\n<p>Check the status of hardened services all at once. This is a good periodic health check:<\/p>\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-ubuntu-2604-aa-status-sshd-hardening.png\" alt=\"Ubuntu 26.04 security services status showing SSH Fail2ban auditd AppArmor active with enforce mode profiles\" title=\"\"><\/figure>\n\n\n<h2>Step 8: Audit Listening Services<\/h2>\n\n<p>Every open port is an attack surface. Map what is actually listening on the box before you trust your firewall rules.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ss -tlnp<\/code><\/pre>\n\n\n<p>You want to see only services you deliberately installed. On a minimal Ubuntu 26.04 server after these steps, that is sshd on 2202 and perhaps a DNS resolver on 127.0.0.53. If you find something unexpected (Avahi, CUPS, Samba from a desktop-flavored install), stop and disable it:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl disable --now cups avahi-daemon 2&gt;\/dev\/null<\/code><\/pre>\n\n\n<p>For a deeper view, check units that pull in network sockets:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>systemctl list-units --type=socket --state=listening<\/code><\/pre>\n\n\n<h2>Step 9: Enable auditd for Login and Privilege Tracking<\/h2>\n\n<p>Auditd records kernel-level events to <code>\/var\/log\/audit\/audit.log<\/code>. It is the backbone of most forensic and compliance workflows. Install it and add a small ruleset that covers the high-value events: changes to user and sudo config, SSH config edits, and every command executed as root by a non-root user.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y auditd\nsudo systemctl enable --now auditd<\/code><\/pre>\n\n\n<p>Drop the rules into the rules.d directory so they survive reboots:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo tee \/etc\/audit\/rules.d\/hardening.rules &gt;\/dev\/null &lt;&lt;'EOF'\n-w \/etc\/passwd -p wa -k passwd_changes\n-w \/etc\/shadow -p wa -k shadow_changes\n-w \/etc\/sudoers -p wa -k sudoers_changes\n-w \/etc\/ssh\/sshd_config -p wa -k sshd_config\n-w \/var\/log\/auth.log -p wa -k authlog\n-a always,exit -F arch=b64 -S execve -F euid=0 -F auid&gt;=1000 -F auid!=4294967295 -k root_cmds\nEOF\nsudo augenrules --load<\/code><\/pre>\n\n\n<p>Verify the rules loaded:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo auditctl -l<\/code><\/pre>\n\n\n<p>Test the <code>sudoers_changes<\/code> watch by running a harmless edit. Open and save <code>\/etc\/sudoers<\/code> via <code>visudo<\/code>, then query the log:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ausearch -k sudoers_changes -ts recent | head -20<\/code><\/pre>\n\n\n<p>You should see a fresh record with the user, timestamp, and the syscall that touched the file. That is forensic gold when you need to answer &#8220;who changed this, and when&#8221;.<\/p>\n\n<h2>Step 10: Run a Lynis Audit to Measure the Result<\/h2>\n\n<p>Lynis is a free security auditing tool that scores how well your system is hardened and prints specific recommendations. Install it from the Ubuntu repos (recent enough for baseline use, though the upstream version is newer):<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y lynis<\/code><\/pre>\n\n\n<p>Run a full audit. The first pass takes around 60 seconds on a small VM:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo lynis audit system --quick<\/code><\/pre>\n\n\n<p>Scroll to the summary block at the end. On the test VM after applying every step in this guide, Lynis reported a hardening index of 73 across 260 tests:<\/p>\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2026\/04\/wm-ubuntu-2604-lynis-audit-score.png\" alt=\"Ubuntu 26.04 Lynis audit system result showing hardening index 73 out of 100 with 260 tests performed\" title=\"\"><\/figure>\n\n\n<p>A hardening index of 73 on a baseline Ubuntu 26.04 is a solid starting point. Fresh installs typically land around 55 to 60. The remaining points come from things Lynis expects that are workload-dependent: a local mail relay, a second DNS resolver, PAM password quality rules, and disk encryption at rest. Review the suggestions:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo grep Suggestion \/var\/log\/lynis-report.dat | head -20<\/code><\/pre>\n\n\n<p>Apply the ones that make sense for your workload. Do not chase 100. An opinionated score of 80 on a machine that actually serves traffic beats a vanity 95 on a locked-down box that does nothing useful.<\/p>\n\n<h2>Step 11: Optional Extras Worth Adding<\/h2>\n\n<p>These are not strictly mandatory but come up repeatedly in production hardening reviews.<\/p>\n\n<p><strong>Process accounting<\/strong> logs every command run by every user to <code>\/var\/log\/account\/pacct<\/code>. Handy for incident review.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo apt install -y acct\nsudo systemctl enable --now acct\nlastcomm | head<\/code><\/pre>\n\n\n<p><strong>Disable core dumps<\/strong> globally. Core files often contain secrets (tokens, passwords held in memory) and attackers harvest them from crash directories.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>echo \"* hard core 0\" | sudo tee -a \/etc\/security\/limits.conf\necho \"fs.suid_dumpable = 0\" | sudo tee \/etc\/sysctl.d\/50-coredump.conf\nsudo sysctl -p \/etc\/sysctl.d\/50-coredump.conf<\/code><\/pre>\n\n\n<p><strong>Block USB mass storage<\/strong> on servers with physical access risk. This prevents someone walking up with a thumb drive and copying data, or loading malware.<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>echo \"blacklist usb-storage\" | sudo tee \/etc\/modprobe.d\/blacklist-usb-storage.conf\nsudo modprobe -r usb-storage 2&gt;\/dev\/null<\/code><\/pre>\n\n\n<p>Skip this one on laptops or workstations. It is only sensible on servers where USB is not a legitimate input path.<\/p>\n\n<h2>Troubleshooting: Issues I Hit During Testing<\/h2>\n\n<h3>Error: &#8220;Connection refused&#8221; after changing the SSH port<\/h3>\n\n<p>On Ubuntu 26.04, <code>ssh<\/code> is socket-activated. Simply restarting <code>ssh.service<\/code> will not pick up a new <code>Port<\/code> directive because <code>ssh.socket<\/code> is still listening on the old one. Stop and disable the socket unit, enable <code>ssh.service<\/code> so it starts on boot (it ships disabled), then restart the service:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo systemctl stop ssh.socket\nsudo systemctl disable ssh.socket\nsudo systemctl enable ssh\nsudo systemctl restart ssh<\/code><\/pre>\n\n\n<p>Confirm with <code>ss -tlnp | grep sshd<\/code>. If you still see the old port, your drop-in file is not being parsed, most commonly because of a typo or because <code>Include<\/code> is commented out in <code>\/etc\/ssh\/sshd_config<\/code>. If SSH works in this session but the server cannot be reached after a reboot, you skipped <code>systemctl enable ssh<\/code> \u2014 the socket unit is disabled, so unless the service itself is enabled, nothing listens on port 22 (or your custom port) at boot.<\/p>\n\n<h3>Error: &#8220;Operation not permitted&#8221; from UFW after enabling<\/h3>\n\n<p>This usually means another firewall (nftables rules from a container runtime, or iptables-nft conflict) has claimed the tables. Flush existing rules and enable UFW fresh:<\/p>\n\n\n<pre class=\"wp-block-code code\"><code>sudo ufw --force reset\nsudo ufw default deny incoming\nsudo ufw limit 2202\/tcp\nsudo ufw enable<\/code><\/pre>\n\n\n<p>If you run Docker, note that Docker manages its own iptables chains and its published ports bypass UFW by design. That is a separate topic covered in our <a href=\"https:\/\/computingforgeeks.com\/install-docker-ce-ubuntu-2604\/\" target=\"_blank\" rel=\"noreferrer noopener\">Docker on Ubuntu 26.04 guide<\/a>.<\/p>\n\n<h3>Fail2ban reports &#8220;No file(s) found for sshd&#8221;<\/h3>\n\n<p>This happens when the jail uses the default file backend on a system where rsyslog is not writing <code>\/var\/log\/auth.log<\/code>. The fix is to set <code>backend = systemd<\/code> in the jail, which is what our config above already does. If you inherit an older setup, edit <code>\/etc\/fail2ban\/jail.d\/sshd.local<\/code> and add that line, then restart fail2ban.<\/p>\n\n<h2>Keep Going From Here<\/h2>\n\n<p>Hardening is a direction, not a destination. A few natural next steps once the baseline above is in place:<\/p>\n\n<ul>\n<li>Put your web apps behind HTTPS with the <a href=\"https:\/\/computingforgeeks.com\/install-nginx-ubuntu-2604-lets-encrypt\/\" target=\"_blank\" rel=\"noreferrer noopener\">Nginx plus Let&#8217;s Encrypt setup on Ubuntu 26.04<\/a><\/li>\n<li>Stream auth and audit logs to a central collector. The <a href=\"https:\/\/computingforgeeks.com\/install-prometheus-ubuntu-2604\/\" target=\"_blank\" rel=\"noreferrer noopener\">Prometheus node exporter<\/a> plus <a href=\"https:\/\/computingforgeeks.com\/install-grafana-ubuntu-2604\/\" target=\"_blank\" rel=\"noreferrer noopener\">Grafana dashboards<\/a> covers host metrics, and pairing auditd with a log shipper covers the security side<\/li>\n<li>Add host-level monitoring with <a href=\"https:\/\/computingforgeeks.com\/install-nagios-ubuntu-2604\/\" target=\"_blank\" rel=\"noreferrer noopener\">Nagios on Ubuntu 26.04<\/a> or a similar tool so you actually notice when Fail2ban starts banning thousands of IPs at 3 AM<\/li>\n<li>Review the full <a href=\"https:\/\/computingforgeeks.com\/ubuntu-2604-lts-features\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ubuntu 26.04 LTS feature list<\/a> to understand what else changed in this release, especially the new default kernel and systemd versions<\/li>\n<\/ul>\n\n<p>Re-run <code>sudo lynis audit system<\/code> every few months. Workload changes, new services, and package updates all shift the score. A server that scored 78 six months ago might be at 62 today because someone installed a service that exposes a new port without firewalling it. The tool is cheap to run and the output is specific enough to act on immediately.<\/p>","protected":false},"excerpt":{"rendered":"<p>A freshly installed Ubuntu 26.04 server is reasonable out of the box, but it is not hardened. Default SSH settings, no firewall rules, no intrusion monitoring, no kernel tuning. Stick it on a public IP and you will see brute-force SSH attempts within minutes. This guide walks through a practical hardening baseline for Ubuntu 26.04 &#8230; <a title=\"How To Harden Ubuntu 26.04 LTS Server (Complete Security Guide)\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/harden-ubuntu-2604-server\/\" aria-label=\"Read more about How To Harden Ubuntu 26.04 LTS Server (Complete Security Guide)\">Read more<\/a><\/p>\n","protected":false},"author":28,"featured_media":166222,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[50,75,81],"tags":[282,205,2254,39816],"cfg_series":[39802],"class_list":["post-166223","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux-tutorials","category-security","category-ubuntu","tag-linux","tag-security","tag-ubuntu","tag-ubuntu-26-04","cfg_series-ubuntu-2604-security"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166223","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/28"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=166223"}],"version-history":[{"count":2,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166223\/revisions"}],"predecessor-version":[{"id":167479,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/166223\/revisions\/167479"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/166222"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=166223"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=166223"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=166223"},{"taxonomy":"cfg_series","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/cfg_series?post=166223"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}