Server SecurityGrid Pulsar team··11 min read

Ubuntu 24.04 LTS Hardening Checklist: Verify Every Step

Harden Ubuntu 24.04 LTS on a single VPS with SSH keys, firewall rules, security updates, restricted access, and recovery checks you can verify yourself.

Ubuntu 24.04 LTS Hardening Checklist: Verify Every Step featured image
On this page
Short answer

You disable password login, close your terminal, and discover that your SSH key never worked. Start hardening Ubuntu 24.04 LTS by proving a separate key-based login and console recovery path. Then restrict SSH, enable a firewall, configure security updates, and review service permissions. Verify each change through effective configuration and fresh connections, and finish with a restore rehearsal before trusting the server with production data.

Prove you can reconnect before changing access

Your existing SSH session still works, but a new terminal returns permission denied after an authentication change. Prevent that lockout by proving key-based access before editing SSH, and keep the original session open until every access check passes. Open your VPS console now and confirm that it offers a usable recovery path.

Use a normal account with working sudo access on Ubuntu 24.04 LTS. The laptop examples call that account deploy and use the documentation address 203.0.113.10; replace those values with your account and server address. If you still depend on root login, complete the account and key preparation in our first-hour Ubuntu server setup guide before continuing.

Run this connection check from your laptop using the key you intend to keep. Disabling connection sharing forces a fresh authentication attempt instead of reusing an existing authenticated connection. A private-key passphrase prompt is acceptable; falling back to the server account password is deliberately disabled.

bash
ssh -o ControlMaster=no -o ControlPath=none -o PreferredAuthentications=publickey -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no [email protected]
Check: A fresh shell opens under your normal account using public-key authentication. Confirm that sudo works inside that session, and leave the original administrative session open.

Install the utilities used below from the server session. Authenticate sudo before beginning if your account requires a password. These commands assume a fresh VPS; on an existing production machine, first record which applications must remain reachable.

bashTested on Ubuntu 24.04
sudo apt-get update
sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server ufw unattended-upgrades apparmor-utils
sudo /usr/sbin/sshd -t
Check: Package installation completes without errors, and sshd -t exits successfully without diagnostic output. Resolve package or SSH configuration errors before changing access.

Disable SSH passwords and direct root login

Once a fresh key-based login works, remove password authentication and direct root access. Ubuntu reads SSH configuration snippets near the beginning of its main configuration, and the first value generally wins. An apparently correct setting can therefore lose to an earlier snippet, as explained in Ubuntu's OpenSSH documentation.

Save a protected copy of the existing configuration before editing it. The command below preserves an existing backup rather than replacing it on a repeat run. Keep its location in your recovery notes so you can restore access through the console.

bashTested on Ubuntu 24.04
sudo cp -an /etc/ssh /root/ssh-before-hardening
Check: The command completes without errors. Confirm that /root/ssh-before-hardening/sshd_config exists before editing the live configuration.

Save this configuration as /etc/ssh/sshd_config.d/00-local-hardening.conf, owned by root and writable only by root. Inspect earlier snippets and any Match sections for conflicting authentication rules. This baseline assumes public-key authentication without an interactive additional authentication method.

conf
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

Validate the configuration before reloading the running service. A reload keeps your recovery session available while new connections use the updated settings. If validation fails, correct the configuration before attempting another reload.

bashTested on Ubuntu 24.04
sudo /usr/sbin/sshd -t && sudo systemctl reload ssh.service
sudo /usr/sbin/sshd -T | grep -E '^(pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|permitrootlogin) '
Check: Validation and reload succeed. Effective configuration shows pubkeyauthentication yes, passwordauthentication no, kbdinteractiveauthentication no, and permitrootlogin no. Repeat the laptop connection check successfully before closing your recovery session.

If you use conditional SSH rules, inspect the effective configuration for the actual account and connecting address with sshd -T -C. Treat the global output as incomplete evidence for those accounts. Our SSH hardening guide for servers with agent keys covers separating human access from automation.

