Automating CIS Benchmark Compliance on Linux with OpenSCAP and Ansible

Automate CIS benchmark compliance scanning and remediation on Linux using OpenSCAP and Ansible. Covers RHEL 9 and Ubuntu 24.04 setup, Ansible Lockdown roles, drift detection, and CI/CD pipeline integration for audit-ready compliance at scale.

With over 5,500 Linux kernel CVEs reported in 2025 alone — and compliance frameworks tightening their requirements every year — manually hardening servers against CIS benchmarks just isn't sustainable anymore. The Center for Internet Security (CIS) benchmarks pack hundreds of individual configuration recommendations, and trying to apply and verify them by hand across a fleet of servers? That's tedious, error-prone, and honestly, borderline impossible to maintain at scale.

This guide walks you through a complete, automated CIS compliance workflow using OpenSCAP for scanning and reporting, the ComplianceAsCode project for security content, and Ansible for remediation and continuous enforcement. By the end, you'll have a repeatable pipeline that hardens fresh systems, detects configuration drift, and produces audit-ready reports — all without manual intervention.

What Are CIS Benchmarks and Why Automate Them?

CIS Benchmarks are consensus-based security configuration guidelines maintained by the Center for Internet Security. They provide prescriptive, vendor-neutral recommendations for hardening operating systems, cloud services, databases, and applications. Each benchmark is organized into numbered sections covering areas like filesystem configuration, user authentication, network settings, logging, and service management.

For Linux, CIS offers benchmarks at two hardening levels:

  • Level 1 — Practical baseline security measures with minimal impact on system functionality. Suitable for most production environments.
  • Level 2 — Stricter defense-in-depth controls for high-security environments. These may restrict certain system functionality, so tread carefully.

Each level has separate profiles for servers and workstations. A typical RHEL 9 CIS Level 2 Server profile contains over 300 individual controls spanning filesystem permissions, kernel parameters, PAM configuration, SSH hardening, firewall rules, auditd settings, and more. That's a lot of ground to cover manually.

So why automate? Three big reasons:

  1. Consistency — Every server gets the exact same hardening configuration, eliminating human error.
  2. Speed — A full CIS hardening pass that would take hours manually completes in minutes with Ansible.
  3. Continuous compliance — Scheduled scans detect drift immediately instead of waiting for the next quarterly audit.

Toolchain Overview

Our compliance automation stack uses three components that work together pretty seamlessly:

OpenSCAP

OpenSCAP is an open-source Security Content Automation Protocol (SCAP) toolkit that evaluates systems against security policies. It provides the oscap command-line scanner, which reads SCAP datastream files and produces detailed compliance reports in HTML, XML, and ARF formats. OpenSCAP can also generate remediation scripts in Bash or Ansible format directly from scan results — which is genuinely useful when you want targeted fixes.

ComplianceAsCode (scap-security-guide)

The ComplianceAsCode project — distributed as the scap-security-guide package — provides the actual CIS benchmark content that OpenSCAP evaluates against. It includes SCAP datastream profiles, pre-built Ansible playbooks, and Bash remediation scripts for multiple Linux distributions including RHEL 7/8/9/10, Ubuntu 22.04/24.04, AlmaLinux 9, Amazon Linux 2023, Debian, and Fedora.

Ansible Lockdown Roles

The Ansible Lockdown project provides production-ready Ansible roles that implement CIS benchmark controls with fine-grained variable toggles. Unlike the auto-generated OpenSCAP playbooks, these roles are maintained by the community with extensive testing, sensible defaults, and the ability to selectively enable or disable individual controls via variables. In my experience, these roles are the better choice for production use.

Installing OpenSCAP and the SCAP Security Guide

RHEL 9 / AlmaLinux 9 / Rocky Linux 9

sudo dnf install -y openscap-scanner scap-security-guide

This installs the oscap scanner binary and the CIS benchmark datastreams. After installation, the SCAP content lives at /usr/share/xml/scap/ssg/content/, and pre-built Ansible playbooks are at /usr/share/scap-security-guide/ansible/.

