Linux Hardening Basics: a practical checklist for Ubuntu and Red Hat family servers
Works on Ubuntu 22.04 / 24.04 and RHEL 8 / 9 / 10 (and rebuilds such as Rocky and AlmaLinux). Commands are shown for both families where they differ.
Hardening means shrinking what an attacker can reach and limiting what they can do if they get in: fewer open ports, fewer running services, fewer privileges, stronger logins, and good logs so you notice. It is a habit, not a one-time task. This sheet is the everyday checklist. The compliance sheets on this tab (CIS, PCI DSS, HIPAA, STIG, NIST, ISO 27001 and SOC 2) are longer, more formal versions of the same ideas.
This sheet is based on common practice and is cross-checked against the style of the CIS Benchmarks. The CIS Benchmark for your exact distribution and version is the authoritative checklist: see the CIS sheet.
Only change machines you own or administer. Hardening can break things. Read "Before you start" first.
Before you start (this part saves you from a bad afternoon)
1. Have a way back in that does not depend on the thing you are changing: the cloud console, a hypervisor console or a physical keyboard. SSH and firewall mistakes lock you out.
2. Take a snapshot or backup first. Test on a copy before production.
3. Keep a SECOND SSH session open while you change SSH, PAM or the firewall. Test from a third one before you close the second.
4. Change one area at a time, and write down what you changed and why (a comment in the file is enough).
5. Validate config before reloading: sshd -t, visudo -c, nginx -t, and so on.
1. Patch first, and keep patching
Most break-ins use known bugs that already have a fix. Updates are the biggest single win.
Ubuntu:
sudo apt update && sudo apt upgrade
sudo apt install unattended-upgrades # automatic security updates
sudo dpkg-reconfigure -plow unattended-upgrades
systemctl status unattended-upgrades
ls /var/run/reboot-required # exists = a reboot is pending
# optional automatic reboot: in /etc/apt/apt.conf.d/50unattended-upgrades set Unattended-Upgrade::Automatic-Reboot "true"; and Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Red Hat / RHEL:
sudo dnf upgrade --refresh
sudo dnf install dnf-automatic
# in /etc/dnf/automatic.conf set upgrade_type = security and apply_updates = yes
sudo systemctl enable --now dnf-automatic.timer
sudo needs-restarting -r # says whether a reboot is needed
A kernel update does nothing until you reboot into it: check uname -r. See the Security tab for real examples (Dirty Frag and Copy Fail).
2. Accounts, passwords and sudo
sudo awk -F: '$2=="" {print $1}' /etc/shadow # accounts with an EMPTY password (should print nothing)
awk -F: '$3==0 {print $1}' /etc/passwd # accounts with UID 0 (should print only: root)
getent passwd | awk -F: '$7 !~ /nologin|false/ {print $1, $7}' # accounts that can log in interactively
sudo passwd -l olduser # lock an account nobody uses (or: sudo usermod -L olduser)
sudo chage -l alice # password aging for one user
Good habits: one person = one account (no shared logins), no direct root logins, give admin rights through sudo groups, and remove accounts when people leave.
sudo: edit ONLY with sudo visudo, or put files in /etc/sudoers.d/ and check them with sudo visudo -cf /etc/sudoers.d/yourfile. A common hardening line is: Defaults use_pty
Password quality: in /etc/security/pwquality.conf set for example minlen = 14 (length beats complexity rules). Prefer SSH keys or MFA over passwords wherever you can.
Lock out repeated guessing (pam_faillock): in /etc/security/faillock.conf set for example deny = 5 and unlock_time = 900.
RHEL 8/9/10: sudo authselect enable-feature with-faillock (then edit faillock.conf)
Ubuntu: the PAM lines must be added to /etc/pam.d/common-auth and common-account (see man pam_faillock). Test in a second session. The CIS tooling in the scanning-tools sheet can do this for you.
3. SSH: the front door
Modern Ubuntu and RHEL read drop-in files from /etc/ssh/sshd_config.d/. The FIRST value found for a setting wins, and cloud images often ship a file (for example 50-cloud-init.conf) that turns password logins on. So name your file so it sorts first: /etc/ssh/sshd_config.d/00-hardening.conf
Contents (create it with sudoedit):
PermitRootLogin no
PasswordAuthentication no # ONLY after you have proven key login works
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 4
LoginGraceTime 30
X11Forwarding no
AllowTcpForwarding no # only if you do not use SSH tunnels
ClientAliveInterval 300
ClientAliveCountMax 2 # drops an idle session after about 10 minutes
LogLevel VERBOSE
AllowGroups sshusers # optional: only this group may log in (make sure YOU are in it first)
Test and apply:
sudo sshd -t # silence = the syntax is fine
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|maxauthtries|allowgroups' # the settings sshd will REALLY use
sudo systemctl reload ssh # Ubuntu (the service is called ssh)
sudo systemctl reload sshd # RHEL family (the service is called sshd)
Do not hand-pick ciphers unless a standard makes you (for example FIPS). Current OpenSSH defaults are good, and old hand-written cipher lists are a classic way to break logins later.
Also worth doing: keep the SSH keys of leavers out of authorized_keys, use a passphrase on private keys, and consider MFA or SSH certificates for bigger teams.
4. Firewall: default deny
Allow only what the machine really serves.
Ubuntu (ufw):
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH # DO THIS BEFORE enabling, or you lock yourself out
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Red Hat (firewalld):
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --remove-service=cockpit # if you do not use the web console
sudo firewall-cmd --reload
Docker note: ports published by Docker are added straight into iptables, and can bypass ufw. Check with a scan from another machine, not just ufw status.
Cloud note: security groups and network ACLs are a second, outer firewall. Use both.
5. Run fewer things
sudo ss -tulpn # everything listening on the network, and which program owns it
systemctl list-unit-files --state=enabled --type=service # everything that starts at boot
sudo systemctl disable --now cups # example: switch off a service you do not need
sudo systemctl mask servicename # stronger: nothing can start it
sudo apt purge telnetd rsh-server # Ubuntu: remove insecure legacy servers if present
sudo dnf remove telnet-server # RHEL family
Every listening port should have an owner and a reason. If you cannot say why it is there, find out before you delete it, then remove it.
6. Files, permissions and mounts
findmnt -no OPTIONS /tmp # look for nodev,nosuid,noexec
# example /etc/fstab line: tmpfs /tmp tmpfs defaults,nodev,nosuid,noexec 0 0 (noexec can break installers and some apps: test)
sudo find / -xdev -type f -perm -4000 2>/dev/null # setuid programs: review the list, and know what you expect
sudo find / -xdev -type f -perm -0002 2>/dev/null # world-writable files (should be very few)
sudo find / -xdev \( -nouser -o -nogroup \) 2>/dev/null # files owned by nobody real
ls -l /etc/passwd /etc/shadow /etc/group /etc/gshadow # shadow files must NOT be world-readable
Default file permissions: a umask of 027 (files rw-r-----) is the common hardened value, set in /etc/login.defs (UMASK 027). It can surprise web apps, so test.
7. Kernel and network settings (sysctl)
File: /etc/sysctl.d/99-hardening.conf
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.log_martians = 1
kernel.randomize_va_space = 2
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
fs.suid_dumpable = 0
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
Apply and check:
sudo sysctl --system
sysctl net.ipv4.conf.all.rp_filter kernel.kptr_restrict # confirm the live values
Careful: rp_filter can break machines with asymmetric routing or several uplinks. Leave net.ipv4.ip_forward alone (0 on normal servers), but routers, VPN gateways, Docker and Kubernetes nodes NEED it on. See also user.max_user_namespaces on the Dirty Frag sheet.
8. Mandatory access control: keep it ON
Ubuntu uses AppArmor, RHEL uses SELinux. Both stop a compromised program from touching things it should not.
sudo aa-status # Ubuntu: profiles loaded and enforcing
getenforce ; sestatus # RHEL: should say Enforcing
Never "fix" a denial by turning SELinux off (setenforce 0). Find the denial and allow exactly that:
sudo ausearch -m avc -ts recent | audit2why
Turning it off is like removing the smoke alarm because it beeped.
9. Logging, auditing and time
You cannot investigate what you did not record. Keep logs somewhere an attacker cannot quietly edit (a central log server).
sudo apt install auditd # Ubuntu (RHEL usually has it: sudo dnf install audit)
sudo systemctl enable --now auditd
Example rules, in /etc/audit/rules.d/hardening.rules:
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/sudoers -p wa -k scope
-w /etc/sudoers.d/ -p wa -k scope
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k modules
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
Load and use them:
sudo augenrules --load
sudo auditctl -l # the rules that are live
sudo ausearch -k identity -ts today # who touched the account files today
sudo aureport --summary
Keep the journal across reboots: set Storage=persistent in /etc/systemd/journald.conf, then sudo systemctl restart systemd-journald.
Send logs off the box: rsyslog can forward with a line such as *.* @@loghost.example.com:514 (the @@ means TCP).
Keep the clock right (logs are useless if the time is wrong):
timedatectl status # want: System clock synchronized: yes
chronyc tracking # if you use chrony (the RHEL default)
10. Know when files change (AIDE)
AIDE takes a fingerprint of important files and tells you what changed.
Ubuntu: sudo apt install aide ; sudo aideinit ; sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db ; sudo aide.wrapper --check
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 on a schedule (weekly is a common minimum) and keep a copy of the database somewhere off the machine. After patching, expect changes: rebuild the database once you have reviewed them.
11. Encryption
lsblk -f # look for crypto_LUKS: is the disk encrypted at rest?
sudo cryptsetup status <name> # details of an open encrypted volume
Encrypt laptops and any server holding sensitive data (choose LUKS at install: adding it later means reformatting). Use TLS 1.2 or newer for anything that crosses a network, and certificates from a real CA (certbot is free). On RHEL the system-wide crypto policy is a single knob:
update-crypto-policies --show # DEFAULT, FUTURE, FIPS ...
FIPS mode is a compliance requirement in some frameworks (see the STIG sheet), and it can break software: plan it, do not just switch it on.
12. Switch off kernel modules and protocols you do not use
File: /etc/modprobe.d/hardening.conf
install cramfs /bin/false
install freevxfs /bin/false
install hfs /bin/false
install hfsplus /bin/false
install jffs2 /bin/false
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
install usb-storage /bin/false # only on servers that never need USB drives
Check one: modprobe -n -v cramfs (should print: install /bin/false)
Leave udf alone on Azure VMs: they need it during provisioning. Built-in code (CONFIG_...=y) cannot be blocked this way. This is the same trick used as a stop-gap for Dirty Frag and Copy Fail: see the Security tab.
13. Backups (the control that saves you when the others fail)
Follow 3-2-1: three copies, two kinds of storage, one off site or immutable. A backup you have never restored is a hope, not a backup: practise a restore. Ransomware people delete backups they can reach, so keep one copy the production credentials cannot delete.
Check your work
sudo apt install lynis # Ubuntu (RHEL: sudo dnf install lynis, from EPEL)
sudo lynis audit system # a quick audit with a hardening score and suggestions
sudo ss -tulpn # from the server: what listens
nmap -sT -p- your-server # from ANOTHER machine, and only against systems you own: what really answers
For a formal, framework-based scan (CIS, PCI DSS, HIPAA, STIG) see the OpenSCAP, USG and Lynis sheet.
Common problems
"I locked myself out over SSH": use the console. Remove or fix the last change in /etc/ssh/sshd_config.d/, run sshd -t, and reload. Next time keep a second session open.
"sshd -T shows PasswordAuthentication yes even though I set no": another file in sshd_config.d/ or the main file sets it first. The first value found wins. Sort your file earlier (00-) or grep -ri PasswordAuthentication /etc/ssh/.
"ufw is on but a Docker port is still reachable": Docker writes its own iptables rules ahead of ufw. Bind the container to 127.0.0.1 or filter with the DOCKER-USER chain.
"apt or dnf scripts fail after I made /tmp noexec": some installers run scripts from /tmp. Point them at another temporary directory, or relax noexec on that host.
"An app broke after I set rp_filter": the machine probably has asymmetric routing. Use net.ipv4.conf.all.rp_filter = 2 (loose) or exempt that interface.
"SELinux denies my service": read the denial (ausearch -m avc), fix the file label (restorecon -Rv path) or add a targeted rule. Do not disable SELinux.
"Audit rules will not change": if you added -e 2 (immutable), the rules are locked until reboot. Only add it once the rules are final.
"AIDE reports hundreds of changes": you patched. Review, then rebuild the database.
Tips
Start with the big five: patches, SSH keys instead of passwords, a default-deny firewall, fewer running services and good logs. That alone stops most real-world attacks.
Automate: run a hardening baseline through a tool (see the scanning-tools sheet) and keep the configuration in version control (Ansible, cloud-init). Hand-hardened servers drift.
Write down every deliberate exception ("we leave X on because Y, approved by Z"). Auditors love it and future you will need it.
Least privilege everywhere: users, services, containers, API tokens and cloud roles.
Re-check after every big change (new package, new port, new user). Hardening is not a project, it is housekeeping.
The most secure server is one that is turned off, but the boss keeps asking for uptime.
Sources
CIS Benchmarks (the authoritative per-distribution checklists): https://www.cisecurity.org/cis-benchmarks
Red Hat, Security hardening and compliance scanning (RHEL 9): https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/scanning-the-system-for-configuration-compliance-and-vulnerabilities_security-hardening
Ubuntu Security Guide (USG) for CIS on Ubuntu: https://ubuntu.com/security/certifications/docs/usg/cis
Related sheets on this tab: OpenSCAP, USG and Lynis; CIS Benchmarks; PCI DSS; HIPAA; DISA STIG; NIST; ISO 27001 and SOC 2