Allow SSH before enabling the firewall

A database bound to a public interface can become reachable before you notice it. Start with incoming connections denied and explicitly allow SSH. UFW supplies a manageable host firewall, but installation alone does not activate it; see Ubuntu's firewall documentation.

The following commands allow the standard SSH service and, when running through SSH, the destination port of your current connection. Keep outbound traffic allowed for this baseline so package retrieval and application dependencies continue working. If you run these commands from a console with a custom SSH port, add that port's allow rule before enabling UFW.

bashTested on Ubuntu 24.04
sudo ufw allow OpenSSH
if [ -n "${SSH_CONNECTION:-}" ]; then
  hardening_ssh_port="${SSH_CONNECTION##* }"
  sudo ufw allow "${hardening_ssh_port}/tcp"
fi
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw --force enable
sudo ufw status verbose
Check: UFW reports Status: active, a deny policy for incoming traffic, and an allow rule for your SSH port. Open another fresh SSH connection successfully; an existing session alone does not prove that new connections are allowed.

For a public web server, allow HTTP and HTTPS before directing users to it. Skip this block if the machine has no public web service. A firewall exception permits traffic but does not create a listener.

bashTested on Ubuntu 24.04
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose
Check: UFW shows allow rules for the web ports and retains the SSH rule. Your public application remains reachable after the change.

Inspect any provider firewall separately, because a host rule cannot override an upstream block. Review both address families when the VPS has multiple public addresses. On an existing server, audit old allow rules too: changing the default policy does not remove them.

Verify exposure from your laptop, with Nmap installed, against your own server. Repeat against every public address and inspect container port mappings separately if you deploy containers. Record the intended public services before interpreting the scan.

bash
nmap -Pn -sT -p- 203.0.113.10
Check: The scan reports only intended TCP listeners as open. Closed ports have no accepting listener, while filtered results indicate filtering or another network obstacle; this scan does not verify UDP exposure.

Enable security updates and choose a reboot policy

A hardened configuration still leaves you exposed when vulnerable packages stay installed. Refresh package information and apply pending upgrades during a maintenance window. Package upgrades can restart services, so check application health afterward even when SSH stays connected.

bashTested on Ubuntu 24.04
sudo apt-get update
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold upgrade
sudo dpkg --audit
Check: APT completes without errors, and dpkg --audit produces no report of incomplete package installation. Investigate held-back packages separately and confirm that your application still answers its normal health check.

Enable daily automatic package refreshes and unattended upgrades while keeping automatic rebooting disabled. Retain the distribution's security origins unless you have reviewed a specific additional repository. Adding a repository does not automatically include it in unattended updates, as Ubuntu's automatic-update documentation explains.

bashTested on Ubuntu 24.04
sudo tee /etc/apt/apt.conf.d/99-local-hardening >/dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Unattended-Upgrade::Automatic-Reboot "false";
EOF
sudo systemctl enable --now apt-daily.timer apt-daily-upgrade.timer
apt-config dump | grep -E 'APT::Periodic::(Update-Package-Lists|Unattended-Upgrade)|Unattended-Upgrade::Automatic-Reboot'
sudo unattended-upgrade --dry-run --debug
Check: Effective APT configuration shows both periodic settings enabled and automatic rebooting set to false. The dry run completes without errors and includes the distribution security origin among its allowed sources. No eligible packages is a valid result.

Assign someone responsibility for pending reboots instead of leaving them indefinitely. Check for /var/run/reboot-required after maintenance and arrange a console-backed restart when it exists. Application runtimes installed outside APT need their own update procedure; this package policy does not maintain them.

Inspect listeners and confirm AppArmor enforcement

A service you forgot about is still part of your attack surface. Inspect listening sockets and running services, then identify the owner and purpose of each listener. Pay particular attention to databases and development servers that should accept only local connections.