Ubuntu 24.04 LTS

sudo apt update
sudo apt install -y openscap-scanner ssg-base ssg-debderived

One thing to watch out for: on Ubuntu 24.04, the package name changed from the older libopenscap8 to openscap-scanner. The ssg-debderived package provides the Ubuntu-specific CIS datastreams.

Verify the Installation

Confirm that OpenSCAP is installed and list the available CIS profiles for your distribution:

# RHEL 9
oscap info /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml | grep -i cis

# Ubuntu 24.04
oscap info /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml | grep -i cis

You should see profile IDs like xccdf_org.ssgproject.content_profile_cis (Level 2 Server), xccdf_org.ssgproject.content_profile_cis_server_l1 (Level 1 Server), and their workstation counterparts.

Running Your First CIS Baseline Scan

Before applying any hardening, run a baseline scan to measure your current compliance posture. This establishes a starting point and gives you a before-hardening report you can compare against later. Trust me, you'll want this comparison when stakeholders ask "what did the hardening actually do?"

RHEL 9 — CIS Level 2 Server Scan

sudo oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis \
  --fetch-remote-resources \
  --results /tmp/cis-baseline-results.xml \
  --results-arf /tmp/cis-baseline-arf.xml \
  --report /tmp/cis-baseline-report.html \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

Ubuntu 24.04 — CIS Level 1 Server Scan

sudo oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis_level1_server \
  --fetch-remote-resources \
  --results /tmp/cis-baseline-results.xml \
  --results-arf /tmp/cis-baseline-arf.xml \
  --report /tmp/cis-baseline-report.html \
  /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml

Here's what those flags do: --results produces an XCCDF XML result file, --results-arf generates an Asset Reporting Format file suitable for SIEM ingestion, and --report creates a human-readable HTML report. Open the HTML report in a browser to see a detailed breakdown of every rule, its pass/fail status, and remediation guidance.

A freshly installed, unhardened RHEL 9 system typically scores between 40-55% on the CIS Level 2 Server profile. Don't panic — that's normal. After automated remediation, you should reach 90% or higher.

Generating Ansible Playbooks from Scan Results

OpenSCAP can extract Ansible remediation tasks directly from the SCAP content and produce a ready-to-run playbook. This is particularly handy for generating a targeted playbook that addresses only the specific controls your system is failing.

Generate a Playbook from the SCAP Profile

# Generate a full Ansible playbook for the CIS profile
sudo oscap xccdf generate fix \
  --fix-type ansible \
  --profile xccdf_org.ssgproject.content_profile_cis \
  --output /tmp/cis-remediation-playbook.yml \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

Generate a Playbook from Scan Results (Failed Rules Only)

# Generate remediation only for rules that failed during the baseline scan
sudo oscap xccdf generate fix \
  --fix-type ansible \
  --result-id "" \
  --output /tmp/cis-fix-failures.yml \
  /tmp/cis-baseline-results.xml

The second approach is more targeted — it creates remediation tasks only for the controls your specific system is currently failing, rather than the entire profile. This cuts down the playbook size and execution time significantly.

Alternatively: Use Pre-Built Playbooks

The scap-security-guide package ships pre-built Ansible playbooks for each profile:

ls /usr/share/scap-security-guide/ansible/
# rhel9-playbook-cis.yml
# rhel9-playbook-cis_server_l1.yml
# rhel9-playbook-cis_workstation_l1.yml
# rhel9-playbook-cis_workstation_l2.yml

These are ready to use without running a scan first — nice for getting started quickly.

Hardening with Ansible Lockdown Roles

While OpenSCAP-generated playbooks work, the Ansible Lockdown project provides more polished, community-maintained roles with extensive variable-driven customization. The RHEL9-CIS role, for example, implements all 300+ CIS controls with individual toggle variables. That level of granularity is a game-changer when you need to tailor hardening to your specific environment.

