How to Harden an Ubuntu 24.04 Server

Serdar Tekin
Serdar Tekin
Co-Founder & CTO
IntermediateUpdated Oct 27, 202624 min read~45+ minutes total
Ubuntu 24.04Server HardeningSecuritySSHUFWAppArmorLinux
Last reviewed:
Tutorial
How to Harden an Ubuntu 24.04 Server
rafftechnologies.com/learn
On this page

Prerequisites

  • An Ubuntu 24.04 server with administrative access, a non-root sudo account or permission to create one, a second terminal for SSH testing, and a provider console/rescue or equivalent recovery path

Don’t have a server yet?

Ubuntu 24.04 is reasonably secure as a fresh installation, but production hardening is about reducing attack surface and making access, updates, network exposure, application confinement, logging, and recovery deliberate. A useful baseline does not come from copying a long list of random sysctl values or disabling every feature. It comes from verifying which users, services, ports, packages, and recovery paths your server actually needs.

The original tutorial used a Raff Linux VM running Ubuntu 24.04. The following steps cover security controls that an administrator must configure and verify inside a self-managed VM. The saved tested-on record remains Ubuntu 24.04 LTS on Raff 2 vCPU / 2 GB RAM Linux VM; OpenSSH, UFW, unattended-upgrades, Fail2Ban, and recovery guidance reviewed in July 2026. This revision re-verifies the workflow against current Ubuntu Server documentation on September 5, 2026 without claiming a new end-to-end machine test.

This tutorial hardens a general-purpose internet-connected Ubuntu server with a non-root administrator, tested SSH keys, safe OpenSSH drop-ins, a deny-by-default UFW policy, verified automatic security updates, service/account review, AppArmor, optional Fail2Ban, logging, and a tested recovery plan. It intentionally avoids hard-coded SSH cipher lists, cargo-cult kernel tweaks, and claims of CIS/FIPS/compliance. Those controls require workload-specific testing and, where applicable, the appropriate compliance tooling.

Prerequisites:

  • An Ubuntu 24.04 server with working administrative access
  • A non-root sudo-capable account, or permission to create one
  • A second terminal for testing new SSH sessions
  • A provider console/rescue path or another recovery method
  • Knowledge of the applications and ports the server must keep

Step 1 — Inventory the server before changing security controls

Start by recording the server state you are about to change.

Confirm Ubuntu:

Bash
cat /etc/os-release uname -r

Check the current user and sudo capability:

Bash
id sudo -v

List listening sockets:

Bash
sudo ss -tulpn

List failed and enabled services:

Bash
systemctl --failed systemctl list-unit-files --type=service --state=enabled

Check the current SSH port and effective authentication settings:

Bash
sudo sshd -T | grep -E \ '^(port|permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication) '

Check UFW and AppArmor:

Bash
sudo ufw status verbose sudo aa-status

Do not change a control until you know which production dependency it could affect. For example, an application may need a public listener, a backup agent may require outbound access, and a deployment system may rely on a specific SSH account.

Verify: You should have a clear list of current administrators, listening ports, enabled services, SSH settings, firewall state, and whether AppArmor is loaded.

Step 2 — Apply pending Ubuntu updates and plan required reboots

Refresh package metadata and install the updates your maintenance policy allows:

Bash
sudo apt update sudo apt upgrade

Review packages before accepting the upgrade on a production server instead of automatically using -y when you need change control.

Check whether a reboot is required:

Bash
if [ -f /var/run/reboot-required ]; then cat /var/run/reboot-required else echo "No reboot required" fi

If a reboot is required, schedule it when the workload can tolerate it and confirm the server returns healthy afterward.

Review configured repositories:

Bash
find /etc/apt/sources.list.d -maxdepth 1 -type f -print

Minimize third-party repositories. Ubuntu's current security guidance explicitly recommends avoiding unnecessary third-party package sources because every additional repository adds a software supply-chain and maintenance dependency.

Do not remove packages just because they look unfamiliar. Package dependencies, cloud agents, networking, boot, and storage components can be critical even when they are not obvious application dependencies.

