Security HOWTO: securing a Linux system step by step

Updated November 2026

Most Linux break-ins today come from three things: missing updates, weak or reused passwords on SSH, and services that nobody meant to expose. Fix those three first. This page walks through them in order, then adds a firewall, mandatory access control (AppArmor or SELinux), logging, file integrity checks and backups, with commands for Debian, Ubuntu, Fedora and RHEL-family systems.

Work through the sections in order on a new machine. On a machine that is already in use, keep a second SSH session open while you change SSH or firewall settings, so a mistake does not lock you out.

Know what you protect

Write down three things before you start: what the machine does (web server, file server, laptop), who logs in to it, and which network ports must be reachable from outside. Everything else can be closed or removed. A web server, for example, needs ports 80 and 443 open to the world and port 22 (SSH) open to you. It does not need a print server, a mail server listening on the network, or a database reachable from the Internet.

Install updates automatically

Security fixes only help if they are installed. Turn on automatic security updates and check once a week that they ran.

Debian and Ubuntu

Ubuntu installs unattended-upgrades by default and applies security updates once a day. On Debian, install it and confirm it is enabled:

sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades    # answer "Yes"
sudo unattended-upgrade -d                   # one manual run with debug output

Settings live in /etc/apt/apt.conf.d/50unattended-upgrades. To reboot automatically when an update needs it, set Unattended-Upgrade::Automatic-Reboot "true"; and pick a time with Unattended-Upgrade::Automatic-Reboot-Time "03:30";. Logs are in /var/log/unattended-upgrades/. On Ubuntu the file /var/run/reboot-required appears when a reboot is pending. The needrestart package lists services still running old library code.

RHEL, AlmaLinux and Rocky Linux 9 and 10

sudo dnf install dnf-automatic
sudoedit /etc/dnf/automatic.conf       # set: upgrade_type = security
sudo systemctl enable --now dnf-automatic-install.timer
sudo dnf needs-restarting -r           # exit code 1 means a reboot is needed

The dnf-automatic-install.timer downloads and installs updates. Use dnf-automatic-notifyonly.timer instead if you only want to be told. Setting reboot = when-needed in the [commands] section reboots after kernel or systemd updates.

Fedora

Fedora uses DNF 5, where the tool is a plugin with a different timer name:

sudo dnf install dnf5-plugin-automatic
sudoedit /etc/dnf/automatic.conf       # set: upgrade_type = security, apply_updates = yes
sudo systemctl enable --now dnf5-automatic.timer
sudo dnf needs-restarting              # in DNF 5 this reports whether a reboot is needed

Users, passwords and sudo

Ubuntu 26.04 LTS uses sudo-rs, a Rust rewrite, as the default sudo. The original program is still available as sudo.ws. Ordinary sudoers rules work in both.

Harden SSH

Use keys instead of passwords. On your own computer, create a key and copy it to the server:

ssh-keygen -t ed25519
ssh-copy-id alice@server.example.com
ssh alice@server.example.com          # must log in without the account password

