Hardline controls
Every Hardline hardening control by phase, from SSH and firewall to kernel and accounts: why it matters, what it checks, how to undo it, plus STIG IDs.
All 67 built-in controls by phase. Every finding in hardline audit links here, and hardline explain <id> prints the same text in your terminal.
SSH · Firewall · Access control · Auditing · TLS · Containers · Kernel · Accounts
Phase 1 · SSH
Key-only SSH login
Passwords can be guessed and are sprayed at every public SSH server all day. Keys can't. Hardline turns off PasswordAuthentication and KbdInteractiveAuthentication and caps login attempts at 3.
Before it does, it counts the keys in every login user's authorized_keys. A user without a key keeps password login through a Match User block, so nobody gets locked out. Add a key, then re-run apply.
- Key-only SSH: disable keyboard-interactive login
- Allow at most 3 SSH authentication attempts
- Key-only SSH: disable password login
No direct root login
Log in as yourself and use sudo, so every root action is tied to a person and goes through the audit log. Hardline sets PermitRootLogin no and checks that root's password is locked (passwd -S root).
- No direct SSH login as root
- Root password is locked
Empty passwords and user environments
PermitEmptyPasswords no stops an account with a blank password from logging in at all. PermitUserEnvironment no stops users from setting variables such as LD_PRELOAD through ~/.ssh/environment, which can get around restrictions on what they're allowed to run.
- Refuse SSH logins with empty passwords
- Ignore user-supplied SSH environment files
Idle session timeout
An SSH session left open on an unattended laptop is a root shell waiting for someone. ClientAliveInterval 600 and ClientAliveCountMax 1 drop a session after 10 minutes with no response.
- End unresponsive SSH sessions after one keepalive
- Drop idle SSH connections after 10 minutes
X11 forwarding
Servers don't need to show windows on your desktop. With X11 forwarding on, a compromised server can reach back into your local X session and log your keystrokes. Hardline sets X11Forwarding no.
- Disable SSH X11 forwarding
STIG ciphers, MACs and key exchange
stig profile only. Limits SSH to the FIPS 140-3 approved algorithms the Ubuntu 24.04 STIG requires. Very old clients might not be able to connect afterwards, so baseline keeps OpenSSH's modern defaults.
- SSH uses FIPS 140-3 approved ciphers
- SSH uses FIPS 140-3 approved key exchange
- SSH uses FIPS 140-3 approved MACs
Phase 2 · Firewall
Host firewall
Hardline turns on a default-deny inbound firewall, allows every port that's listening right now, and rate-limits SSH per source address. That way, enabling the firewall doesn't take down a running service.
--firewall ufw (the default) uses ufw. --firewall nftables writes a single table inet hardline and never flushes rules that belong to Docker, LXD or anything else. On RHEL, Rocky and AlmaLinux, Hardline installs firewalld if needed, opens SSH and every listening port in the default zone with firewall-offline-cmd, checks the result with --check-config, then starts firewalld. Firewall changes are guarded the same way SSH changes are.
- A host firewall is active
- SSH is rate-limited per source address
- ufw is enabled
- ufw is installed
- ufw rate-limits every listening port
Phase 3 · Access control
AppArmor
AppArmor limits what each profiled program can touch, even when it runs as root. Hardline makes sure it's installed, enabled and enforcing. A profile in complain mode only logs what it would block. Hardline reports those profiles but doesn't switch them to enforce, since that can break the program.
- AppArmor is enabled with policy loaded
- AppArmor is installed
- No AppArmor profiles in complain mode
SELinux
RHEL, Rocky Linux and AlmaLinux 9 only. SELinux confines every service to what its policy allows, even when it runs as root. Hardline checks it is Enforcing now and set to enforcing in /etc/selinux/config, so a reboot doesn't quietly turn it off. Hardline only reports this one: switching it on from disabled needs a full relabel and a reboot, and is best done in a maintenance window.
- SELinux is enforcing, now and at boot
Phase 4 · Auditing
Audit logging
auditd records who changed accounts, groups, sudoers and passwords, and every sudo command. Rules go in /etc/audit/rules.d/hardline.rules and are loaded with augenrules. Hardline normalises them the same way auditctl -l prints them, so the comparison is exact.
- auditd is enabled and running
- auditd is installed
- Audit every use of sudo
- Audit changes to /etc/group
- Audit changes to /etc/gshadow
- Audit changes to /etc/security/opasswd
- Audit changes to /etc/passwd
- Audit changes to /etc/shadow
- Audit changes to /etc/sudoers
- Audit changes to /etc/sudoers.d
Immutable audit rules
stig profile only. -e 2 locks the audit rules until the next reboot, so someone with root access can't quietly switch auditing off. That also applies to you: undoing this control needs a reboot, and Hardline warns before it applies it.
- Audit rules are immutable until reboot
Phase 5 · TLS
TLS on nginx and Apache
Hardline is read-only here. It finds the HTTPS listeners in your nginx or Apache config, checks that only TLS 1.2 and 1.3 are offered, and warns when a certificate expires within 21 days. Web server configs vary too much to edit safely, so each finding says what to change.
- TLS certificates are not about to expire
- Web servers offer only TLS 1.2 and 1.3
Phase 6 · Containers
Docker daemon
Hardline merges a few safe defaults into /etc/docker/daemon.json: no traffic between containers on the default bridge, no-new-privileges, live-restore, and capped json-file logs. Your other settings are left alone. It refuses to apply a setting that is also passed as a dockerd flag, because Docker won't start when a setting is in both. Docker isn't restarted for you. Restart it when it's convenient.
Containers running as root are reported but not changed.
- daemon.json options aren't also passed as dockerd flags
- No traffic between containers on the default bridge
- Containers keep running across daemon restarts
- Container logs use the json-file driver
- At most 3 rotated log files per container
- Container log files are capped at 10 MB
- Containers cannot gain new privileges by default
- Running containers don't run as root
Published container ports
Docker writes its own iptables rules, so -p 5432:5432 is open to the internet even with ufw set to deny everything. Hardline lists every port published on all addresses. Bind them to 127.0.0.1 (-p 127.0.0.1:5432:5432) and put a reverse proxy in front of anything that has to be public.
- No container ports published on all addresses
Phase 7 · Kernel
Kernel settings
A few sysctl settings that make common local attacks harder: full address space randomization, kernel pointers and the kernel log hidden from normal users, symlink and hardlink protection in shared directories, and SYN cookies against SYN floods. Most are already Ubuntu's defaults. Hardline checks the running value, so a stray file in /etc/sysctl.d that weakens one gets caught.
Fixes go in /etc/sysctl.d/99-zz-hardline.conf, which sorts after Ubuntu's own files so it wins at boot, and are loaded right away with sysctl -p. Rollback puts back the values that were running before.
- Only root can read the kernel log
- Kernel pointers are hidden from users
- Hardlink attacks on other users' files are blocked
- Symlink attacks in shared directories are blocked
- Full address space layout randomization
Kernel modules
Linux loads drivers for USB storage and a few rarely used network protocols (DCCP, RDS, TIPC) automatically when something asks for them. Old bugs in these protocols have let local users get root, and a server almost never needs them. Hardline writes install <module> /bin/false and blacklist <module> to /etc/modprobe.d/hardline.conf, so the kernel can't load them.
A module that is already loaded stays loaded until the next reboot. Rollback deletes the file.
- The DCCP network protocol can't be loaded
- The RDS network protocol can't be loaded
- The TIPC network protocol can't be loaded
- USB mass storage can't be loaded
Phase 8 · Accounts
Accounts
Every account needs a password or a lock. An empty password field in /etc/shadow lets anyone log in at the console. Hardline lists those accounts and leaves the choice to you: sudo passwd <user> or sudo passwd -l <user>.
The stig profile also wants sudo to ask for a password and new accounts to be disabled 35 days after their password expires (INACTIVE=35 in /etc/default/useradd). Cloud images give the default user passwordless sudo, so Hardline only reports that one. Set a password for that user before you remove NOPASSWD, or you lose sudo.
- New accounts are disabled 35 days after their password expires
- No account has an empty password
- sudo asks for a password
Password rules
Hardline installs libpam-pwquality, which checks new passwords whenever someone runs passwd. SSH keys aren't affected.
The stig profile sets the DISA values: at least 15 characters, with an uppercase letter, a lowercase letter, a digit and a symbol, at least 8 characters different from the old password, no dictionary words, and a minimum age of 1 day and a maximum of 60 in /etc/login.defs. Hardline edits only those lines and leaves the rest of each file alone. The ages apply to accounts created afterwards. pwquality.conf comes with the package, so on a fresh server run apply twice: once to install it, and again to set the values.
- Passwords expire after 60 days
- Passwords can be changed at most once a day
- New passwords need a digit
- New passwords can't be dictionary words
- New passwords change at least 8 characters
- New passwords need a lowercase letter
- New passwords are at least 15 characters
- New passwords need a special character
- New passwords need an uppercase letter
- Password quality checks are installed
Add a control
Each control is a TOML file plus a test, and needs either a STIG reference or a written reason it's baseline-only. Start with CONTRIBUTING.md.