Verify: apt update should complete successfully, required upgrades should be understood/applied according to policy, and you should know whether a reboot is pending.

Step 3 — Use a dedicated non-root administrator and verify sudo access

If your image already provides a non-root sudo account, keep it and verify it instead of creating another unnecessary administrator:

Bash
id sudo whoami

The second command should return:

Text
root

If you are currently administering the machine only as root, create a replacement account. Replace deploy with your chosen username:

Bash
sudo adduser deploy sudo usermod -aG sudo deploy

Verify membership:

Bash
id deploy

Then test the new account in a separate session before changing root SSH access:

Bash
ssh deploy@your_server_ip sudo whoami

Use separate administrator identities where practical. Sharing one Linux account or one private key between several people makes revocation and log attribution harder.

Review current sudo members:

Bash
getent group sudo

Verify: At least one non-root administrator should have a tested SSH path and working sudo, and every sudo member should have a current operational reason for that access.

Step 4 — Install and test SSH key authentication before disabling passwords

Generate an Ed25519 key on the administrator's local computer if needed:

Bash
ssh-keygen -t ed25519 -C "ubuntu-admin"

Copy only the public key to the server:

Bash
ssh-copy-id your_user@your_server_ip

On the server, review key permissions:

Bash
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chown -R "$USER":"$USER" ~/.ssh

Open a new terminal and verify key authentication:

Bash
ssh your_user@your_server_ip

Do not disable password authentication, root login, or the old working access method until this separate key-based session succeeds.

For key rotation, multiple administrators, and client configuration, use Generate SSH Keys on Ubuntu 24.04.

Verify: A new independent SSH session should authenticate with the intended public key and the account should still have working sudo access.

Step 5 — Harden OpenSSH with an early drop-in and verify the effective configuration

Ubuntu places this line near the top of /etc/ssh/sshd_config:

