A default Linux install is built to work out of the box, not to resist an attacker who already has a foothold, and most server compromises exploit the gap: password SSH, broad sudo rights, unmonitored changes and services that run with far more privilege than they need. Hardening closes that gap with a known baseline, applied by automation and checked continuously. This guide covers CIS Benchmarks and DISA STIGs, minimal builds and patching, SSH, sudo and PAM, SELinux and AppArmor, systemd sandboxing, kernel sysctl settings, nftables, auditd, AIDE, central logging and OpenSCAP scanning. It is written for Linux administrators, platform and SRE teams, and security engineers who own server baselines on RHEL-family, Debian and Ubuntu systems. Setting names and defaults are taken from upstream manual pages and vendor documentation; check them against your distribution release.
Key takeaways
- Start from a published baseline. CIS Benchmark Level 1 Server or the DISA STIG for your distribution gives an auditable starting point; tailor it and record every exception.
- SSH is the front door and the identity problem. Disable root and password logins, restrict who may connect, and manage the keys and service accounts behind them as credentials with owners.
- Least privilege needs enforcement, not just policy. Narrow sudo rules, SELinux or AppArmor in enforcing mode and systemd sandboxing limit what a compromised account or service can do.
- Detection is part of hardening. auditd rules for identity files and privileged commands, AIDE integrity checks and central log forwarding make tampering visible.
- Automate and measure drift. Apply the baseline with configuration management and scan with OpenSCAP so compliance is a number, not an assumption.
Choosing a baseline: CIS Benchmarks and DISA STIGs
CIS describes its Benchmarks as a "consensus-based effort of cybersecurity experts globally", with Linux benchmarks for RHEL, Rocky, AlmaLinux, Oracle Linux, Debian, Ubuntu, SUSE and Amazon Linux among others. DISA Security Technical Implementation Guides (STIGs) are the US Department of Defense configuration standards, with findings rated CAT I (high), CAT II (medium) and CAT III (low).
| Baseline | Best for | Trade-off | Tooling |
|---|---|---|---|
| CIS Level 1 Server | Most production servers | Low operational impact, moderate depth | CIS-CAT Pro, SCAP Security Guide cis profiles, Ubuntu Security Guide cis_level1_server |
| CIS Level 2 Server | Sensitive or internet-facing systems | Canonical: "more rules that further reduce the attack surface", at some cost to usability | Same tools, cis_level2_server |
| DISA STIG | Government, defence and contracts that require it | Prescriptive, stricter on logging, authentication and banners; more exceptions to manage | SCAP Security Guide stig profile, DISA SCAP content |
| Internal baseline derived from one of the above | Large estates with many roles | Needs ownership and version control | Ansible, Puppet, Chef or Salt plus OpenSCAP tailoring |
CIS also publishes hardened images on cloud marketplaces. Whichever you choose, keep a tailoring file listing disabled rules and the reason, because unexplained exceptions are what auditors and attackers both find.
Minimal build and patching
- Install the minimum: start from the minimal or server profile, remove compilers, X11 and unused network services, and confirm what listens with
ss -tulpn. Disable units you do not need withsystemctl disable --nowandsystemctl mask. - Separate and restrict filesystems: mount
/tmp,/var/tmpand/dev/shmwithnodev,nosuid,noexecwhere workloads allow, and keep/var/logand/var/log/auditon their own partitions so log floods cannot fill root. - Set a restrictive default umask: 027 for system accounts, and keep sensitive files at mode 600 or 640 (
chmod 600for private keys and secrets files, root-owned withchown root:root). - Review SUID and SGID binaries: list them with
find / -xdev -perm -4000 -o -perm -2000, remove the bit where not required and alert on new ones. Abuse is ATT&CK T1548.001 Setuid and Setgid. - Patch on a schedule:
unattended-upgradeson Debian and Ubuntu anddnf-automaticon RHEL-family systems apply security updates; plan reboots or kernel live patching so the running kernel matches the installed one. See the Patch Management Guide. - Trust the package path: install only from signed repositories you control or mirror. The XZ Utils backdoor showed how a compromised upstream library could reach
sshdthrough ordinary distribution updates.
SSH hardening
OpenSSH defaults favour compatibility. According to the sshd_config(5) manual for OpenSSH 9.9, PasswordAuthentication, KbdInteractiveAuthentication, AllowTcpForwarding and AllowAgentForwarding all default to yes and PermitRootLogin defaults to prohibit-password. A typical server baseline:
| Setting | Default | Hardened value | Why |
|---|---|---|---|
PermitRootLogin | prohibit-password | no | Forces named accounts and sudo, so actions are attributable |
PasswordAuthentication, KbdInteractiveAuthentication | yes | no (unless keyboard-interactive is your MFA path) | Removes password guessing and reuse |
AuthenticationMethods | any | publickey, or publickey,keyboard-interactive for MFA | Requires keys, optionally plus a second factor |
AllowGroups | everyone | A dedicated group, for example ssh-users | Service and system accounts cannot log in interactively |
MaxAuthTries / LoginGraceTime | 6 / 120 | 3 or 4 / 60 | Slows guessing and frees unauthenticated slots |
DisableForwarding | not set | yes on servers that do not need tunnels | Disables X11, agent, TCP and socket forwarding in one line |
ClientAliveInterval / ClientAliveCountMax | 0 / 3 | 300 / 3 | Drops dead sessions; a count of 0 disables termination |
LogLevel | INFO | VERBOSE | Records the fingerprint of the key used, so logins map to keys |
TrustedUserCAKeys, AuthorizedKeysFile | none / user home | SSH CA file; root-owned key path such as /etc/ssh/authorized_keys/%u | Short-lived certificates and keys users cannot add themselves |
- Check the effective configuration: sshd uses the first value it reads for most keywords, and
Include /etc/ssh/sshd_config.d/*.confdrop-ins, often written by cloud-init, can re-enable passwords. Runsshd -Tto print what is actually in force andsshd -tbefore reloading. - Rate limiting: OpenSSH 9.8 and later apply
PerSourcePenaltiesby default; fail2ban jails on the sshd log add longer bans for hosts that must expose port 22. Neither replaces turning passwords off. - Keys are credentials: ATT&CK T1098.004 SSH Authorized Keys notes adversaries "may modify the SSH authorized_keys file to maintain persistence", and T1021.004 covers lateral movement over SSH with valid keys. Inventory keys, assign owners, prefer SSH certificates with short validity and remove keys at leaver time. Rotate host keys if they leak, as in the GitHub RSA SSH host key exposure. See the SSH Key Management Guide.
Accounts, sudo and PAM
- Edit with visudo, scope narrowly: put rules in
/etc/sudoers.d/and grant specific commands to groups, notALL. The sudoers manual warns that "Wildcards in command line arguments should be used with care" and that negatingALLto exclude a few commands "rarely works as intended". Editors, pagers and shells allowed through sudo give a root shell; usesudoeditfor file edits. - Logging defaults: set
Defaults use_pty,Defaults logfile="/var/log/sudo.log"and, for administrators of sensitive systems,log_inputandlog_output(off by default). sudo 1.9 can send event and I/O logs to a centralsudo_logsrvdthroughlog_servers. - Sudo caching: credentials are cached for 5 minutes per terminal by default (
timestamp_timeout). Shorten it on shared bastions; ATT&CK tracks abuse as T1548.003 Sudo and Sudo Caching. AvoidNOPASSWD: ALLexcept for tightly scoped automation. - Lockout and password quality:
pam_faillockreads/etc/security/faillock.conf(for exampledeny=5,unlock_time=900). It does not lock root unlesseven_deny_rootis set, and its records in/var/run/faillockmay not survive a reboot unlessdir=points elsewhere. Pair it withpam_pwqualityfor any remaining passwords. Change PAM throughauthselecton RHEL-family systems andpam-auth-updateon Debian and Ubuntu rather than editing stacks by hand. - Service accounts: give each daemon its own account with a
/usr/sbin/nologinshell, no password, no SSH access and no sudo unless a reviewed rule needs it. Remove or lock default and vendor accounts. See the Service Account Security Guide. - Secrets on disk: database passwords and API tokens in
.envfiles or unit files are readable by anyone who compromises the service user. Inject them from a vault or with systemdLoadCredential=. See the Secrets Management Guide.
Mandatory access control and service sandboxing
SELinux (RHEL, Fedora, Rocky, AlmaLinux) and AppArmor (Ubuntu, Debian, SUSE) confine processes even when they run as root. Keep SELinux at SELINUX=enforcing in /etc/selinux/config (check with getenforce) and fix denials with booleans (setsebool -P) or file contexts (semanage fcontext) rather than setenforce 0. On AppArmor, aa-status lists profiles in enforce and complain mode; move custom profiles from complain to enforce once logs are clean.
systemd adds per-service isolation without writing a full policy. Apply it in a drop-in with systemctl edit <unit> and score units with systemd-analyze security.
| Directive | Effect |
|---|---|
NoNewPrivileges=yes | The service and its children cannot gain privileges through execve, which neutralises SUID binaries for that service (default false) |
ProtectSystem=strict with ReadWritePaths= | Whole filesystem read-only except listed paths; full covers /usr, boot loader directories and /etc |
ProtectHome=yes, PrivateTmp=yes, PrivateDevices=yes | Hides home directories, gives a private /tmp and a minimal /dev |
CapabilityBoundingSet= | Restricts Linux capabilities, for example only CAP_NET_BIND_SERVICE |
SystemCallFilter=@system-service, RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX | seccomp system call allow list and socket family limits |
DynamicUser=yes | Transient UID for the service lifetime; implies NoNewPrivileges, ProtectSystem=strict and read-only home |
Kernel, network and firewall settings
Put settings in /etc/sysctl.d/60-hardening.conf and load them with sysctl --system. Values below follow the kernel documentation and common CIS recommendations; test each against your workloads.
| Setting | Value | Purpose |
|---|---|---|
kernel.kptr_restrict | 2 | Hides kernel pointers that help exploit development |
kernel.dmesg_restrict | 1 | Requires CAP_SYSLOG to read the kernel log |
kernel.unprivileged_bpf_disabled | 1 or 2 | Blocks unprivileged eBPF; 1 cannot be cleared without a reboot |
kernel.kexec_load_disabled | 1 | Stops loading a replacement kernel at runtime; irreversible until reboot |
kernel.yama.ptrace_scope | 1 or higher | Limits process tracing used for credential theft from memory |
kernel.randomize_va_space | 2 | Full address space layout randomisation |
kernel.io_uring_disabled | 2 where unused | Removes a large kernel attack surface on hosts that do not use it |
fs.suid_dumpable | 0 | No core dumps from SUID programs |
net.ipv4.conf.all.accept_redirects, send_redirects, accept_source_route | 0 | Rejects routing manipulation |
net.ipv4.conf.all.rp_filter, net.ipv4.tcp_syncookies | 1 | Reverse path filtering and SYN flood protection |
net.ipv4.ip_forward | 0 unless routing or containers need it | A server should not route traffic |
Host firewall with nftables
nftables replaces iptables on current distributions, and firewalld on RHEL-family systems uses it as its backend. The nftables project's server example uses one inet table for IPv4 and IPv6, an input chain with policy drop, ct state established and related accepted and invalid dropped, loopback accepted, ICMPv6 neighbour discovery allowed ("connectivity breaks" otherwise), and an explicit list such as tcp dport { 22, 443 } accept; forward is dropped unless the host routes. Restrict port 22 to bastion or management ranges with a source set rather than opening it to the world, log dropped packets at a rate limit, and load rules from a version-controlled file with nft -f. See the Firewall Management Guide.
auditd, file integrity and central logging
The audit.rules(7) manual prefers the explicit syscall form over the older -w watch syntax, asks for an arch on every syscall rule for performance, and recommends keys so ausearch -k and aureport can find events. Place rules in /etc/audit/rules.d/, load them with augenrules --load, and end with -e 2 to make the rule set immutable until reboot.
- Identity and privilege files:
-a always,exit -F arch=b64 -F path=/etc/shadow -F perm=wa -F key=identity, with similar rules for/etc/passwd,/etc/group,/etc/sudoers,/etc/sudoers.dand/etc/ssh/sshd_config. - Privileged commands by real users:
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=unset -k priv_exec, which ties root activity back to the login user. - Kernel modules:
-S init_module,finit_module,delete_modulewith amoduleskey. - SSH keys: watch
authorized_keyspaths; ATT&CK's detection for T1098.004 looks for exactly these modifications. - File integrity with AIDE:
aide --init, move/var/lib/aide/aide.db.new.gztoaide.db.gz, then runaide --check. Red Hat advises at least weekly and ideally daily checks,aide --updateafter approved changes, and keeping the database, configuration and binary on "read-only media" or off-host. AIDE detects; IMA can also prevent execution of altered files. - Central logging: forward journald,
/var/log/secureorauth.log, sudo logs and audit records to a SIEM over TLS, so an attacker with root cannot erase the only copy. See the Log Management and Retention Guide.
Automating and verifying the baseline
Hardening applied by hand drifts within weeks. Define the baseline as code (Ansible roles such as those in the DevSec Hardening Framework, or Puppet, Chef or Salt), build golden images from it, and scan running hosts. On RHEL the SCAP Security Guide content lives at /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml: oscap info lists profiles, oscap xccdf eval --profile <id> --report report.html <data stream> scans (oscap-ssh does the same remotely), and oscap xccdf generate fix produces an Ansible playbook without changing the host. Red Hat warns that remediation can leave a system non-functional and offers no automated revert. On Ubuntu Pro, usg audit and usg fix wrap the same OpenSCAP technology, with tailoring files for exceptions. Lynis gives a quick, agentless second opinion.
Implementation roadmap
- Inventory and classify servers by distribution, role and exposure; record who owns each and which baseline applies.
- Scan before changing: run OpenSCAP or CIS-CAT in report mode to get a baseline score per role.
- Tailor: disable rules that break the role, with a written reason and an owner, and version the tailoring file.
- Pilot: apply through configuration management to non-production hosts, run application tests, and keep an existing root session open when changing SSH or PAM. Put AppArmor profiles in complain mode first and review SELinux denials with
ausearch -m avcbefore enforcing. - Roll out in rings: internet-facing and bastion hosts first, then internal tiers. Roll back by reverting the configuration commit and re-running the play, which is why manual fixes are not allowed.
- Bake into images so new servers start compliant, then scan continuously and alert on drift.
Decision points
- CIS or STIG: choose STIG when a contract or regulator requires it; otherwise CIS Level 1 Server, with Level 2 for exposed or sensitive hosts.
- SELinux or AppArmor: use the one your distribution ships and supports. Switching adds risk without benefit.
- Keys or certificates for SSH: keys with central authorized_keys suit small estates; an SSH CA with short-lived certificates scales better and makes leaver removal automatic.
- Small teams can rely on CIS hardened images, USG or SSG playbooks and a managed SIEM. Large estates need a baseline owner, per-role tailoring, continuous scanning and exception expiry dates.
Detection and measurement
Alert on: SSH logins by password or as root, new keys in authorized_keys, changes to sudoers or sshd_config, new SUID files, SELinux switched to permissive, auditd stopped or rules changed, AIDE differences outside change windows, and kernel module loads on servers that should not load modules. See the SIEM Guide.
- Baseline compliance: average OpenSCAP or CIS-CAT score per role, and hosts below the agreed threshold.
- SSH posture: hosts with
PasswordAuthenticationor root login enabled according tosshd -T(target zero). - Privilege sprawl: sudo rules granting
ALLorNOPASSWD, trended down. - MAC coverage: hosts with SELinux enforcing or AppArmor profiles enforced (target 100 per cent).
- Logging coverage: hosts sending audit and auth logs to the SIEM in the last 24 hours.
- Patch latency: median days from security update release to install, and hosts running a kernel older than the installed one.
Common failure patterns
- Password logins left on: one forgotten host that still accepts passwords becomes the entry point. In the PHP Git server compromise, password-based logins to a self-hosted server let an attacker push a backdoor.
- Drop-in overrides: sshd_config looks hardened, but a cloud-init file in
sshd_config.dwins because it is read first. - SELinux disabled to fix one application and never re-enabled.
- Shared admin accounts and keys: one key copied to every server for "ops", with no owner and no expiry.
- Hardening once, by hand: no configuration management, so new servers and rebuilt ones miss half the baseline.
- Logs only on the host: audit trails that root can delete are gone when needed.
Framework mapping
| Framework | Relevant controls |
|---|---|
| NIST SP 800-53 Rev. 5 | CM-6 Configuration Settings; CM-7 Least Functionality; AC-6 Least Privilege; AC-17 Remote Access; AU-2 and AU-12 event logging; SI-2 Flaw Remediation; SI-7 integrity; SC-7 Boundary Protection |
| NIST CSF 2.0 | PR.PS-01 configuration management; PR.PS-02 software maintenance; PR.PS-04 log records generated for monitoring; PR.AA-05 least privilege access; DE.CM-09 monitoring of computing hardware and software |
| CIS Controls v8.1 | 4.1 secure configuration; 4.4 server firewall; 4.7 default accounts; 4.8 unnecessary services; 5.4 dedicated admin accounts; 7.3 OS patching; 8.2 and 8.9 collect and centralise audit logs |
| ISO/IEC 27001:2022 Annex A | A.8.2 Privileged access rights; A.8.5 Secure authentication; A.8.9 Configuration management; A.8.15 Logging; A.8.20 Networks security |
| MITRE ATT&CK | T1021.004 SSH; T1098.004 SSH Authorized Keys; T1548.001 Setuid and Setgid; T1548.003 Sudo and Sudo Caching |
Practitioner checklist
- Pick CIS Level 1 Server, Level 2 or the STIG per server role and version a tailoring file with reasons.
- Set
PermitRootLogin no,PasswordAuthentication no,KbdInteractiveAuthentication noandAllowGroups, and verify withsshd -T. - Inventory SSH keys and service accounts, assign owners and move to SSH certificates where practical.
- Replace broad sudo rules with command-specific ones, and enable
use_ptyand a sudo log file. - Keep SELinux or AppArmor enforcing and add systemd sandboxing to every custom service.
- Deploy the sysctl and nftables baselines from version control.
- Load auditd rules for identity files, sudoers, sshd_config, privileged execution and modules, with
-e 2. - Initialise AIDE, store its database off-host and run checks at least daily.
- Forward auth, sudo and audit logs to the SIEM and alert when a host goes quiet.
- Scan with OpenSCAP or CIS-CAT on a schedule and report drift per role.
Standards and references
- CIS: CIS Benchmarks
- OpenSSH (man7.org): sshd_config(5)
- Sudo (man7.org): sudoers(5)
- Linux kernel documentation: Documentation for /proc/sys/kernel/
- Linux Audit (man7.org): audit.rules(7)
- systemd (man7.org): systemd.exec(5)
- Red Hat: Checking integrity with AIDE
- nftables wiki: Simple ruleset for a server
Related NHI Mgmt Group resources: SSH Key Management Guide · Service Account Security Guide · Secrets Management Guide · Endpoint Security Guide · Privileged Access Management Guide