bashTested on Ubuntu 24.04
sudo ss -lntup
systemctl list-units --type=service --state=running --no-pager
Check: Every listening socket has an understood purpose. Services intended only for local access bind to loopback addresses or use local sockets, and every wildcard listener has an explicit reason to exist.

Disable an unnecessary service only after checking what depends on it, then repeat the socket inspection. For services you need, change the application's bind address rather than relying entirely on a firewall. Avoid copying a generic service-removal script onto a VPS whose networking and recovery components you have not identified.

AppArmor limits what profiled applications can access, and Ubuntu normally loads it by default. Enforcement is per profile: complain mode records violations without blocking them. Use Ubuntu's AppArmor documentation to interpret the status before changing a profile.

bash
sudo aa-status
Check: On the VPS itself, aa-status reports that AppArmor is loaded and identifies profiles in enforce mode. Confirm that any application you intend to confine actually has an enforcing profile; loaded AppArmor alone does not establish application coverage.

Run that check on the host, not inside an application container or restricted build environment. If a profile blocks legitimate work, inspect the denial and adjust the narrow rule involved. We leave unrelated kernel tuning alone until a workload-specific requirement justifies a change and a recovery plan exists.

Separate administrative access from agent access

An old deployment account with unrestricted sudo can undo every earlier restriction. Review privileged accounts and remove obsolete SSH keys after confirming who still uses them. Keep a human recovery identity separate from deployment automation so revoking automation does not also remove your administrative access.

bashTested on Ubuntu 24.04
getent passwd | awk -F: '$3 == 0 { print $1 }'
getent group sudo
sudo -l
sudo visudo -c
Check: Only root has the root user identifier unless you have deliberately documented an exception. The sudo group has no unexplained members, your effective sudo permissions match your role, and visudo -c reports valid syntax.

Group membership is only part of the review: inspect direct grants in /etc/sudoers.d/ and check each automation account's effective permissions. A syntactically valid rule can still grant unrestricted control. Also inspect access to privileged service sockets and writable files that root executes.

We recommend Claude Code for assisted operations, beginning with inspection and proposed changes. Run it through a dedicated identity whose operating-system permissions enforce the intended scope. Read-only access is still sensitive when it includes application secrets, so expose only the files the task requires.

  • Require human approval for SSH, firewall, account, and sudo policy changes.
  • Keep recovery credentials and private SSH keys outside the agent's accessible files.
  • Allow privileged actions only through narrowly scoped, root-owned commands whose inputs cannot expand into arbitrary execution.
  • Review configuration diffs and run the relevant validator before applying a privileged change.
Check: The automation identity's effective permissions exclude unrestricted sudo and privileged sockets. Any privileged helper and its parent directories are unwritable by that identity, and the human recovery login still succeeds independently.

An approval prompt is a workflow control; account permissions determine what a compromised process can actually do. Avoid granting broad access to interpreters or editors through sudo. Use our production guardrails for AI agents when turning repeated maintenance tasks into approved operations.

Retain useful logs and verify an alert reaches you

A failed login record is useful only if it survives long enough for someone to investigate. Configure persistent journal storage with a bounded disk budget. The size below is an example limit for local logs; adjust it to your available space and investigation needs.

bashTested on Ubuntu 24.04
sudo mkdir -p /etc/systemd/journald.conf.d
sudo tee /etc/systemd/journald.conf.d/60-local-retention.conf >/dev/null <<'EOF'
[Journal]
Storage=persistent
SystemMaxUse=200M
EOF
sudo systemctl restart systemd-journald
sudo journalctl --flush
sudo journalctl --disk-usage
sudo journalctl -u ssh.service --since today --no-pager
Check: The journal flush succeeds and disk usage is reported. After a fresh SSH login, the SSH journal contains the corresponding authentication event; after the planned reboot, confirm that the previous boot's journal remains accessible.

Set an external availability check for your public application and route failures to a channel you actually read. Add notification for backup failures and overdue maintenance according to your monitoring setup. Trigger the monitoring service's test notification and acknowledge its arrival before treating the alert as operational.