Text
Include /etc/ssh/sshd_config.d/*.conf

OpenSSH uses the first obtained value for most directives. That means a late file name such as 99-hardening.conf does not necessarily override an earlier snippet. Inspect existing snippets first:

Bash
sudo ls -1 /etc/ssh/sshd_config.d/ sudo grep -RniE \ '^(PermitRootLogin|PasswordAuthentication|KbdInteractiveAuthentication|PubkeyAuthentication|MaxAuthTries|LoginGraceTime)' \ /etc/ssh/sshd_config /etc/ssh/sshd_config.d 2>/dev/null || true

Create an early local hardening snippet:

Bash
sudo tee /etc/ssh/sshd_config.d/00-raff-hardening.conf > /dev/null <<'EOF' PermitRootLogin no PubkeyAuthentication yes PasswordAuthentication no KbdInteractiveAuthentication no MaxAuthTries 3 LoginGraceTime 30 EOF

Protect it with normal root-owned configuration permissions:

Bash
sudo chown root:root /etc/ssh/sshd_config.d/00-raff-hardening.conf sudo chmod 644 /etc/ssh/sshd_config.d/00-raff-hardening.conf

Validate syntax:

Bash
sudo sshd -t

Then verify effective values, which is more important than merely reading the file:

Bash
sudo sshd -T | grep -E \ '^(permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|maxauthtries|logingracetime) '

If your configuration uses Match blocks, evaluate the intended user/context as well:

Bash
sudo sshd -T -C user="$USER",host="$(hostname)",addr=127.0.0.1 | \ grep -E '^(permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|maxauthtries|logingracetime) '

Restart SSH only after validation:

Bash
sudo systemctl restart ssh.service

Keep the original session open. Test a new key-based login again, then confirm a password-only attempt is rejected:

Bash
ssh -o PreferredAuthentications=password \ -o PubkeyAuthentication=no \ your_user@your_server_ip

Do not hard-code custom Ciphers, MACs, or KexAlgorithms in a general baseline unless a defined security/compliance requirement calls for it. Ubuntu's OpenSSH defaults evolve with security updates; custom crypto lists need compatibility testing and long-term maintenance.

Verify: sshd -t should pass, sshd -T should show the intended settings, a fresh key login should work, and password-only/root SSH access should not provide an unintended login path.

Step 6 — Configure UFW without risking SSH lockout

Determine the effective SSH port:

Bash
sudo sshd -T | awk '$1 == "port" {print $2}'

If SSH uses the standard OpenSSH profile, inspect and allow it before enabling UFW:

Bash
sudo ufw app info OpenSSH sudo ufw allow OpenSSH comment 'SSH administration'

For a custom SSH port, allow the actual port instead:

Bash
sudo ufw allow 2222/tcp comment 'SSH administration'

Set a common application-server baseline:

Bash
sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw default deny routed

Review before enabling:

Bash
sudo ufw show added sudo ufw --dry-run enable

Then enable and inspect:

Bash
sudo ufw enable sudo ufw status verbose

Open another new SSH connection before closing existing sessions.

Only add application ports when a real service requires them. For example, a public web server may need TCP 80/443; a private database normally should not be opened to Anywhere.

For source restrictions, IPv6, rule ordering, Docker caveats, and rollback, use Set Up UFW Firewall on Ubuntu 24.04.

Verify: UFW should be active with deny-incoming/deny-routed defaults, the correct SSH rule should exist, and a second SSH connection should still succeed.

Step 7 — Verify automatic security updates instead of overwriting Ubuntu defaults blindly

Ubuntu Server uses unattended-upgrades for automatic security updates and current Ubuntu documentation states that security updates are applied automatically by default.

Check whether the package is installed:

Bash
dpkg -s unattended-upgrades 2>/dev/null | grep '^Status:' || true

Install it only if it is absent:

Bash
sudo apt install -y unattended-upgrades

Inspect the periodic settings:

Bash
apt-config dump | grep -E \ 'APT::Periodic::(Update-Package-Lists|Unattended-Upgrade)'

Inspect the timers that drive APT maintenance:

Bash
systemctl list-timers apt-daily.timer apt-daily-upgrade.timer --all

Review the actual unattended-upgrades policy:

Bash
sudo less /etc/apt/apt.conf.d/50unattended-upgrades

Test it without installing packages:

Bash
sudo unattended-upgrade --dry-run --debug

Review recent service activity:

Bash
sudo journalctl -u unattended-upgrades --since '7 days ago' --no-pager

Do not assume third-party repositories are included just because they exist in APT. Ubuntu's current documentation explicitly notes that adding a repository does not automatically make unattended-upgrades select packages from that origin.

Automatic reboot behavior is workload-dependent. Do not enable unattended reboots blindly on a single production node; define a maintenance/redundancy strategy first.

Verify: The APT timers should be scheduled, unattended security upgrades should be enabled according to your policy, the dry run should complete without configuration errors, and third-party origins should be consciously included or excluded.

Step 8 — Remove or disable only services you have positively identified as unnecessary

Audit listeners again:

Bash
sudo ss -tulpn

Review enabled services:

Bash
systemctl list-unit-files --type=service --state=enabled

Inspect a service before disabling it:

Bash
systemctl status service-name systemctl cat service-name

Only after confirming it is not needed:

Bash
sudo systemctl disable --now service-name

Review package cleanup candidates without immediately deleting them:

Bash
sudo apt autoremove --dry-run

Do not use a generic hardening list to remove packages, disable IPv6, disable ICMP, turn off forwarding, or change kernel parameters without understanding the server role. Those actions can break containers, VPNs, routing, monitoring, clustering, cloud networking, and application dependencies.

For each public listener, answer four questions:

  1. Does the service need to run?
  2. Does it need to listen on this interface?
  3. Does it need a public firewall rule?
  4. How is it authenticated and updated?

Verify: Every remaining listening service and enabled boot service should have a documented operational purpose, and no unreviewed package/service should have been removed merely because it looked unfamiliar.

Step 9 — Keep AppArmor enabled and inspect confinement status

AppArmor is Ubuntu's mandatory access control system. Current Ubuntu documentation states that it is installed and loaded by default, and strongly recommends using it as a defense-in-depth layer.

Check its state:

Bash
sudo aa-status

Also check the profile-loading service:

Bash
systemctl is-enabled apparmor.service systemctl status apparmor.service --no-pager

Review recent AppArmor denials:

Bash
sudo journalctl -k -g 'apparmor="DENIED"' --since '24 hours ago' --no-pager

Do not disable AppArmor globally because one application fails. Ubuntu 24.04 integrates AppArmor more deeply into the kernel than older releases, and disabling it reduces the system's security posture. First determine which profile denied which operation, then make a narrow local profile adjustment when the application genuinely requires that access.

Optional profile-management tools are available through:

Bash
sudo apt install apparmor-utils

Do not force experimental profiles into enforcement on a production application without testing. complain mode is useful when developing or validating a profile; enforce mode blocks violations.

Verify: aa-status should show AppArmor loaded with profiles present, and any DENIED events should be understood rather than worked around by disabling AppArmor wholesale.

Step 10 — Audit administrator accounts, SSH keys, and stale access

List users with interactive shells:

Bash
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $6, $7}' /etc/passwd

Review sudo access:

Bash
getent group sudo

Find authorized-key files under normal home directories:

Bash
sudo find /home -maxdepth 3 -type f -name authorized_keys -print

Review each administrative user's key file with line numbers, for example:

Bash
sudo nl -ba /home/deploy/.ssh/authorized_keys

Disabling a user's Unix password does not revoke an already-authorized SSH public key. When removing access, audit both the account and its authorized_keys, active sessions, deployment systems, CI/CD credentials, API tokens, and any external identity source.

If multiple people administer the server, use individual accounts/keys rather than one shared private key.

Do not add an OpenSSH AllowUsers/AllowGroups rule until you have inventoried every required human and automation identity; an incomplete allowlist is a common lockout source.

Verify: Every interactive account, sudo member, and authorized SSH key should map to a current owner or automation purpose, with stale access removed through a tested change process.

Step 11 — Add Fail2Ban only when it solves a real log-based attack surface

Fail2Ban is optional. It watches authentication/log events and temporarily blocks sources that repeatedly match failure patterns. It is not a substitute for SSH keys, firewall restrictions, timely updates, or upstream DDoS controls.

If your public SSH service receives repeated authentication noise and Fail2Ban fits the threat model, use the dedicated tutorial:

Install Fail2Ban on Ubuntu 24.04 for SSH Protection

At minimum, if Fail2Ban is already installed, verify the service and SSH jail:

Bash
systemctl is-active fail2ban sudo fail2ban-client status sudo fail2ban-client status sshd

Review its recent logs:

Bash
sudo journalctl -u fail2ban --since '24 hours ago' --no-pager

Do not infer security quality from raw ban counts. Internet scanning levels, SSH exposure, authentication methods, jail thresholds, and source networks all change how many bans a server records.

Verify: If Fail2Ban is part of your design, it should be active with only intended jails; if it is not part of your design, SSH should still be protected by the stronger primary controls already configured.

Step 12 — Review time, authentication logs, failed units, disk, and security signals

Correct time is required for authentication, TLS validation, logs, package metadata, and incident reconstruction.

Check synchronization:

Bash
timedatectl status

Review SSH events:

Bash
sudo journalctl -u ssh --since '24 hours ago' --no-pager

Review warning-or-higher system messages:

Bash
sudo journalctl -p warning --since '24 hours ago' --no-pager

Check failed services:

Bash
systemctl --failed

Check disk and inode pressure:

Bash
df -h df -i

Review recent logins:

Bash
last -a | head -n 20

If your environment has centralized logging or monitoring, forward security-relevant events off the server so an attacker with root access cannot silently erase the only copy. Define alerts around unexpected service failures, disk exhaustion, authentication anomalies, newly exposed ports, and backup failures rather than relying on manual review alone.

Verify: Time synchronization should be healthy, there should be no unexplained failed services or disk exhaustion, and suspicious authentication/system events should have an investigation path.

Step 13 — Prepare backups and a recovery path before declaring the server hardened

A security change can be correct and still cause an outage. A hardened server needs a recovery path that does not depend on the same control you might accidentally break.

Before production use:

  • keep application/data backups on an appropriate schedule;
  • retain at least one recovery copy outside the VM;
  • protect backup credentials separately from application credentials;
  • test restoring critical data and configuration;
  • document the SSH port, UFW policy, administrator identities, and important service paths;
  • verify a provider console, rescue environment, or equivalent out-of-band path before removing the last known-good SSH method.

Raff Data Protection can provide an infrastructure-level recovery layer. Application-aware backups remain necessary for databases and workloads that need consistency beyond a VM-level recovery point.

Do not call a backup strategy complete merely because jobs report success. Restore testing is what proves that the backup is usable.

Verify: You should know how to regain administrative access after an SSH/UFW mistake and how to restore the server's critical application data/configuration from a separate recovery copy.

Step 14 — Run the final Ubuntu server hardening checklist

Verify SSH syntax and effective settings:

Bash
sudo sshd -t sudo sshd -T | grep -E \ '^(permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|maxauthtries|logingracetime) '

Verify firewall and network exposure:

Bash
sudo ufw status numbered sudo ufw show listening sudo ss -tulpn

Verify AppArmor:

Bash
sudo aa-status

Verify automatic updates:

Bash
systemctl list-timers apt-daily.timer apt-daily-upgrade.timer --all sudo unattended-upgrade --dry-run

Verify accounts and service health:

Bash
getent group sudo systemctl --failed

Check pending package updates and reboot state:

Bash
sudo apt update apt list --upgradable 2>/dev/null test -f /var/run/reboot-required && \ cat /var/run/reboot-required || \ echo "No reboot required"

Finally, test from another machine:

  1. a fresh key-based SSH session works;
  2. password-only SSH does not provide an unintended login path;
  3. required public application ports work;
  4. unnecessary ports are not reachable;
  5. backup/recovery access is documented and tested.

This tutorial is a practical baseline, not a certification. If your organization must meet CIS, FIPS, PCI DSS, HIPAA, SOC 2 controls, or another standard, map the applicable requirements to the actual system and use supported auditing/compliance tooling. Do not claim compliance simply because a generic server-hardening checklist was completed.

Verify: The server should pass the final checks with no unexplained access, listeners, failed services, pending recovery gap, or security control that exists only on paper.

Troubleshooting

A new SSH session stops working after the hardening change

Keep an existing session open and check:

Bash
sudo sshd -t sudo sshd -T | grep -E \ '^(port|permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication) ' sudo ufw status numbered

Inspect all snippets because an earlier file may have supplied the first value:

Bash
sudo ls -1 /etc/ssh/sshd_config.d/ sudo grep -RniE \ '^(PermitRootLogin|PubkeyAuthentication|PasswordAuthentication|KbdInteractiveAuthentication)' \ /etc/ssh/sshd_config /etc/ssh/sshd_config.d 2>/dev/null

Correct the effective setting before restarting SSH again.

PasswordAuthentication no appears in a file but sshd -T says yes

OpenSSH normally uses the first obtained value for most directives. Another earlier snippet may already define the option. Inspect snippet order and use sshd -T as the authority for the effective configuration.

UFW is active but a Docker container port is still reachable

Docker-published traffic can bypass ordinary UFW host-filter assumptions. Inspect:

Bash
docker ps --format 'table {{.Names}}\t{{.Ports}}' sudo ufw show raw

Bind private container ports to loopback or use Docker-aware/upstream filtering. The full Docker caveat is covered in the UFW tutorial.

unattended-upgrade --dry-run does not include a third-party package

That can be expected. Adding an APT repository does not automatically add its origin to the unattended-upgrades policy. Review /etc/apt/apt.conf.d/50unattended-upgrades and the third-party vendor's supported update approach before modifying allowed origins.

AppArmor blocks an application

Inspect the denial rather than disabling AppArmor:

Bash
sudo journalctl -k -g 'apparmor="DENIED"' --since '1 hour ago' --no-pager

Identify the profile and operation, then make a narrow tested profile adjustment if the access is legitimately required.

A security scanner demands many sysctl or SSH crypto changes

Determine which benchmark/profile generated the finding and whether it applies to this server. Security benchmark recommendations can conflict with application functionality and distro defaults. Apply standards-driven changes through a tested compliance process rather than copying them into a general baseline.

Conclusion

You now have an Ubuntu 24.04 hardening baseline built around verified access and reduced attack surface: non-root administration, tested SSH keys, effective OpenSSH configuration checks, deny-by-default UFW, automatic security updates, service/account review, AppArmor confinement, optional Fail2Ban, security logging, and recovery planning.

The most important operational habit is continuous verification. Re-run the listener, firewall, account, update, AppArmor, service-health, and backup checks after application deployments and access changes. Hardening is not a one-time state and it is not synonymous with compliance; it is an ongoing process of keeping the server's privileges and exposed surface aligned with what the workload actually requires.

The detailed SSH key, UFW, and optional Fail2Ban procedures are linked alongside the corresponding hardening steps above. Before changing production access, confirm that a rescue or rollback path works.

Deploy a Raff Ubuntu Linux VM and apply a documented server-hardening baseline with tested SSH access, patching, firewall rules, monitoring, and a recovery plan. This checklist is not compliance certification.

Was this article helpful?
Serdar Tekin
Serdar Tekin
Co-Founder & CTO

Co-Founder & CTO at Raff, focused on cloud infrastructure, systems architecture, security, reliability, and production operations.

Put it into practice

Spin up a Raff server, follow the steps above on real infrastructure, and you are live. Linux or Windows, backed by a 14-day money-back guarantee.

Frequently asked questions

What is Ubuntu server hardening?
Ubuntu server hardening is the process of reducing unnecessary privileges, software, services, and network exposure while strengthening remote access, patching, application confinement, logging, monitoring, and recovery. It is an ongoing operational process rather than a one-time script.
Should I disable root SSH login on Ubuntu 24.04?
For a normal remote administration baseline, yes after a non-root sudo account and its SSH key have been proven in a separate session. Keep the old working session and a recovery path until the replacement access method is tested.
Why use an early sshd_config.d file instead of 99-hardening.conf?
Ubuntu includes sshd_config.d near the top of the main configuration, and OpenSSH uses the first obtained value for most directives. An earlier snippet can therefore win over a later one. Always inspect existing snippets and confirm the effective result with `sshd -T`.
Are automatic security updates enabled by default on Ubuntu Server?
Current Ubuntu Server documentation states that unattended security updates are applied automatically by default through `unattended-upgrades`. Verify the package, APT timers, periodic settings, allowed origins, and a dry run rather than blindly replacing the default configuration.
Should I disable AppArmor to avoid application permission problems?
No as a general fix. Ubuntu installs and loads AppArmor by default and recommends it as a defense-in-depth control. Investigate the specific AppArmor denial and adjust the relevant profile narrowly when legitimate application access is required.
Is Fail2Ban required on a hardened Ubuntu server?
No. Fail2Ban is optional and mainly helps with repeated log-based authentication failures. SSH keys, safe OpenSSH configuration, firewall restrictions, updates, and access control are more fundamental protections.
Should I apply a generic sysctl hardening script?
Not without understanding each setting and the server role. Generic kernel/network tweaks can break containers, VPNs, routing, cloud networking, monitoring, and applications. Apply workload-specific or benchmark-required settings through a tested change process.
Does completing this checklist make an Ubuntu server CIS, FIPS, PCI DSS, HIPAA, or SOC 2 compliant?
No. This is a practical security baseline, not a certification. Compliance requires mapping the applicable standard to the complete environment, documented controls, evidence, and supported auditing or compliance tooling.