OpenSCAP Hardening
- Scan first, remediate second. A baseline report tells you what the profile wants; blind remediation tells you what you just broke.
- Keep vendor content pristine. Tailor with a separate file instead of editing the SSG datastream shipped by the package manager.
- Generated Bash and Ansible fixes are review material, not doctrine. Fold the useful parts back into your real config-management code.
- Every exemption needs an owner, a reason, and an expiry date. Otherwise "temporary waiver" becomes permanent drift.
- Fail pipelines on meaningful regressions, not on every low-value banner or package-removal rule that does not apply to your image class.
SCAP and SSG overview
OpenSCAP is the scanner and tooling. SCAP is the content format family: XCCDF for rules/profiles, OVAL for checks, ARF for results, and a datastream to bundle it all together. SSG (scap-security-guide) is the rule content most Linux admins actually use on RHEL, Alma, Rocky, and similar distributions.
| Component | What it is | Why you care |
|---|---|---|
| OpenSCAP | The oscap CLI and libraries | Runs scans, generates reports, and produces remediation content |
| SSG | Vendor/community-authored benchmark content | Gives you CIS/STIG/OSPP-style profiles without writing rules yourself |
| XCCDF profile | A chosen set of rules and variable values | Defines what "compliant" means for this host class |
| ARF/XML results | Machine-readable scan output | Best artifact for CI, diffing, and downstream parsing |
| HTML report | Human-readable rendering of the results | Useful for triage, not ideal as the system of record |
Examples below assume an EL 9-style host, but the workflow is the same on EL 8 or on other distributions with matching content packages.
Datastreams and profiles
The first job is finding the right datastream file for the OS version you are scanning. The second job is identifying the correct profile ID inside that file.
dnf install -y openscap-scanner openscap-utils scap-security-guide
# Typical content paths on RHEL/Alma/Rocky
ls /usr/share/xml/scap/ssg/
ls /usr/share/xml/scap/ssg/content/
DS=/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
oscap info "$DS"
# Narrow the output to profiles and tailoring components
oscap info "$DS" | sed -n '/Profiles:/,$p'
Document type: Source Data Stream
Imported: 2026-02-01T00:00:00
Profiles:
xccdf_org.ssgproject.content_profile_cis_server_l1
xccdf_org.ssgproject.content_profile_stig
xccdf_org.ssgproject.content_profile_ospp
stig for every VM usually produces avoidable churn. Most teams start with a CIS L1 profile or a tailored internal baseline and graduate upward where the workload justifies it.
Running a baseline scan
Start with a read-only evaluation. Produce both ARF/XML and HTML so you have one artifact for machines and one for humans.
DS=/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
PROFILE=xccdf_org.ssgproject.content_profile_cis_server_l1
oscap xccdf eval \
--profile "$PROFILE" \
--progress \
--oval-results \
--results /var/tmp/oscap-results.xml \
--results-arf /var/tmp/oscap-results.arf.xml \
--report /var/tmp/oscap-report.html \
"$DS"
Result states matter:
passmeans the check succeeded.failmeans the rule applied and the host did not meet it.notapplicablemeans the rule does not apply to this system class.errormeans the scanner could not determine a result cleanly. Treat these separately from true fails.notcheckedusually means a rule was selected but had no usable check in the current context.
# Re-render a report later from saved XML
oscap xccdf generate report /var/tmp/oscap-results.xml \
> /var/tmp/oscap-report-regenerated.html
oscap xccdf eval --remediate exists, but it is for controlled or throwaway environments. Baseline first, then decide what you actually want to change.
Tailoring a profile
Tailoring is how you turn a vendor benchmark into your benchmark. The usual workflow is: copy IDs from oscap info, generate a tailoring file, then scan using the new profile ID from that file.
DS=/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
autotailor \
--output /etc/oscap/rhel9-tailoring.xml \
--new-profile-id xccdf_org.example.content_profile_prod \
--unselect service_usbguard_enabled \
--var-value var_accounts_tmout=900 \
--var-value var_accounts_passwords_pam_faillock_deny=5 \
"$DS" cis_server_l1
oscap info /etc/oscap/rhel9-tailoring.xml
oscap xccdf eval \
--profile xccdf_org.example.content_profile_prod \
--tailoring-file /etc/oscap/rhel9-tailoring.xml \
--results /var/tmp/oscap-tailored-results.xml \
--report /var/tmp/oscap-tailored-report.html \
"$DS"
autotailor does not validate your IDs. If you typo a rule or value ID, it happily writes the tailoring file anyway. Always run oscap info against the output and then do a real evaluation with it.
Generating remediation
Generating a fix script is the safest way to see what OpenSCAP wants to do before it touches the box. Review the generated code exactly like you would review any other automation.
DS=/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
PROFILE=xccdf_org.example.content_profile_prod
TAILOR=/etc/oscap/rhel9-tailoring.xml
oscap xccdf generate fix \
--profile "$PROFILE" \
--tailoring-file "$TAILOR" \
--fix-type bash \
"$DS" > /var/tmp/oscap-remediate.sh
bash -n /var/tmp/oscap-remediate.sh
less /var/tmp/oscap-remediate.sh
Typical generated fixes include package installs/removals, systemd state changes, authselect changes, sysctl drop-ins, SSH hardening, and file-permission adjustments. Compare those actions against your existing Ansible or image-build logic before running anything.
# Only for controlled environments after review
bash /var/tmp/oscap-remediate.sh
# Re-scan to confirm the actual delta
oscap xccdf eval \
--profile "$PROFILE" \
--tailoring-file "$TAILOR" \
--results /var/tmp/post-remediation.xml \
"$DS"
Ansible remediation
For managed fleets, generate Ansible instead of shell. It is easier to review, version, and merge into existing roles.
oscap xccdf generate fix \
--profile xccdf_org.example.content_profile_prod \
--tailoring-file /etc/oscap/rhel9-tailoring.xml \
--fix-type ansible \
/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml \
> /var/tmp/oscap-remediate.yml
ansible-playbook -i inventory/prod /var/tmp/oscap-remediate.yml --limit web01
# Better long-term pattern: pull the generated ideas into your own role
- name: Enforce TMOUT
ansible.builtin.lineinfile:
path: /etc/profile.d/99-hardening.sh
create: true
mode: '0644'
line: 'readonly TMOUT=900'
- name: Ensure firewalld is enabled
ansible.builtin.systemd:
name: firewalld
enabled: true
state: started
Exemptions and waivers
There are two separate things here:
- Tailoring changes the benchmark itself for a host class.
- Waivers record exceptions in your process so the business decision is visible and expires on purpose.
Do not bury a hard exception in tribal memory. Put it in version control.
# compliance/waivers.yml
- rule: xccdf_org.ssgproject.content_rule_service_usbguard_enabled
scope: "all-vm-guests"
reason: "No USB passthrough on this platform; service is irrelevant"
owner: "platform-team"
expires: "2026-12-31"
- rule: xccdf_org.ssgproject.content_rule_accounts_tmout
scope: "break-glass-hosts"
reason: "Interactive admin sessions must remain active during migration window"
owner: "security-architecture"
expires: "2026-06-30"
Keep the authoritative exception in the tailoring file if it changes benchmark applicability. Keep the governance metadata in a waiver file or ticket system, and make your CI fail when a waiver is expired.
CI and image workflows
Compliance becomes tractable when you scan built images or golden root filesystems in CI, publish the ARF and HTML as artifacts, and only fail on the subset of rules your platform team has agreed to enforce.
compliance:rhel9:
stage: test
image: registry.access.redhat.com/ubi9/ubi:latest
script:
- dnf -y install openscap-scanner openscap-utils scap-security-guide
- mkdir -p artifacts
- |
oscap-chroot /mnt/image xccdf eval \
--profile xccdf_org.example.content_profile_prod \
--tailoring-file /workspace/compliance/rhel9-tailoring.xml \
--results-arf artifacts/oscap-results.arf.xml \
--report artifacts/oscap-report.html \
/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
artifacts:
when: always
paths:
- artifacts/oscap-results.arf.xml
- artifacts/oscap-report.html
This is a strong pattern for image pipelines, kickstart-built templates, and cloud-image validation. Pair it with CI/CD Pipelines so the scan output is archived, diffable, and reviewed like any other build artifact.
Troubleshooting noisy findings
| Symptom | Likely cause | What to do |
|---|---|---|
Lots of error results, not just fail |
Wrong content for the OS version, missing package context, or unsupported scan target | Confirm the datastream matches the target OS and inspect stderr plus the raw XML, not just the HTML report |
| Container or image scans fail host-oriented rules | Using a full server profile against a minimal image or container rootfs | Tailor out irrelevant service/kernel rules or use a profile designed for that artifact class |
| Remediation succeeds, then later the host drifts back | Config management or image build pipeline overwrites the same files | Move the chosen remediation into Ansible, image build, or another source of truth |
| Package-removal or file-banner rules are always noisy | Benchmark is stricter than the platform standard for that host class | Tailor with intent; do not keep re-litigating the same accepted exception on every scan |
Profile not found or rule not found |
Wrong ID or copied an ID from a different SSG release | Use oscap info on the exact datastream installed on the scanner host and regenerate the tailoring file |
| Scans take too long on CI workers | Heavy OVAL checks, slow storage, or scanning more than you need | Run against a built image rootfs locally on the worker, and keep the enforced profile smaller than the audit-only profile |
# Useful triage loop
DS=/usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
PROFILE=xccdf_org.ssgproject.content_profile_cis_server_l1
oscap xccdf eval --profile "$PROFILE" --results /tmp/results.xml "$DS"
oscap xccdf generate report /tmp/results.xml > /tmp/results.html
less /tmp/results.html
# If a rule looks wrong, inspect the installed content first
oscap info "$DS"
Cross-reference
- Enterprise Linux Lifecycle for the distro/package context around EL 8/9 content.
- SELinux Debugging because many hardening profiles touch labels, booleans, and ports.
- systemd Unit Authoring for the service-hardening directives benchmarks often recommend.
- sysctl Tuning for the kernel tunables that appear in both CIS and STIG-style baselines.
- CI/CD Pipelines and Ansible for making remediation and validation repeatable.