Install the Role

ansible-galaxy install ansible-lockdown.rhel9_cis

Create an Inventory and Playbook

# inventory.ini
[cis_targets]
server1.example.com
server2.example.com
192.168.1.50
# harden-cis.yml
---
- name: Apply CIS Level 1 Server hardening
  hosts: cis_targets
  become: true
  roles:
    - role: ansible-lockdown.rhel9_cis
      vars:
        rhel9cis_level_1: true
        rhel9cis_level_2: false

Run the Hardening Playbook

ansible-playbook -i inventory.ini harden-cis.yml

The role is idempotent — running it multiple times produces the same result. The first run applies all hardening changes; subsequent runs verify and correct any drift. This idempotency is what makes it safe to run on a schedule.

Customizing CIS Controls for Your Environment

Here's something that trips people up: CIS benchmarks are guidelines, not rigid mandates. Every environment has specific requirements that may conflict with certain controls. The Ansible Lockdown roles expose variables for each control section, giving you granular customization.

Common Customizations

# group_vars/cis_targets.yml
---
# Skip the time synchronization rule if using a custom NTP setup
rhel9cis_rule_2_1_1: false

# Keep X11 forwarding enabled for specific admin workflows
rhel9cis_rule_5_2_11: false

# Customize the minimum password length (CIS recommends 14)
rhel9cis_pass_min_length: 16

# Set the account lockout threshold
rhel9cis_pam_faillock_deny: 5
rhel9cis_pam_faillock_unlock_time: 900

# Define allowed crypto policies for RHEL 9
rhel9cis_crypto_policy: "FUTURE"

# Whitelist specific SUID/SGID binaries your apps need
rhel9cis_suid_sgid_whitelist:
  - /usr/bin/passwd
  - /usr/bin/sudo
  - /usr/bin/mount

A word of advice: document every customization and the business justification for it. Auditors will ask why specific controls were skipped, and having a documented rationale in your group_vars file makes the audit process significantly smoother. I've seen teams scramble to explain exceptions during audits — don't be that team.

Post-Hardening Verification Scan

After running the Ansible playbook, execute another OpenSCAP scan to measure the improvement and identify any remaining gaps:

sudo oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis \
  --fetch-remote-resources \
  --results /tmp/cis-post-hardening-results.xml \
  --report /tmp/cis-post-hardening-report.html \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml

Compare the before and after HTML reports side by side. The improvement is usually pretty dramatic — from 40-55% on a fresh install to 90-97% after automated remediation. The remaining failures typically come from controls that need environment-specific decisions, like particular partition layouts or organizational password policies that differ from CIS defaults.

Building a Continuous Compliance Pipeline

One-time hardening isn't enough. Configuration drift happens constantly — package updates change defaults, administrators make manual fixes under pressure, and new software installations introduce insecure configurations. A continuous compliance pipeline catches drift before it becomes an audit finding.

Architecture Overview

The pipeline has three stages:

  1. Scheduled Scan — A cron job or systemd timer runs OpenSCAP scans daily or weekly across all managed hosts.
  2. Drift Detection — Results get compared against the expected baseline, and any regressions trigger alerts.
  3. Automated Remediation — Optionally, the Ansible hardening playbook re-runs to correct drift automatically.

Ansible Playbook for Remote Scanning

