CIS benchmarks and OpenSCAP
Scan, tailor and remediate with evidence.
A benchmark turns "is this server hardened?" into a list of checks that each pass or fail. In this lesson you learn what the CIS benchmarks are and how they differ from vendor guidance, scan a Rocky Linux 10.2 server against the CIS Level 1 Server profile with OpenSCAP, read the results, apply and roll back the fix for one rule, record an exception in a tailoring file, and find out what is and is not available for Ubuntu 26.04. It ends the course, so it also says where to go next.
Whose recommendations these are
The Center for Internet Security (CIS) is a non-profit that publishes consensus hardening benchmarks for operating systems and applications. A benchmark is a numbered list of recommendations, each with a rationale, an audit procedure and a remediation, grouped into profiles: Level 1 aims to reduce attack surface without breaking normal use, Level 2 goes further for high-security environments and expects some functionality to be lost. CIS is a third party. Its benchmarks are not the operating system's defaults, and they are not Red Hat's or Canonical's own guidance (Red Hat's security hardening guide and Ubuntu's security documentation), although the vendors help write them and ship tooling for them. Other baselines, such as the DISA STIG, overlap heavily but differ in detail.
Checking hundreds of recommendations by hand is not practical, so the checks are written as SCAP content (Security Content Automation Protocol): an XCCDF checklist of rules and profiles, OVAL tests that inspect the system, and remediation snippets, bundled in one "data stream" file. OpenSCAP's oscap is the scanner that evaluates it. The SCAP Security Guide (SSG, from the ComplianceAsCode project) is the content, and on RHEL-family systems it ships as the scap-security-guide package. SSG's CIS profiles implement the CIS benchmark; CIS's own assessor tool (CIS-CAT) is a separate product.
Rocky's package carries a Rocky data stream (ssg-rl10-ds.xml) next to the RHEL one; scan with the one for your distribution. The CIS profile names follow the benchmark: cis is Level 2 Server, cis_server_l1 is Level 1 Server, and the two workstation profiles are for desktops. This release of the content encodes version 1.0.1 of the CIS Red Hat Enterprise Linux 10 Benchmark; CIS renumbers and re-levels recommendations between benchmark versions, so always say which one you scanned against.
Scan
A scan only reads the system. --results saves machine-readable results (the evidence you keep and compare), --report writes an HTML report for people, and the per-rule lines on standard output are worth keeping too. On this lab server the whole Level 1 scan takes about twenty seconds.
oscap exits with 0 when every rule passes, 2 when the scan worked and at least one rule failed, and 1 on an error, so a script that runs scans must treat 2 as a normal result. The warning line comes from one OVAL test in this content release and does not stop the scan. Count the results and read the score:
notapplicable rules test something this system does not have (for example, a package that is not installed). The score is weighted by rule, so 69 is not "69 percent of rules passed". It is a trend line for this host and this profile, not a grade to compare across different profiles. The results file also records which content produced it: the SSG release and the benchmark version of the profile.
Keep those two numbers with every results file. A scap-security-guide update can add, split or retune rules, and the counts and score then move with no change on the host. The failures are where the work is:
Many of these are controls this course has already covered: AIDE (the previous lesson), faillock and password history (PAM), sudo logging and use_pty (sudo hardening), login banners, a separate /tmp partition (mounts). Some are cheap on this server, some need a change window, and some do not fit it at all. "Ensure Red Hat GPG Key Installed" fails even with the Rocky content, because Rocky signs its packages with its own key: a reminder to read each failure before acting on it. A default Rocky install fails a benchmark by design; the defaults serve many uses, and the benchmark describes one hardened one. SELinux, which you configured in the last lesson, appears too: the CIS Level 1 profile requires it not to be disabled, and the Level 2 profile requires enforcing mode.
Generate fixes, read them, apply one
oscap xccdf generate fix writes a bash (or Ansible) remediation for every rule that failed in a results file. Generate it, and read it before anything runs.
125 fixes, one per failed rule, and some of them change how people log in. The SSH fix shows why reading matters. It writes PermitRootLogin no into /etc/ssh/sshd_config.d/00-complianceascode-hardening.conf, which sorts first and therefore wins under sshd's first-match rule (the SSH lesson's drop-in convention). It also deletes every PermitRootLogin line from sshd_config and from every other file in sshd_config.d, including files your configuration management or cloud-init owns, which will fight back on their next run. Other fixes in the same script enable and start firewalld (installed and enabled on this lab server, but stopped) and change its zones, rewrite the authselect PAM profile and set a boot-loader password, any of which can lock you out or stop the application the server exists for.
So remediate one rule at a time, after reading its fix. --rule limits a scan to one rule, and --remediate runs the fix for rules that fail. The rule here stops the host sending ICMP redirects, which only a router needs (the network sysctls lesson explains the setting).
The fix set the running value and wrote /etc/sysctl.d/net_ipv4_conf_all_send_redirects.conf so it persists. Verify with the system's own tools as well as the scanner: sysctl shows the live value and the file shows why it will survive a reboot. The rollback for this rule is the reverse:
oscap xccdf eval --remediate without --rule applies every fix in the profile, and bash remediate.sh does the same. Apply fixes in a staging copy of the server first, take a snapshot or backup of the real one, keep a second way in (a console as well as the SSH session the change might break), and change one area at a time with a rollback written down. CIS recommendations are a starting point you tailor, not a script to run.Tailoring: exceptions with a reason
Some rules will not apply to a given server. On Rocky, "Ensure Red Hat GPG Key Installed" can never pass: Rocky's packages are signed with Rocky's own key, and that is the only key installed.
The right response is a documented exception, not a failing rule everyone learns to ignore and not an edited copy of the profile. A tailoring file is a small XCCDF document that extends a profile: it can unselect rules, select extra ones and change values such as a minimum password length. autotailor (in the openscap-utils package) writes one. On RHEL 10.2 that package pulls in about thirty packages of RPM build tooling, so many teams run it on an admin machine and copy the tailoring file to servers.
The tailored profile cis_server_l1_customized is the base profile minus one rule: 124 failures instead of 125, the rule absent from the output and marked notselected in the results. The file records what was excepted but not why. Keep the reason (Rocky signs with its own key; the Rocky key is checked instead by dnf's gpgcheck=1) with the tailoring file in version control, review it when the server changes, and scan with the tailoring file from then on, so the report shows only failures someone still has to act on.
Once tailored, a scheduled scan (a systemd timer running oscap xccdf eval weekly, keeping each results file) turns the benchmark into drift detection: a rule that passed last week and fails today means something changed on the host, whether a hurried fix during an outage or an intruder re-enabling root login. Compare rule results as well as the score, and treat exit status 1 as the error, 2 as "some rules fail". Store the SSG version with each results file (rpm -q scap-security-guide, or the version lines above), and after a content update take a new baseline scan before you compare again. On more than one server, the results need a home: archive each host's results.xml centrally, or use a compliance service that runs the same SCAP content across your hosts, so you can compare hosts as well as weeks.
The same applies to every control in this course. Each drop-in you wrote (sshd, sudoers, PAM, sysctl, modprobe, nftables, AIDE) is a file for configuration management, not a hand edit. Run its validator as the tool's check step before the file is installed (sshd -t, visudo -cf, nft -c -f, findmnt --verify), create what a file depends on before the file (the group before AllowGroups, the key list before RevokedKeys), and roll a change out to a few canary hosts before the rest. A scan then catches the host someone changed by hand.
Ubuntu 26.04
Canonical's supported CIS tooling is the Ubuntu Security Guide (USG), an Ubuntu Pro service built on the same SSG content: sudo pro enable usg, sudo apt install usg, then usg audit cis_level1_server, usg fix and usg generate-tailoring. Canonical documents it for 20.04, 22.04 and 24.04. On 26.04 it is not offered yet:
The scanner is in the archive (openscap-scanner), and the community ssg-debderived package in universe carries SSG content, but only for older releases.
All 405 rules of the 24.04 Level 1 profile come back notapplicable and oscap exits 0: a clean-looking result that checked nothing. The content starts with a platform check (a CPE, Common Platform Enumeration, name for the older Ubuntu 24.04 release) that this system does not match, and the 26.04 openscap package also reports its own CPE dictionary missing. The skip is the correct behaviour: settings, package names and defaults changed between releases (sudo-rs and the Rust coreutils, for a start), and a 24.04 result on a 26.04 host would be misleading even if you forced it. Until USG or SSG ships content for 26.04, apply the controls in this course, and where you must show compliance, check the CIS benchmark document for the release (from the CIS website) by hand.
Where to go next
This is the last lesson of Linux hardening. The next course, Advanced Linux security (linux-det), takes the attacker's view of the same hosts: privilege escalation paths and how to find them first, the audit pipeline and detections built on the auditd rules you wrote, osquery, eBPF and Falco, hunting, live triage and incident response. It assumes Linux essentials and this course: auditd installed with keyed rules, AppArmor or SELinux enforcing, SUID, cron and timer baselines, logs forwarded with correct time, and file integrity monitoring in place. Advanced Linux internals (linux-perf) is optional if you want the kernel and systemd mechanisms behind sysctls, cgroups and units in more depth.
Try this
On a RHEL-family lab machine with scap-security-guide installed, run the Level 1 Server scan above and keep results.xml. Pick one failing rule from the list that you understand from this course, read its fix in the generated script, and apply it with --rule and --remediate. Confirm the change with the system's own command (for a sysctl, sysctl; for a file, stat or cat), re-run the scan with --rule to see it pass, then undo the change and confirm the rule fails again. Finally, write a tailoring file that unselects one rule you cannot meet on your lab, scan with it, and check that the rule is notselected in the new results.
Takeaway
Use a CIS profile as a measured starting point, not a script: scan, tailor out what does not fit with a written reason, fix one reviewed area at a time with a rollback ready, and re-scan on a schedule so a newly failing rule tells you what changed. On Ubuntu 26.04, do not scan with content made for another release.