The CIS Benchmarks from the Center for Internet Security are consensus configuration baselines for operating systems, each with hundreds of numbered recommendations and an audit and remediation procedure for each one. Rather than applying all of them at once, the practical approach is to measure where a server stands, fix the controls with the best security return, document the exceptions your workload needs, and measure again. In this tutorial you will do exactly that on Ubuntu 24.04: baseline the server with Lynis, apply a set of core CIS Level 1 controls by hand, and see how to automate the full benchmark.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, and a non-root user with
sudoprivileges who logs in with an SSH key. - Console access that does not depend on SSH (for example the VNC console in your provider's panel), in case an SSH change locks you out.
- Preferably, a test server that mirrors production. Test every control there first.
You can download the full benchmark PDF for free (registration required) from the CIS website. The section numbers mentioned below refer to the families of controls, since exact numbering changes between benchmark versions.
Understanding CIS profiles
Each benchmark defines two levels, and separate variants for servers and workstations:
| Profile | Intent | Typical impact |
|---|---|---|
| Level 1 Server | Essential hardening that any server should have | Low; rarely breaks applications |
| Level 2 Server | Defense in depth for sensitive environments | Can disable features or add operational overhead |
Start with Level 1 Server. Treat Level 2 as a list of candidates to evaluate one by one against what your applications need.
Step 1 - Measuring a baseline with Lynis
Lynis is an open-source auditing tool. It is not a CIS scanner, but its checks overlap heavily with the benchmark and it gives you a simple hardening index to track. Install it and run an audit:
sudo apt update
sudo apt install lynis
sudo lynis audit system --quiet
--quiet shows only warnings. The summary at the end of a normal run, and the report file, contain the score. Read it from the report:
sudo grep -E '^hardening_index=' /var/log/lynis-report.dat
hardening_index=62
List the warnings and suggestions, each with a test ID you can look up:
sudo grep -E '^(warning|suggestion)\[\]=' /var/log/lynis-report.dat | head -20
To see the details of a single finding, pass its test ID:
sudo lynis show details SSH-7408
Write down the hardening index. You will compare it with the result at the end of this guide.
Step 2 - Disabling unused filesystem modules
CIS section 1 asks you to prevent the kernel from loading filesystem drivers that a server does not need, which removes attack surface in rarely audited code. Create a modprobe configuration file:
sudo nano /etc/modprobe.d/cis-filesystems.conf
Add the following lines. install ... /bin/false makes any load attempt fail, and blacklist prevents automatic loading:
install cramfs /bin/false
blacklist cramfs
install freevxfs /bin/false
blacklist freevxfs
install jffs2 /bin/false
blacklist jffs2
install hfs /bin/false
blacklist hfs
install hfsplus /bin/false
blacklist hfsplus
install udf /bin/false
blacklist udf
Importantdo not add
squashfson Ubuntu even though older benchmarks list it. Snap packages are mounted as squashfs images, and blocking it breaks every snap on the system.
Verify that loading one of the modules is refused:
sudo modprobe -n -v cramfs
install /bin/false
Step 3 - Hardening kernel network parameters
CIS section 3 covers network parameters that stop the server from acting as a router, accepting spoofed routes or redirects, and flooding its log with nonsense. Create a sysctl file:
sudo nano /etc/sysctl.d/60-cis-hardening.conf
# The host is not a router
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# Do not send or accept ICMP redirects
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Reject source-routed packets
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Log packets with impossible addresses, enable reverse path filtering
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# ICMP and SYN flood protections
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
# Kernel protections
kernel.randomize_va_space = 2
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
fs.suid_dumpable = 0
Warningif the server runs Docker, Kubernetes, a VPN gateway or any other form of routing, it needs
net.ipv4.ip_forward = 1. Remove the two forwarding lines in that case and record it as an exception. Keep IPv6 enabled if your server has an IPv6 address; disabling it is a Level 2 choice, not a requirement.
Load every sysctl file and confirm a couple of values:
sudo sysctl --system > /dev/null
sysctl net.ipv4.conf.all.accept_redirects net.ipv4.tcp_syncookies
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.tcp_syncookies = 1
Step 4 - Hardening the SSH server
CIS section 5 includes about twenty SSH settings. On Ubuntu 24.04, /etc/ssh/sshd_config includes every file in /etc/ssh/sshd_config.d/ at the top, and for each option the first value read wins. A file with a low number therefore overrides both the main file and files such as 50-cloud-init.conf. Create one:
sudo nano /etc/ssh/sshd_config.d/10-cis-hardening.conf
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
PermitUserEnvironment no
HostbasedAuthentication no
IgnoreRhosts yes
X11Forwarding no
MaxAuthTries 4
MaxStartups 10:30:60
MaxSessions 10
LoginGraceTime 60
ClientAliveInterval 300
ClientAliveCountMax 3
LogLevel VERBOSE
Banner /etc/issue.net
PasswordAuthentication no is stricter than the benchmark requires, but it is the right choice for an internet-facing server. Make sure your key-based login works before applying it. The Banner line shows the text in /etc/issue.net before login; replace its content with a short authorized-use notice without OS version details:
echo "Authorized access only. Activity may be monitored." | sudo tee /etc/issue.net
Test the configuration and restart SSH only if the test passes:
sudo sshd -t && sudo systemctl restart ssh
Confirm the effective values with the extended test mode:
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|maxauthtries|x11forwarding)'
permitrootlogin no
passwordauthentication no
x11forwarding no
maxauthtries 4
Open a second SSH session now, while the first one is still connected, to confirm you can still log in.
Step 5 - Enforcing password quality and aging
Even with key-only SSH, local passwords are still used for sudo and the console. Install the pwquality PAM module; Ubuntu enables it automatically in /etc/pam.d/common-password:
sudo apt install libpam-pwquality
Set the CIS minimums in a drop-in file:
sudo nano /etc/security/pwquality.conf.d/50-cis.conf
minlen = 14
minclass = 4
maxrepeat = 3
enforce_for_root
Then set password aging defaults for new accounts in /etc/login.defs. Find the existing lines and change their values:
sudo nano /etc/login.defs
PASS_MAX_DAYS 365
PASS_MIN_DAYS 1
PASS_WARN_AGE 7
login.defs only affects accounts created from now on. Apply the same policy to your existing user, replacing your_user:
sudo chage --maxdays 365 --mindays 1 --warndays 7 your_user
sudo chage -l your_user
Verify the quality rules by trying a weak password as a normal user with passwd; it should be rejected with a message such as BAD PASSWORD: The password is shorter than 14 characters.
Step 6 - Checking file permissions
CIS section 7 (section 6 in older versions) checks the permissions of account files and looks for world-writable or unowned files. Ubuntu's defaults for the account files are already compliant, so check them rather than change them:
stat -c '%a %U:%G %n' /etc/passwd /etc/group /etc/shadow /etc/gshadow
644 root:root /etc/passwd
644 root:root /etc/group
640 root:shadow /etc/shadow
640 root:shadow /etc/gshadow
Search the local filesystems for world-writable files and for files without a valid owner. Any result deserves a look:
sudo find / -xdev -type f -perm -0002 -print
sudo find / -xdev \( -nouser -o -nogroup \) -print
Fix what you find individually, for example with sudo chmod o-w /path/to/file or by assigning the right owner. Never apply recursive permission changes to system directories.
Step 7 - Automating the full benchmark
The steps above cover the controls with the most impact, but the full Level 1 benchmark has well over a hundred checks. Two tools evaluate and fix them automatically.
Ubuntu Security Guide (USG) is Canonical's official CIS tooling for Ubuntu. It requires an Ubuntu Pro subscription, which is free for personal use on a small number of machines. After attaching the server with sudo pro attach, install it and run an audit:
sudo pro enable usg
sudo apt install usg
sudo usg audit cis_level1_server
The audit prints a summary and the path of an HTML report with the result of every rule. sudo usg fix cis_level1_server applies all the remediations of the profile at once, so review the audit report before running it, and run it on a test server first.
OpenSCAP with the SCAP Security Guide is free and open source and includes a CIS Level 1 Server profile for Ubuntu 24.04. It can generate a remediation script from the failed rules, so you can review and trim each change before applying it.
Whichever you choose, run the audit, go through the failures, and write down every rule you deliberately leave unfixed.
Step 8 - Documenting exceptions and re-measuring
Every hardened server has controls that do not fit its workload. Record them in a simple table kept next to your configuration, so that the next audit does not treat them as oversights:
| Control | Status | Reason | Compensating control |
|---|---|---|---|
| IP forwarding disabled | Exception | Host runs Docker | Firewall restricts published ports |
/tmp on separate partition | Exception | Single-disk VPS | Regular Lynis and file integrity scans |
Now run Lynis again and compare the hardening index with the value from Step 1:
sudo lynis audit system --quiet
sudo grep -E '^hardening_index=' /var/log/lynis-report.dat
hardening_index=74
To catch configuration drift, run the audit automatically every month. Create a systemd service:
sudo nano /etc/systemd/system/lynis-audit.service
[Unit]
Description=Monthly Lynis security audit
[Service]
Type=oneshot
ExecStart=/usr/sbin/lynis audit system --cronjob
Nice=10
And a timer that runs it on the first day of each month:
sudo nano /etc/systemd/system/lynis-audit.timer
[Unit]
Description=Run Lynis monthly
[Timer]
OnCalendar=monthly
Persistent=true
[Install]
WantedBy=timers.target
Enable the timer and check when it will run:
sudo systemctl daemon-reload
sudo systemctl enable --now lynis-audit.timer
systemctl list-timers lynis-audit.timer
Each run overwrites /var/log/lynis-report.dat with the latest score and findings.
Troubleshooting
You can no longer log in over SSH. Use the out-of-band console, then check which rule rejected you with sudo journalctl -u ssh -n 50. The usual causes are PasswordAuthentication no without a working key, or a login that took longer than LoginGraceTime. Remove or adjust the line in 10-cis-hardening.conf, run sudo sshd -t, and restart ssh.
Containers lose network access after Step 3. Docker needs IP forwarding. Remove the forwarding lines from 60-cis-hardening.conf, run sudo sysctl --system, and restart Docker.
A service account's password expired. Accounts used by applications should not expire. Set them to never expire and record the exception: sudo chage --maxdays -1 service_account.
Conclusion
You measured an Ubuntu 24.04 server against a CIS-aligned baseline, applied core Level 1 controls for kernel modules, network parameters, SSH, passwords and file permissions, documented the exceptions, and scheduled a monthly audit to catch drift. Next, run a complete CIS Level 1 audit with USG or OpenSCAP to close the remaining gaps, add file integrity monitoring such as AIDE, and put these settings into your configuration management so every new server starts hardened.