# scan-compliance.yml
---
- name: Run CIS compliance scan on all hosts
  hosts: cis_targets
  become: true
  vars:
    scan_profile: "xccdf_org.ssgproject.content_profile_cis"
    scan_date: "{{ ansible_date_time.date }}"
    report_dir: "/var/log/compliance"
  tasks:
    - name: Ensure report directory exists
      ansible.builtin.file:
        path: "{{ report_dir }}"
        state: directory
        mode: "0750"

    - name: Run OpenSCAP CIS scan
      ansible.builtin.command:
        cmd: >
          oscap xccdf eval
          --profile {{ scan_profile }}
          --fetch-remote-resources
          --results {{ report_dir }}/cis-results-{{ scan_date }}.xml
          --report {{ report_dir }}/cis-report-{{ scan_date }}.html
          /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
      register: scan_result
      failed_when: scan_result.rc not in [0, 2]
      changed_when: false

    - name: Extract compliance score
      ansible.builtin.shell:
        cmd: >
          oscap xccdf eval --profile {{ scan_profile }}
          /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml 2>&1
          | tail -1
      register: score_output
      changed_when: false

    - name: Fetch report to control node
      ansible.builtin.fetch:
        src: "{{ report_dir }}/cis-report-{{ scan_date }}.html"
        dest: "./reports/{{ inventory_hostname }}/"
        flat: true

Note: oscap returns exit code 2 when some rules fail (which is expected on most systems), so we use failed_when: scan_result.rc not in [0, 2] to prevent Ansible from treating partial compliance as a fatal error.

Scheduling with systemd Timers

# /etc/systemd/system/cis-compliance-scan.service
[Unit]
Description=Weekly CIS Compliance Scan
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis \
  --fetch-remote-resources \
  --results /var/log/compliance/cis-results-weekly.xml \
  --report /var/log/compliance/cis-report-weekly.html \
  /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml
ExecStartPost=/usr/local/bin/notify-compliance-team.sh
# /etc/systemd/system/cis-compliance-scan.timer
[Unit]
Description=Run CIS scan weekly on Sunday at 02:00

[Timer]
OnCalendar=Sun *-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now cis-compliance-scan.timer

Integrating Results with Your SIEM

The ARF (Asset Reporting Format) XML files that OpenSCAP generates can be forwarded to SIEM platforms for centralized compliance monitoring. Several approaches work well here:

  • Wazuh — Has native OpenSCAP integration via the wodle module. Configure it in ossec.conf to run periodic SCAP evaluations and index the results.
  • Elastic Stack — Use Filebeat to ship the XML results to Elasticsearch, then build Kibana dashboards showing compliance trends across your fleet.
  • Splunk — The Splunk Add-on for Linux can ingest OpenSCAP ARF files and provide compliance dashboards out of the box.

For a lightweight notification approach (honestly, this is what most smaller teams end up using), parse the XML results with a script that extracts the pass/fail ratio and sends alerts via Slack, email, or PagerDuty when compliance drops below your threshold:

#!/bin/bash
# notify-compliance-team.sh
RESULTS="/var/log/compliance/cis-results-weekly.xml"
PASS=$(xmllint --xpath "count(//rule-result/result[text()='pass'])" "$RESULTS")
FAIL=$(xmllint --xpath "count(//rule-result/result[text()='fail'])" "$RESULTS")
TOTAL=$((PASS + FAIL))
SCORE=$(echo "scale=1; $PASS * 100 / $TOTAL" | bc)

if (( $(echo "$SCORE < 90" | bc -l) )); then
  curl -X POST "$SLACK_WEBHOOK_URL" \
    -H "Content-Type: application/json" \
    -d "{\"text\": \"CIS Compliance Alert: $(hostname) scored ${SCORE}% (${PASS}/${TOTAL} rules passed). Review report at /var/log/compliance/\"}"
fi

CI/CD Integration for Infrastructure as Code

If you manage server configurations through Git and infrastructure-as-code pipelines, CIS compliance scanning should be a gate in your CI/CD workflow. This ensures that no configuration change can reach production without passing a compliance check — which is exactly the kind of guardrail you want.

GitLab CI Example

# .gitlab-ci.yml
stages:
  - lint
  - harden
  - verify

ansible_lint:
  stage: lint
  script:
    - ansible-lint harden-cis.yml

apply_hardening:
  stage: harden
  script:
    - ansible-playbook -i inventory.ini harden-cis.yml
  only:
    - main

