🔒 Linux Hardening Basics

hardening & compliance / survival guide

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