OpenSCAP Hardening

Running SCAP scans with vendor content, tailoring profiles to reality, generating remediations, and turning compliance findings into repeatable engineering work.

Compliance heuristics
  • 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.

ComponentWhat it isWhy you care
OpenSCAPThe oscap CLI and librariesRuns scans, generates reports, and produces remediation content
SSGVendor/community-authored benchmark contentGives you CIS/STIG/OSPP-style profiles without writing rules yourself
XCCDF profileA chosen set of rules and variable valuesDefines what "compliant" means for this host class
ARF/XML resultsMachine-readable scan outputBest artifact for CI, diffing, and downstream parsing
HTML reportHuman-readable rendering of the resultsUseful 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
Pick the profile for the host class, not the audit mood. Jumping straight to 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:

# Re-render a report later from saved XML
oscap xccdf generate report /var/tmp/oscap-results.xml \
  > /var/tmp/oscap-report-regenerated.html
Do not use scan-time remediation on your first run. 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
Generated playbooks are scaffolding. Use them to discover what the benchmark expects, then implement the durable version in your roles, image build, or host baseline code. See Ansible and Ansible Best Practices.

Exemptions and waivers

There are two separate things here:

  1. Tailoring changes the benchmark itself for a host class.
  2. 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

SymptomLikely causeWhat 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