# Automating CIS and STIG hardening with OpenSCAP

> How to evaluate and auto-remediate a Linux image against a CIS Benchmark or DISA STIG using OpenSCAP and the SCAP Security Guide — and where doing it by hand goes wrong.

_24 June 2026 · The Primcoat Team_

Hardening a Linux system to a published baseline — a [CIS Benchmark](https://primcoat.app/glossary/cis-benchmark) or a [DISA STIG](https://primcoat.app/glossary/disa-stig) — is one of those tasks that is conceptually simple and operationally miserable. The benchmark is a few hundred rules. Applying them by hand is a few hundred opportunities to make a typo, skip a control, or set something that quietly breaks the machine three weeks later. The good news is that the whole cycle is automatable, and the tooling is open source.

This is a practical walk through doing it with [OpenSCAP](https://primcoat.app/glossary/openscap) and the SCAP Security Guide.

## The two pieces: a scanner and content

OpenSCAP is the scanner. It does not contain any policy of its own — it is an engine that evaluates a system against a policy expressed in [SCAP](https://primcoat.app/glossary/openscap) format and, optionally, applies fixes.

The policy comes from the **SCAP Security Guide** (SSG), an open-source project that maintains hardening content for most common operating systems. On a Debian or Ubuntu system:

```bash
apt-get install openscap-scanner ssg-debderived
```

On RHEL, AlmaLinux, or Rocky:

```bash
dnf install openscap-scanner scap-security-guide
```

The content lands under `/usr/share/xml/scap/ssg/content/`, one datastream file per OS version — for example `ssg-ubuntu2404-ds.xml` or `ssg-rhel9-ds.xml`. **The content is OS-specific and version-specific**, and this is the first place people go wrong: evaluating an Ubuntu 24.04 host against Ubuntu 22.04 content produces a score, and the score is meaningless.

## Step 1: see what profiles the content offers

A single datastream carries several profiles — CIS Level 1, CIS Level 2, STIG, and others. List them:

```bash
oscap info /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml
```

You are looking for profile IDs like `xccdf_org.ssgproject.content_profile_cis_level1_server` or `xccdf_org.ssgproject.content_profile_stig`.

## Step 2: evaluate, before you change anything

Run a scan and keep both the machine-readable results and a human-readable report:

```bash
oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis_level1_server \
  --results scan-results.xml \
  --report scan-report.html \
  /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml
```

`oscap` exits non-zero if any rule fails, which is worth knowing before you wire it into CI — a passing scan is exit `0`, and anything else is not automatically an error condition you should treat as a broken build. The HTML report is the artifact to actually read: every rule, its result, its rationale, and the remediation.

## Step 3: remediate

There are two honest ways to apply the fixes, and a dishonest one.

The direct route is to let OpenSCAP remediate in place:

```bash
oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_cis_level1_server \
  --remediate \
  --results scan-results.xml \
  /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml
```

The better route, for anything you will build repeatably, is to **generate a remediation role rather than mutating a live host**. OpenSCAP can emit the fixes as a Bash script or an Ansible playbook:

```bash
oscap xccdf generate fix \
  --profile xccdf_org.ssgproject.content_profile_cis_level1_server \
  --fix-type ansible \
  /usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml \
  > remediate.yml
```

Now the hardening is a reviewable, version-controlled artifact you apply during a build, instead of a one-time act performed on a running machine that nobody can reproduce later.

The dishonest route is to remediate, see a rule still failing, and add it to a waiver list without understanding why. Which brings us to the part the tooling cannot do for you.

## The part that is not automatic: exceptions

Some rules will fail remediation, and some *should* fail. A STIG rule requiring a graphical login banner is irrelevant on a headless server. A control mandating a package be absent conflicts with an application that needs it. The benchmark cannot know your environment, so it errs toward strict.

The right response is a **documented exception**: a record of which rule you are not applying, and why. "We accept this finding because the control does not apply to a headless build, signed off by X on Y" is defensible to an auditor. Silently dropping the rule from your scan so the score looks better is exactly the kind of thing an audit exists to catch. Handle exceptions as data you keep, not as edits you make to the content.

## Step 4: re-scan, and gate on the score

After remediation, scan again — against the same profile, into a fresh report — and treat the score as a build gate. If the hardened image scores below your threshold, the build should fail. An image that ships below its compliance bar and reports the shortfall "later" is worse than a build that never completed, because it looks finished.

## Where this gets hard at scale

Everything above works cleanly for one image. The difficulty is that this is not a one-time job:

- The SSG content is updated as benchmarks are revised, so last quarter's remediation is not this quarter's.
- Each OS and version needs its own content and its own profile, so a fleet spanning Ubuntu, RHEL, and Windows is running several of these pipelines, not one.
- The scan, the remediation, the exceptions, and the resulting evidence all have to be kept together, per build, or you cannot answer "what was the compliance state of the image running in production last March?"

That maintenance is the actual work, and it is the part [Primcoat](https://primcoat.app/platform/security) automates: it runs exactly this OpenSCAP cycle — evaluate, remediate against the matching SSG content, re-score, gate — on every build, records the exceptions as structured data, and keeps the compliance report alongside the [SBOM](https://primcoat.app/glossary/sbom) and the [signature](https://primcoat.app/glossary/cosign) for the image that was actually produced. The `oscap` commands above are worth knowing regardless; whether you want to run them on a schedule forever is a separate question.

---

Source: https://primcoat.app/blog/automating-cis-stig-hardening-openscap
Primcoat — a managed VM image factory. https://primcoat.app