compliance_gate:
  stage: verify
  script:
    - ansible-playbook -i inventory.ini scan-compliance.yml
    - python3 scripts/check_compliance_threshold.py --min-score 90
  artifacts:
    paths:
      - reports/
    expire_in: 90 days

GitHub Actions Example

# .github/workflows/compliance.yml
name: CIS Compliance Check
on:
  push:
    branches: [main]
  schedule:
    - cron: "0 2 * * 0"  # Weekly Sunday 02:00

jobs:
  compliance-scan:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Run CIS hardening
        run: ansible-playbook -i inventory.ini harden-cis.yml
      - name: Run compliance scan
        run: |
          sudo oscap xccdf eval \
            --profile xccdf_org.ssgproject.content_profile_cis \
            --fetch-remote-resources \
            --results results.xml \
            --report report.html \
            /usr/share/xml/scap/ssg/content/ssg-rhel9-ds.xml || true
      - name: Enforce compliance threshold
        run: python3 scripts/check_compliance_threshold.py --min-score 90
      - name: Upload compliance report
        uses: actions/upload-artifact@v4
        with:
          name: cis-compliance-report
          path: report.html
          retention-days: 90

Troubleshooting Common Issues

OpenSCAP Returns Exit Code 2

Don't worry — exit code 2 just means some rules evaluated to "fail," which is completely normal on any system that isn't fully hardened yet. Only exit code 1 indicates an actual scanner error.

Rules Fail After Remediation

Some CIS controls require a reboot to take effect (kernel parameters, partition changes, PAM updates). Run the scan again after rebooting. Also, certain controls may need multiple remediation passes when rules have interdependencies — it's not uncommon.

Package Conflicts on Ubuntu 24.04

If you encounter package conflicts with the older libopenscap8 name, make sure you're using the new openscap-scanner package. Also verify that the ssg-debderived package includes the ssg-ubuntu2404-ds.xml datastream for your distribution version.

Ansible Lockdown Role Breaks Application

Always — and I can't stress this enough — run CIS hardening in a staging environment first. Use --check --diff flags to preview changes before applying them. Disable individual rules that conflict with your application requirements using the role variables documented in the role's README.

Frequently Asked Questions

What is the difference between CIS Level 1 and Level 2?

CIS Level 1 provides a practical baseline of security controls with minimal impact on system functionality — suitable for most production servers. Level 2 adds stricter defense-in-depth controls that may affect system usability or performance, including more aggressive logging, stricter network rules, and additional kernel hardening. Most organizations start with Level 1 and selectively adopt Level 2 controls based on their risk profile.

Can I use OpenSCAP to scan remote hosts over SSH?

Yes, you can. OpenSCAP supports remote scanning via SSH using the oscap-ssh wrapper command. However, for scanning multiple hosts, using Ansible to orchestrate local oscap scans on each target (as shown in this guide) is more scalable and reliable. The Ansible approach also makes it straightforward to collect reports centrally and trigger automated remediation.

How often should I run CIS compliance scans?

At minimum, weekly. Organizations with strict compliance requirements (PCI DSS, HIPAA, FedRAMP) should scan daily. The scans are lightweight — a full CIS evaluation typically completes in 2-5 minutes — so even daily scanning has minimal performance impact. Supplement scheduled scans with event-triggered scans after package updates, configuration changes, or security incidents.

Does CIS hardening work with containers?

CIS publishes separate benchmarks for Docker, Kubernetes, and container runtimes. The host-level CIS Linux benchmarks covered here apply to the container host operating system. For container images themselves, use image scanning tools like Trivy or Grype alongside host hardening. The ComplianceAsCode project is also expanding its container-specific content.

What compliance score should I target?

Aim for 90% or higher on your chosen CIS profile. Reaching 100% is rarely practical because some controls will inevitably conflict with your specific applications or infrastructure requirements. Document every exception with a business justification, implement compensating controls where possible, and make sure your compliance team signs off on the accepted risk.

About the Author Editorial Team

Our team of expert writers and editors.