A fresh Ubuntu server is not safe to expose to the internet as-is: it allows password logins, has no firewall policy, does not ban brute-force attempts, and does not patch itself. This walkthrough locks down a new Ubuntu server 22.04 or 24.04 host in seven steps. You will move SSH to key-only access, put a default-deny firewall in front of it with nftables, ban repeat offenders with fail2ban, tighten the kernel, turn on system auditing with auditd, and enable automatic security updates. Every step is copy-paste and includes a check so you know it worked.
One warning before you start: SSH and firewall changes can lock you out. Keep a second SSH session open to the server the whole time, and only close it once you have confirmed a fresh login still works.
Prerequisites
- A fresh Ubuntu 22.04 or 24.04 server with sudo access.
- An SSH key pair on your local machine. If you do not have one:
ssh-keygen -t ed25519
Step 1: update, and create a non-root sudo user
Patch first, then create a normal user so you can stop logging in as root:
sudo apt update && sudo apt upgrade -y
sudo adduser deploy
sudo usermod -aG sudo deployFrom your local machine, install your public key for that user, then confirm you can log in as deploy:
ssh-copy-id deploy@<server-ip>
ssh deploy@<server-ip>Do not continue until that login works. The next step disables password login, so key access has to be in place first.
Step 2: harden SSH
Ubuntu reads drop-in config from /etc/ssh/sshd_config.d/, which is cleaner than editing the main file. Create a hardening file:
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers deploy
ClientAliveInterval 300
ClientAliveCountMax 2
EOFValidate the syntax before applying it, then reload:
sudo sshd -t
sudo systemctl reload sshIf sshd -t prints nothing, the config is valid. Now open a new terminal and confirm a key login still works and that password login is refused:
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password deploy@<server-ip>That command should be denied. Root login should also be refused.
Step 3: put a default-deny firewall in front with nftables
Ubuntu ships nftables. Write a ruleset that drops everything inbound except loopback, established connections, ICMP at a sane rate, SSH (rate-limited), and web traffic:
sudo tee /etc/nftables.conf > /dev/null <<'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
ip protocol icmp icmp type echo-request limit rate 5/second accept
ip6 nexthdr ipv6-icmp accept
tcp dport 22 ct state new limit rate 10/minute accept
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
EOFLoad it and enable it at boot:
sudo nft -f /etc/nftables.conf
sudo systemctl enable --now nftables
sudo nft list rulesetThe policy drop on the input chain is the important part: anything you did not explicitly allow is blocked. Confirm your open SSH session survived and a new one connects before moving on. If you do not serve web traffic, remove the port 80 and 443 line.
Step 4: ban brute-force attempts with fail2ban
fail2ban reads the logs and bans IPs that fail to authenticate repeatedly:
sudo apt install -y fail2banCreate a local jail that watches SSH via the systemd journal and bans through nftables:
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
backend = systemd
banaction = nftables-multiport
[sshd]
enabled = true
port = ssh
EOFRestart and check the jail is active:
sudo systemctl restart fail2ban
sudo fail2ban-client status sshdYou will see the jail status and, over time, a list of banned IPs. backend = systemd matters on current Ubuntu because SSH logs to the journal, not a flat file.
Step 5: tighten the kernel with sysctl
A short set of network and memory hardening defaults:
sudo tee /etc/sysctl.d/99-hardening.conf > /dev/null <<'EOF'
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_redirects = 0
kernel.randomize_va_space = 2
EOF
sudo sysctl --systemThe final command applies the settings and prints each one back so you can confirm they took.
Step 6: turn on system auditing with auditd
auditd records who did what, which is what you need when you are reconstructing an incident:
sudo apt install -y auditd audispd-plugins
sudo systemctl enable --now auditdAdd rules that watch the files an attacker would touch, track kernel module loading, and then lock the rule set:
sudo tee /etc/audit/rules.d/hardening.rules > /dev/null <<'EOF'
-w /etc/passwd -p wa -k passwd_changes
-w /etc/shadow -p wa -k shadow_changes
-w /etc/sudoers -p wa -k sudoers_changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules
-e 2
EOF
sudo augenrules --load
sudo auditctl -lTest it by touching a watched file and searching the audit log:
sudo touch /etc/sudoers.d/zzz-test && sudo rm /etc/sudoers.d/zzz-test
sudo ausearch -k sudoers_changes -i | tailNote that -e 2 makes the rules immutable until the next reboot. That is deliberate: an attacker cannot quietly disable auditing. If you need to edit rules, you have to reboot first.
Step 7: enable automatic security updates
Unpatched software is the most common way in. Turn on unattended security upgrades:
sudo apt install -y unattended-upgrades
sudo tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOFConfirm what it would do without applying anything yet:
sudo unattended-upgrade --dry-run --debug
By default it installs security-pocket updates only. If you want it to reboot automatically after a kernel update, add these two lines to /etc/apt/apt.conf.d/50unattended-upgrades and pick a quiet hour:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";What goes wrong
Locked out of SSH. Almost always one of three things: you disabled password auth before the key worked, you reloaded a config that failed sshd -t, or the nftables ruleset has no rule allowing port 22. Keep a second session open and test every change from a third one before trusting it.
nftables rules vanish after reboot. You loaded them with nft -f but did not enable the service. sudo systemctl enable --now nftables makes /etc/nftables.conf load at boot.
fail2ban never bans anyone. The usual cause is the wrong backend. On current Ubuntu, SSH logs go to the journal, so backend = systemd is required. If SSH runs on a non-standard port, set it in the [sshd] jail.
Cannot edit audit rules. That is -e 2 doing its job. Reboot, edit, reload, re-lock.
Auto-reboot at a bad time. If you enable automatic reboot, set the time to a maintenance window. A production box rebooting at noon because of a kernel update is its own kind of incident.