Only when key login works, add a drop-in file on the server. Current Debian, Ubuntu, Fedora and RHEL configs start with Include /etc/ssh/sshd_config.d/*.conf. For each setting, sshd uses the first value it reads, so give your file a low number:

# /etc/ssh/sshd_config.d/10-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers alice bob
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no

Keep your current session open. Before you reload or restart sshd, leave the SSH session you are working in open (and ideally a second one), and keep console access (cloud provider console, IPMI, KVM) in reserve. Existing sessions survive a reload; if the new settings lock you out, you can still fix them from the open session.

Test the file, check the settings sshd will really use, then reload:

sudo sshd -t
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|allowusers'
sudo systemctl reload ssh      # Debian/Ubuntu (Ubuntu 26.04 also accepts "sshd")
sudo systemctl reload sshd     # Fedora, RHEL, Arch, openSUSE

Open a new terminal and log in again before you close the old session. Only when that works is the change safe. If sshd -T still shows passwordauthentication yes, another file in sshd_config.d (cloud images often add one) is read before yours.

OpenSSH 9.8 and later also slow down attackers on their own: PerSourcePenalties, on by default, refuses connections for a while from addresses that keep failing to authenticate. With passwords turned off, tools such as fail2ban are optional. They mostly reduce log noise. As of November 2026 the current OpenSSH release is 10.6 (October 6, 2026); Ubuntu 26.04 LTS ships 10.2 and RHEL 10 ships 9.9 with backported fixes.

Turn on a firewall

Allow only the ports you listed at the start. When you work over SSH, add the SSH rule before you enable the firewall. On Ubuntu and Debian, ufw is the simple choice:

sudo apt install ufw
sudo ufw limit ssh/tcp          # allow SSH, rate-limited
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Fedora and RHEL use firewalld, which is on by default:

sudo firewall-cmd --list-all
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --remove-service=cockpit   # if you do not use Cockpit
sudo firewall-cmd --reload

The Firewall HOWTO explains nftables rulesets, logging and how to avoid locking yourself out.

Run fewer services

Every listening program is a door. List them:

sudo ss -tulpn                                   # listening TCP/UDP sockets and their programs
systemctl list-units --type=service --state=running

Stop and disable what you do not need, for example a print server on a web host:

sudo systemctl disable --now cups.service cups.socket
sudo apt purge cups         # or: sudo dnf remove cups

Bind services that only local programs use to 127.0.0.1 (for example a database or Redis). For your own services, systemd-analyze security name.service rates how well a unit is sandboxed and lists options such as NoNewPrivileges=yes, ProtectSystem=strict and PrivateTmp=yes that you can add in a drop-in with sudo systemctl edit name.service.

Keep AppArmor or SELinux on

Mandatory access control limits what a program can do even after it is broken into. Debian and Ubuntu use AppArmor. Fedora and RHEL use SELinux. Leave it enabled.

sudo aa-status        # AppArmor: loaded profiles and their mode
getenforce            # SELinux: should print "Enforcing"
sudo ausearch -m AVC -ts recent      # SELinux denials (needs auditd)

When something is blocked, fix the label or policy setting instead of turning protection off. On SELinux that usually means semanage fcontext plus restorecon for files in a new place, semanage port for a new port, or setsebool -P for a documented switch. The server hardening guide shows these commands in detail. On AppArmor, sudo aa-complain /path/to/program (from apparmor-utils) switches one profile to log-only while you investigate.

Read your logs

systemd's journal collects most logs. A few useful commands:

journalctl -p warning -b             # warnings and worse since boot
journalctl -u ssh --since today      # SSH log (unit "sshd" on Fedora/RHEL)
journalctl -u ssh | grep -c 'Failed'

For a record of who changed sensitive files, add the audit daemon. The auditctl -w watch form still works but is deprecated, so write rules in the syscall form:

sudo apt install auditd          # Fedora/RHEL: sudo dnf install audit
echo '-a always,exit -F arch=b64 -F path=/etc/sudoers -F perm=wa -k sudoers' | \
  sudo tee /etc/audit/rules.d/50-sudoers.rules
sudo augenrules --load
sudo ausearch -k sudoers -i

Send logs to a second machine if you can. An attacker who gets root can edit local logs.

Check file integrity with AIDE

AIDE records checksums of system files and later reports what changed. Build the database on a clean system, right after install:

# Debian/Ubuntu
sudo apt install aide
sudo aideinit
sudo aide --config /etc/aide/aide.conf --check

# Fedora/RHEL
sudo dnf install aide
sudo aide --init
sudo mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
sudo aide --check

Run the check daily from a timer or cron job and read the report. After you install updates, review the changes and then update the database (sudo aide --update on Fedora/RHEL, then rename the new file as above). Keep a copy of the database off the machine, or an intruder can simply rebuild it.

Back up, and test restores

Backups are your recovery plan for ransomware, a bad update and a dead disk. Keep at least one copy on another machine and one copy offline or append-only. restic is one good tool. It encrypts and deduplicates:

restic init --repo /srv/restic-repo
restic -r /srv/restic-repo --verbose backup /etc /home /var/www
restic -r /srv/restic-repo snapshots
restic -r /srv/restic-repo restore latest --target /tmp/restore-test

Do a test restore every few months. A backup you have never restored is a hope, not a backup.

Quick checklist

If you think you were broken into

Disconnect the machine from the network but leave it running if you can, so memory and open connections can still be examined. Do not trust any program on it. Copy logs and an image of the disk from a rescue system. Then reinstall from known-good media, restore data (not programs) from a backup made before the break-in, change every password and key that was on the machine, and find out how the attacker got in before you reconnect. Report it to your provider or organization's security team as their rules require.

The classic version of this HOWTO (last revised 2004) is kept by the LDP at tldp.org. Its advice on tcp_wrappers, ipchains and telnet no longer applies.

Sources