ubuntu
Applying the CIS Ubuntu benchmark — the controls that matter and the ones we skip
A pragmatic walk through CIS Ubuntu 22.04 and 24.04 Level 1 and Level 2: which controls move attacker economics, which produce yellow ticks for auditors, and how to audit at scale.
15 de mayo de 2026 · 9 min · por Sudhanshu K.
Applying the CIS Ubuntu benchmark — the controls that matter and the ones we skip
The CIS Ubuntu Linux 22.04 LTS Benchmark v2.0.0 has 244 individual controls across Level 1 and Level 2. The CIS Ubuntu 24.04 benchmark, currently at v1.0.0, has 232. If you treat either document as a checklist to be ticked top to bottom, you will spend two weeks on a single server, produce a report that's 90% green, and emerge with security gains that are mostly cosmetic.
We apply CIS across our managed Ubuntu fleet because customers ask for it and because — used correctly — it's a genuinely useful inventory of well-tested controls. But we apply it selectively, with reasons for every exception, and we audit continuously rather than just at point-in-time. This post is what we actually do.
Level 1 vs Level 2 — the practical distinction
CIS divides controls into two profiles:
- Level 1 is "should not impact functionality for most use cases." Think disabling unused filesystems, banner messages, password complexity, SSH client config, syslog destinations.
- Level 2 is "may impact functionality" — full AppArmor enforcement on every named profile, removing entire package categories, disabling kernel modules that some workloads need.
For a baseline server build, we apply all Level 1 and a deliberately curated subset of Level 2. The Level 2 items we skip are the ones we've found cause more incidents than they prevent, or where a different control (a network policy, a host-level firewall rule, an admission policy upstream) already mitigates the same threat.
The controls that actually move attacker economics
If you only had budget to apply a fraction of the benchmark, this is the high-leverage list. Every item below has materially changed the outcome of a real incident we've responded to.
1. SSH hardening (CIS 5.2.x)
# /etc/ssh/sshd_config.d/00-cis.conf
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
X11Forwarding no
AllowTcpForwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 0
LoginGraceTime 60
Banner /etc/issue.net
HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.comOf all the CIS controls, this block is the one I'd apply first to a vanilla cloud image. Disabling password auth alone collapses the brute-force surface from "every botnet on the internet" to "nothing, unless your private keys leak."
2. Filesystem mount options (CIS 1.1.x)
# /etc/fstab — separate partitions with restrictive options
/dev/xvdb1 /tmp ext4 defaults,noexec,nodev,nosuid 0 2
/dev/xvdb2 /var/tmp ext4 defaults,noexec,nodev,nosuid 0 2
/dev/xvdb3 /home ext4 defaults,nodev 0 2
/dev/xvdb4 /var/log ext4 defaults,nodev,nosuid 0 2
/dev/xvdb5 /var/log/audit ext4 defaults,nodev,nosuid 0 2
tmpfs /dev/shm tmpfs defaults,noexec,nodev,nosuid 0 0noexec on /tmp and /var/tmp is the single most effective control against a class of post-exploitation payloads that drop a binary into /tmp and execute it. Doesn't stop everything, but it makes the easy attacks not work — which is the whole point.
3. Auditd policy aligned to CIS 4.1.x
A full auditd config is its own article, but the minimum CIS-aligned policy:
# /etc/audit/rules.d/cis.rules
-w /etc/sudoers -p wa -k scope
-w /etc/sudoers.d/ -p wa -k scope
-w /var/log/sudo.log -p wa -k actions
-a always,exit -F arch=b64 -S execve -F euid=0 -k root-cmd
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/ssh/sshd_config -p wa -k sshd
-a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=4294967295 -k mounts
-a always,exit -F arch=b64 -S unlink,unlinkat,rename,renameat -F auid>=1000 -F auid!=4294967295 -k delete
-e 2The -e 2 at the end makes the rule set immutable until reboot — an attacker with root can still flush audit logs, but they can't quietly disable the rules to stop further audit events being generated. This is one of those small details that distinguishes a real audit policy from a compliance-tick audit policy.
4. AppArmor in enforce mode (CIS 1.6.x)
# Check current state
sudo aa-status
# Switch any profiles in complain mode to enforce
sudo aa-enforce /etc/apparmor.d/*The Ubuntu default ships about 30 AppArmor profiles in enforce mode (for cups, tcpdump, evince etc.) and a handful in complain. CIS L2 requires all to be in enforce. This is the right answer for production: AppArmor profiles aren't free to write, but the bundled ones are battle-tested, and switching complain to enforce essentially never breaks anything for server workloads.
We extend AppArmor for our own services: every customer application gets a profile, and unconfined is not a state we leave any process in.
5. UFW or nftables default-deny (CIS 3.5.x)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.0.0.0/8 to any port 22 proto tcp
sudo ufw allow 443/tcp
sudo ufw enableThe whole point of a host-based firewall when you already have cloud security groups is defence in depth. If a security group is misconfigured (and they are, regularly — we see it in nearly every pen-test engagement we run), UFW catches it at the host. Default deny ingress, allow only what's needed, log everything else.
The CIS controls we deliberately skip or modify
Now the politically interesting half. These are CIS controls we don't apply by default — sometimes because they're outdated, sometimes because there's a better control, sometimes because they cause more pain than they prevent.
Disabling specific filesystem modules (CIS 1.1.1.x)
CIS recommends blacklisting cramfs, freevxfs, jffs2, hfs, hfsplus, udf, usb-storage, etc. via modprobe blacklists. The threat model is "an attacker plugs in a USB device" or "an attacker mounts an exotic filesystem to evade detection."
On a cloud VM, this is nonsense. There is no USB. There is no console. The kernel module loading attack surface is root-only to begin with. We skip these unless the customer has a specific physical-server compliance need.
Password complexity rules (CIS 5.4.x)
CIS L1 wants minimum 14-character passwords, complexity classes, history depth of 5, and a maximum age of 365 days. We disable interactive password authentication entirely (see the SSH section above), which makes most of these moot. The handful of break-glass accounts that can have a password use a randomly-generated 32+ character secret from a vault, with rotation tied to access events rather than calendar dates.
Password age policies that force rotation on a calendar tend to produce worse passwords (the famous "Password1!" -> "Password2!" pattern). NIST agrees with us on this in SP 800-63B; CIS hasn't caught up yet.
Disabling IPv6 (CIS 3.1.x optional)
Some CIS profiles recommend disabling IPv6 if unused. In 2026 this is the wrong default. IPv6 is on by default for a reason; the cost of having it enabled but unrouted is essentially zero, and we increasingly have workloads that use it. We leave it on and ensure UFW or nftables policy covers both stacks.
Removing all compilers (CIS 1.5.x adjacent)
The "remove gcc from production hosts" recommendation made sense in 2005. In 2026 most container builds and many language runtimes need a compiler available somewhere; the right control is "this server is not a build server, deploy is immutable, application code isn't compiled here," not "rm gcc and hope nothing breaks."
Audit tooling — what we actually use
You cannot manage CIS compliance by hand at scale. We use three layers:
Layer 1: USG (Ubuntu Security Guide)
Canonical's usg tool, included with Ubuntu Pro, ships CIS profiles for 22.04 and 24.04 as well as DISA STIG and others. Two key commands:
sudo usg audit cis_level1_server
sudo usg fix cis_level1_serveraudit produces an HTML and XML report. fix applies the controls. We use audit continuously and fix only during provisioning, with every change captured in our Ansible inventory.
Layer 2: OpenSCAP
For customers with compliance auditors who specifically ask for OpenSCAP/SCAP output (most government and regulated industry engagements), we run openscap-scanner against the upstream CIS SCAP content:
sudo apt install libopenscap8
sudo oscap xccdf eval \
--profile xccdf_org.cisecurity.benchmarks_profile_Level_1_-_Server \
--results /var/log/cis-results.xml \
--report /var/log/cis-report.html \
/usr/share/xml/scap/cis-ubuntu-24.04-benchmark.xmlOpenSCAP output is what 90% of formal audit programs expect to see.
Layer 3: continuous monitoring
USG runs nightly on every host. Results ship to a central index. Drift from baseline (any host that fails a control today that passed yesterday) opens a ticket automatically. This is the part most teams skip — they audit at provisioning time, declare victory, and never look again. CIS compliance has a half-life; without continuous monitoring you lose half your controls within six months as people make changes for legitimate operational reasons and forget to update the baseline.
How long does this actually take?
For a fresh Ubuntu 24.04 install on a server intended for production:
- USG-driven Level 1 application: ~5 minutes including reboot
- Custom Level 2 selections, AppArmor profile development, auditd rules: 30-60 minutes if it's a standard server profile, longer for bespoke workloads
- Initial OpenSCAP scan and remediation: 15 minutes
- Documenting exceptions for the controls we skipped and why: another 30 minutes
For an existing server that nobody's hardened, it's typically a half day to first pass and another half day to chase exceptions. Faster if you've automated this; we have, which is why our provisioning service ships every new Ubuntu host with the CIS baseline pre-applied and the documented exception register version-controlled.
The takeaway
CIS is a tool, not a religion. The benchmark's value is that someone competent has already enumerated the controls; your job is to apply judgment about which ones earn their keep in your environment. A fleet that scores 95% on Level 1 with documented exceptions for the other 5% is in much better shape than a fleet that scores 100% by ticking every box mechanically and pretending the auditd rules nobody monitors are doing something.
If you'd like a CIS audit of your existing Ubuntu fleet — including the analysis of which controls actually matter for your workloads — that's the kind of engagement our cybersecurity team runs regularly, and it's how a lot of customers come to us.
Sudhanshu K. is a security engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). She's been writing AppArmor profiles since they were called SubDomain and has views on the SCAP XCCDF schema that nobody else asked for.