Check: The test notification arrives at the intended destination and identifies the affected server or service. A dashboard showing a healthy status without a successful notification test is incomplete evidence.

We treat automated login bans as an optional follow-up once key-only SSH and exposure checks work. Choose additional tooling only when you can explain which events it will detect and how you will recover from a mistaken block. Our Fail2ban and CrowdSec comparison for a single server helps with that decision.

Schedule a restore rehearsal and complete a controlled reboot

Your next task is to restore the application somewhere isolated, using the backup you intend to trust. A backup job reporting success does not establish that the database can be opened or the application can start. Use a disposable machine or isolated environment that cannot send production messages or alter live data.

Keep recoverable copies outside the VPS and protect their credentials separately from the production account. Use the database's supported backup mechanism or a documented consistent snapshot procedure. Copying changing database files without coordinating consistency can leave you with an archive that cannot be restored.

  1. Restore application files and database contents into the isolated environment.
  2. Retrieve required secrets through your recovery process without copying them into chat or shell history.
  3. Start the application and verify a representative read and write against restored data.
  4. Record the recovery steps, any missing dependency, and who owns the next rehearsal.
Check: The restored application starts, reads expected data, and completes a disposable write successfully. The recovery procedure works without depending on files or credentials available only on the original VPS.

Then book a maintenance window to verify that the hardened server survives a restart. Keep the provider console available and confirm that expected services are configured to start at boot. Run the reboot only when you are ready for the application interruption and SSH disconnection.

bash
sudo systemctl reboot
Check: After boot, a fresh key-based SSH connection succeeds and the application answers its health check. Confirm that UFW remains active, AppArmor still enforces the intended profiles, update timers are active, and sudo journalctl -b -1 --no-pager can read the previous boot.

Put the next restore rehearsal on your operations calendar before leaving this task. Assign an owner to any failed check and resolve access or recovery failures before depending on unattended maintenance. Repeat the exposure check whenever you add a service or change how it publishes ports.

FAQ

What should an Ubuntu server hardening checklist include?

Start with the controls that prevent loss of access and reduce public exposure, then cover ongoing maintenance and recovery. Each control needs evidence: a new authenticated connection, an effective configuration value, or a successful recovery action. A checklist that merely says to install security packages misses whether those packages are active and whether you can recover when a change fails.

Is Ubuntu 24.04 LTS secure enough for a production VPS?

It can be a suitable production base when you maintain the operating system and configure the workload carefully. The image name cannot tell you whether your database is public or an automation account has unrestricted privileges. Review the application and its deployment process alongside the host. Operating-system hardening cannot repair broken application authorization or protect secrets that the application exposes.

Should I change the SSH port when hardening Ubuntu?

We would spend the effort on reliable key authentication and account separation first. A different port adds another setting to maintain across clients, firewalls, and recovery notes. If you change it, keep the old path available until a fresh connection to the new listener succeeds. Check the actual listening socket as well as the SSH configuration before removing the old firewall rule.

Do I need Fail2ban if SSH passwords are disabled?

Decide based on an explicit operational need, such as repeated unwanted attempts that obscure useful logs. Automated bans introduce another access-control component that you must understand and recover from. They do not revoke a stolen valid key or restrict what an authenticated account can execute. We would complete authentication and privilege checks before adding a banning service to a small VPS.

Can Claude Code safely harden my Ubuntu server?

Claude Code can help inspect configuration and prepare changes, but its access should match the task. Use a dedicated account, review privileged modifications, and keep a separate human recovery path. For access-control changes, require the agent to explain the expected verification result before execution. Confirm the result yourself through a fresh connection rather than accepting a successful command exit as complete proof.

Free ebook

Put this guide in the bigger picture

See how this topic connects to the rest of your product. Our free field guide explains apps, infrastructure and AI, with checklists to help you make business decisions.

We use cookies to ensure that we give you the best experience on our website. If you continue to use this site we will assume that you are happy with it.