---
key: "image-hardening-benchmarks"
title: "Harden the images you ship against CIS, STIG and SCAP benchmarks"
url: "https://softwaresecurity.com/domains/runtime/containers/image-hardening-benchmarks/"
api: "https://softwaresecurity.com/api/v1/articles/image-hardening-benchmarks.json"
domain: "runtime"
topic: "Runtime, cloud \u0026 APIs"
type: "Guide"
identifiers: ["CWE-1188","A02:2025"]
tags: ["Kubernetes and containers","Secure defaults and frameworks","AppSec programs","Compliance and assurance evidence"]
stages: ["build","operate","govern"]
roles: ["developer","appsec-engineer","platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Harden the images you ship against CIS, STIG and SCAP benchmarks

Hold each image your team ships (container base images, golden VM images, Kubernetes node images) to a published benchmark through a pinned SCAP Security Guide profile, scan every build with OpenSCAP, and fail the build on any failure that a deviation register does not explain with an owner, an approver and an expiry.

Canonical URL: https://softwaresecurity.com/domains/runtime/containers/image-hardening-benchmarks/

## In short

Choose a published benchmark for each image you ship: the CIS Benchmark for its distribution, or the DISA STIG if you deliver to the US Department of Defense, as implemented by a pinned SCAP Security Guide (SSG) profile. Scan every build in CI with OpenSCAP, offline and pinned, and fail the build on any rule that fails, errors or comes back unknown unless a deviation register lists it with the control, a reason, a compensating control, an owner, an approver other than the owner and an expiry. Expired entries, and scans of another profile or content version, fail the build too. Fix in the image what you can, register only what you cannot, and check running hosts and nodes for what an image on disk cannot show.

- Benchmark what the team builds and runs: container base images, golden VM images, Kubernetes node images and the container runtime. Laptops and office IT have their own baselines.
- CIS Benchmark PDFs are free for non-commercial use under CC BY-NC-SA 4.0; XCCDF and other formats, CIS-CAT Pro and commercial use need CIS SecureSuite. DISA STIGs are public downloads.
- SSG turns CIS and STIG recommendations into OpenSCAP profiles such as `xccdf_org.ssgproject.content_profile_cis_level1_server`, and each profile's description names the benchmark version it follows.
- SSG profiles trail the benchmarks: in 0.1.82 the Ubuntu 24.04 CIS profile follows v1.0.0, while CIS has published v2.0.0. Record the profile and content version you actually test against.
- `oscap xccdf eval` exits 2 when any rule fails or is unknown, so the gate, not the exit code, decides which failures are accepted.
- A deviation is a decision with an owner and an expiry, not a pass. Tailoring a rule out of the profile hides it; a register keeps it visible in every scan.
- An image on disk has no running processes, kernel settings or mounts: two of the eight Ubuntu failures in our scan came from that, not from the image.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| Benchmark | The CIS Benchmark for the image's distribution, Level 1 Server to start; the DISA STIG where customers require it | Use the SSG profile that implements it and record the version that profile follows. | CIS Benchmarks FAQ; SCAP Security Guide 0.1.82 |
| Scanner | OpenSCAP and SSG in a pinned container with `--network=none` | A new SSG release adds and changes rules, so update it deliberately, together with the register. | OpenSCAP 1.4.4 manual page |
| Container images | Scan the exported filesystem with `oscap-chroot`, or use `oscap-podman` as root on Fedora and RHEL | An exported container filesystem contains `/.dockerenv`, so SSG treats it as a container and skips machine-only rules. | ComplianceAsCode developer guide; OpenSCAP manual |
| Scanner exit status | 0: every rule passed; 1: the scan failed; 2: at least one rule failed or was unknown | OpenSCAP 1.4.4's source also returns 2 for `error`. Treat 1 as a failed build and let the gate judge 2. | OpenSCAP 1.4.4 manual page and xccdf_session.c |
| Gate | Fail on unregistered `fail`, `error` and `unknown` results, expired entries, a different profile, benchmark or content version, and an empty scan | List `notchecked` results for manual review; do not count them as passes. | — |
| Register entry | Rule ID, control ID, reason, compensating control, owner, approver (not the owner), approval date and expiry | The project caps an entry at 180 days. | NIST SP 800-70 Rev. 5, sections 2.4.2 and 2.4.3 |
| Kubernetes nodes | The provider's CIS benchmark with kube-bench, or Bottlerocket's `apiclient report cis` | kube-bench 0.16.0's newest generic configuration is CIS Kubernetes Benchmark 1.12; it has none for 2.x. | kube-bench v0.16.0 cfg/config.yaml; Bottlerocket docs |
| Container runtime | CIS Docker Benchmark v1.9.0 | docker-bench-security implements v1.6.0, so treat it as a starting point. | CIS Docker Benchmark page; docker-bench-security README |

## Verify it

1. Run the scan twice on the same image digest and confirm the same counts of each result.
2. Remove one entry from the register and confirm CI fails, naming that rule.
3. Set one entry's expiry to yesterday and confirm CI fails on it.
4. Run the gate against a scan made with another profile or SSG version and confirm it fails.
5. Change one result in a copy of the results to `error` and confirm the gate fails.
6. Confirm that every `notchecked` rule has a recorded manual review.

## What you will learn

- Choose a benchmark and profile for each kind of image you ship, and know the CIS, DISA and SSG access terms.
- Scan a container image offline with OpenSCAP and read pass, fail, notapplicable and notchecked results.
- Keep a deviation register with an owner, an independent approver and an expiry, and fail CI on anything unregistered or expired.
- Recognize the rules an image scan cannot judge, and check running hosts and nodes for them.

Audience: Platform and DevOps engineers who build base images, golden VM images or Kubernetes node images, and AppSec engineers who own the exceptions process.

Before you start:

- Build container images with Docker or BuildKit, or VM images with a tool such as Packer.
- Know which packages and users your images contain and what they run as.
- For the project: Python 3.11 or later, and Docker for live scans.

Time: About 45 minutes: 15 to read and 30 for the exercise

## Which images need a benchmark

A hardening benchmark is a published list of configuration settings for one product, such as an operating system release or a container runtime. The images a software team builds are where its settings are decided once and then copied to every running instance, so they are where to check them. Hardening employees' laptops and office servers is enterprise IT and is not covered here.

Use one benchmark per image family, not one per team. A container image is not a whole host, so many host rules do not apply: there is no bootloader, kernel or SSH daemon to configure. SSG marks such rules for machines only, or for systems with a kernel package, and reports them as `notapplicable` in a container scan.

| What you ship | Benchmark | How to check it |
| --- | --- | --- |
| Container base and application images | The CIS Benchmark or DISA STIG for the image's distribution, through an SSG profile | OpenSCAP on the exported filesystem in CI, as in the project below, or `oscap-podman` on Fedora and RHEL |
| Golden VM images (AMIs, Azure and Google Cloud images) | The CIS Benchmark for the distribution, Level 1 or 2, or the STIG | OpenSCAP inside the build VM during the image build, then again on a booted instance |
| Kubernetes node images | The node OS benchmark plus the provider's CIS benchmark for EKS, AKS or GKE | kube-bench as a Job, Bottlerocket's `apiclient report cis`, or Container-Optimized OS's CIS scanner |
| Container runtime on hosts you run | CIS Docker Benchmark v1.9.0; the worker node sections of the Kubernetes benchmark | docker-bench-security as a starting point, kube-bench for nodes |

## CIS, DISA STIG and SCAP Security Guide: access and versions

CIS Benchmark PDFs are free to download for non-commercial use after registering, under the Creative Commons BY-NC-SA 4.0 license. CIS's FAQ says the additional formats (XCCDF, Word and others) are for CIS SecureSuite members, who also get CIS-CAT Pro and the build kits; CIS sells separate membership categories for consultants and for vendors who integrate the benchmarks into products. The free CIS-CAT Lite assesses only Google Chrome, Windows 10 and 11 and Ubuntu Linux 20.04, so for images you will usually scan with SSG instead. CIS's Level 1 profile is a base recommendation designed not to have an extensive performance impact; Level 2 is defense in depth and can have an adverse effect if applied without due care.

DISA publishes Security Technical Implementation Guides at cyber.mil without a login, as XCCDF 1.1 manual STIGs and, for some products, SCAP benchmarks. Rules carry a vulnerability ID such as `V-257777`, a rule ID and a STIG ID such as `RHEL-09-211010`, and a severity of CAT I, II or III. Current releases include the RHEL 9 STIG V2R10, the Canonical Ubuntu 24.04 LTS STIG V1R7, the Container Platform SRG V2R5 and the Kubernetes STIG V2R6; the Docker Enterprise 2.x STIG was sunset in July 2024. DISA's Container Hardening Process Guide notes that host profiles produce false positives in containers and allows marking items not applicable.

The SCAP Security Guide, from the ComplianceAsCode project, implements these benchmarks as OpenSCAP data streams. Each product has a data stream named like `ssg-ubuntu2404-ds.xml`, and each profile ID starts with `xccdf_org.ssgproject.content_profile_`. The IDs are stable, but the benchmark version behind them lags, as the table shows for SSG 0.1.82 (1 September 2026).

| Product | CIS and STIG profiles | Follows | Latest published |
| --- | --- | --- | --- |
| Ubuntu 24.04 | `cis_level1_server`, `cis_level2_server`, `cis_level1_workstation`, `cis_level2_workstation`, `stig` | CIS v1.0.0; STIG V1R5 | CIS v2.0.0; STIG V1R7 |
| RHEL 9 | `cis_server_l1`, `cis` (Level 2 Server), `cis_workstation_l1`, `cis_workstation_l2`, `stig`, `stig_gui` | CIS v2.0.0; STIG V2R9 | CIS v3.0.0; STIG V2R10 |
| Debian 13 | `cis_level1_server`, `cis_level2_server`, `cis_level1_workstation`, `cis_level2_workstation` | CIS v1.0.0 | CIS v1.1.0 |
| Amazon Linux 2023 | `cis_server_l1`, `cis` (Level 2 Server) | CIS v1.0.0 | CIS v1.0.0 |

## Scan an image offline in CI

OpenSCAP can evaluate a filesystem that is not the running system: `oscap-chroot` sets `OSCAP_PROBE_ROOT` to the directory and runs `oscap xccdf eval` against it. The script below creates a container from the image without starting it, exports its filesystem into a scanner container with no network, and gets the ARF results and HTML report back on stdout, so nothing from the host is mounted. The export contains `/.dockerenv`, one of the markers SSG uses to recognize a container, and most host-only rules are skipped anyway because the image has no kernel package. Pin the image to scan by digest, so the results name exactly what was scanned.

On Fedora and RHEL, `oscap-podman <image> xccdf eval ...` does the same with a Podman image; it must run as root and cannot remediate. The scanner here is Fedora 44 with OpenSCAP 1.4.4 and SSG 0.1.82 pinned by version, and the scan of `ubuntu:24.04` takes a few seconds. The Debian 13 data stream also references Debian's remote OVAL patch definitions, which a scanner without network skips; check patches with your vulnerability scanner instead.

```bash
#!/bin/sh
# Scan a container image with OpenSCAP and the SCAP Security Guide, with no network.
# Usage: ./scan.sh IMAGE@sha256:DIGEST PRODUCT PROFILE [OUTDIR]
#   ./scan.sh ubuntu:24.04@sha256:534b... ubuntu2404 cis_level1_server results
# Writes OUTDIR/arf.xml (ARF results) and OUTDIR/report.html. Exits non-zero only when the
# scan itself fails; failing rules are for the gate to judge.
set -eu
if [ $# -lt 3 ]; then
  echo "usage: $0 IMAGE@sha256:DIGEST PRODUCT PROFILE [OUTDIR]" >&2
  exit 64
fi
image=$1 product=$2 profile=$3 out=${4:-results}
scanner=${SCANNER_IMAGE:-image-scanner}
case $image in
  *@sha256:*|sha256:*) ;;  # a registry digest, or the ID of a local build
  *) echo "scan.sh: pin the image by digest so the results name exactly what was scanned" >&2
     exit 64 ;;
esac
mkdir -p "$out"
rm -f "$out/arf.xml" "$out/report.html"
container=$(docker create "$image" /bin/true)  # created to export its filesystem, never started
trap 'docker rm "$container" >/dev/null' EXIT
# The image's files go in on stdin and the results come out on stdout: no bind mounts. The
# exported filesystem has /.dockerenv, so SSG skips rules that apply only to machines.
docker export "$container" | docker run --rm -i --network=none "$scanner" sh -c '
  set -eu
  mkdir /target /results
  tar -x -C /target
  status=0
  oscap-chroot /target xccdf eval \
    --profile "xccdf_org.ssgproject.content_profile_$2" \
    --results-arf /results/arf.xml --report /results/report.html \
    "/usr/share/xml/scap/ssg/content/ssg-$1-ds.xml" >&2 || status=$?
  # 0: every rule passed; 2: a rule failed, errored or was unknown; anything else: no scan.
  if [ "$status" -ne 0 ] && [ "$status" -ne 2 ]; then exit "$status"; fi
  tar -c -C /results .' sh "$product" "$profile" | tar -x -C "$out"
test -s "$out/arf.xml"
echo "scan.sh: $image -> $out/arf.xml" >&2

```

## Read the results before you gate on them

Scanning `ubuntu:24.04` against the CIS Level 1 Server profile on 11 October 2026 gave 128 pass, 8 fail, 2 notchecked, 270 notapplicable and 240 notselected results. Every failure needs a decision: fix it in the image, or record a deviation. Four were easy to fix in a Dockerfile; two, the `root` PATH rules, are artifacts of scanning an image on disk, because their check reads the environment of running processes (an OVAL `environmentvariable58` test) and an image has none.

The two `notchecked` results say SSG 0.1.82 has no automated check for these rules on Ubuntu 24.04. They are not passes: check world-writable files and files without a group by hand or with your own test, and record that review.

- `accounts_umask_etc_bashrc` and `accounts_umask_etc_profile` (CIS 5.4.3.3): no `umask 027` in `/etc/bash.bashrc` or `/etc/profile`. Fix: append `umask 027` to both.
- `permissions_local_var_log` (6.1.4.1): package logs in `/var/log` are readable by others. Fix: remove other and group-write access.
- `file_permission_user_init_files` (7.2.10): the image's default `ubuntu` user (UID 1000) has mode 0644 `.bashrc`, `.profile` and `.bash_logout`. Fix: `userdel --remove ubuntu`; workloads run as their own UID.
- `package_pam_pwquality_installed` (5.3.1.3): no password quality module, but no account has a password and no login service runs. Deviation, with non-root workloads and no SSH as the compensating controls.
- `use_pam_wheel_group_for_su` (5.2.7): `su` from util-linux is not restricted to a group. Deviation: workloads run with `no-new-privileges`, so the setuid `su` cannot raise privileges.
- `root_path_no_dot` and `root_path_all_dirs` (5.4.2.5): the check reads running processes' PATH, and an image on disk has no processes. Deviation: the image config's PATH has only absolute directories.

## Keep a deviation register

NIST SP 800-70 Rev. 5 (May 2026) says any deviation from checklist settings should be reviewed for risk and accompanied by a documented justification with risk acceptance, and that organizations should customize checklists with specific deviations, documented justifications, risk acceptance and compensating controls. A register in the repository, next to the image it describes, makes that reviewable in a pull request. Each entry names the exact SSG rule ID, the benchmark control, why the setting is not applied, the compensating control, an owner, an approver who is not the owner, the approval date and an expiry, and the register as a whole names the benchmark, profile and SSG content version it was reviewed against.

Prefer a register to tailoring. An XCCDF tailoring file, which `autotailor` generates, can unselect a rule so it never runs, and then the scan reports it as `notselected` and nobody sees it again. A register keeps the rule in every scan and the decision in front of reviewers until it expires. Approval follows your exceptions process: see /frameworks/risk-exceptions-and-metrics/ for who may accept the risk and what happens at expiry.

```yaml
# Deviation register for ubuntu:24.04 scanned against the SCAP Security Guide profile for the
# CIS Ubuntu Linux 24.04 LTS Benchmark v1.0.0, Level 1 - Server (scan-ubuntu2404-cis-l1.xml).
# One entry per failing rule the team accepts for now. The gate fails the build on a failing
# rule without an entry, on an expired entry and on a scan of any other profile or content.
benchmark: xccdf_org.ssgproject.content_benchmark_UBUNTU_24-04
profile: xccdf_org.ssgproject.content_profile_cis_level1_server
content_version: "0.1.82"
deviations:
  - rule: xccdf_org.ssgproject.content_rule_package_pam_pwquality_installed
    control: CIS Ubuntu 24.04 v1.0.0 5.3.1.3
    reason: >-
      The image has no account with a password and no login service, so no password is
      ever set or changed and pam_pwquality would never run.
    compensating_control: >-
      Containers run as UID 10001 with no SSH daemon; the admission policy rejects pods
      that run as root.
    owner: platform-images
    approved_by: appsec-lead
    approved_on: 2026-10-11
    expires: 2027-01-31
  - rule: xccdf_org.ssgproject.content_rule_root_path_no_dot
    control: CIS Ubuntu 24.04 v1.0.0 5.4.2.5
    reason: >-
      False positive of an offline scan: the check reads the environment of running root
      processes (environmentvariable58), and an image filesystem has none.
    compensating_control: >-
      The PATH comes from the image config, /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,
      which has only absolute entries; checked with docker image inspect.
    owner: platform-images
    approved_by: appsec-lead
    approved_on: 2026-10-11
    expires: 2027-01-31

```

## Enforce the register in CI

The gate reads the XCCDF 1.2 `TestResult` from the ARF report and compares it with the register. It fails the build on a failing, erroring or unknown rule without a valid entry, on an expired entry, on a register entry that is incomplete, self-approved or longer than 180 days, and on a scan of another benchmark, profile or SSG version. It matches entries by exact rule ID: matching by substring would let an entry for `accounts_umask` waive every umask rule. Rules that no longer fail are reported, so their entries get removed.

On the scan of `ubuntu:24.04` with the project's four register entries, the gate fails on the four unregistered rules. The project's example image fixes the other four in one `RUN` step, and its scan has 132 passes, the 4 registered failures and a passing gate. The project's starter gate made the mistakes that the tests catch: it waived `error` and `unknown`, ignored expiry and the scan's profile, and matched by substring.

```python
# gate.py (excerpt): decide the build from the scan and the register
import datetime

XCCDF = "{http://checklists.nist.gov/xccdf/1.2}"
# A rule that errored or came back unknown has not shown compliance either.
FAILING = {"fail", "error", "unknown"}
REVIEW = {"notchecked"}
REQUIRED = ("rule", "control", "reason", "compensating_control", "owner", "approved_by",
            "approved_on", "expires")
MAX_DAYS = 180


def as_date(value):
    if isinstance(value, datetime.date):
        return value
    return datetime.date.fromisoformat(str(value))


def check_register(register):
    """Problems with the register itself, whatever the scan says, and the usable entries."""
    problems, usable = [], {}
    seen = set()
    for i, entry in enumerate(register.get("deviations") or []):
        where = f"deviations[{i}] ({entry.get('rule', '?')})"
        missing = [field for field in REQUIRED if not str(entry.get(field) or "").strip()]
        if missing:
            problems.append(f"{where}: missing {', '.join(missing)}")
            continue
        if entry["rule"] in seen:
            problems.append(f"{where}: duplicate entry")
        seen.add(entry["rule"])
        if entry["approved_by"] == entry["owner"]:
            problems.append(f"{where}: approved by its own owner")
        try:
            approved, expires = as_date(entry["approved_on"]), as_date(entry["expires"])
        except ValueError as error:
            problems.append(f"{where}: {error}")
            continue
        if not 0 < (expires - approved).days <= MAX_DAYS:
            problems.append(f"{where}: expiry must fall within {MAX_DAYS} days of approval")
            continue
        usable[entry["rule"]] = entry
    return problems, usable


def evaluate(scan, register, today):
    """Returns (failures, waived, review, stale): lists of messages."""
    failures, entries = check_register(register)
    for key in ("benchmark", "profile", "content_version"):
        if str(scan[key]) != str(register.get(key)):
            failures.append(f"scan {key} is {scan[key]!r}; the register was reviewed for "
                            f"{register.get(key)!r}")
    if not any(result != "notselected" for result in scan["results"].values()):
        failures.append("the scan evaluated no rules")
    waived, review = [], []
    for rule, result in sorted(scan["results"].items()):
        if result in REVIEW:
            review.append(f"{rule}: {result}, review by hand")
        if result not in FAILING:
            continue
        entry = entries.get(rule)  # the exact rule ID: no prefixes, no substrings
        if entry is None:
            failures.append(f"{rule}: {result}, not in the register")
        elif as_date(entry["expires"]) < today:
            failures.append(f"{rule}: {result}, deviation expired on {entry['expires']}")
        else:
            waived.append(f"{rule}: {result}, deviation {entry['control']} until "
                          f"{entry['expires']} (owner {entry['owner']})")
    stale = [f"{rule}: no longer failing; remove the deviation"
             for rule in entries if scan["results"].get(rule) not in FAILING]
    return failures, waived, review, stale

```

## An image is not the whole host

A scan of an image on disk cannot see services that run, kernel parameters, mounts, audit rules or the environment of running processes. For golden VM images, run OpenSCAP inside the build VM after provisioning, keep the ARF report with the image, and scan an instance booted from it. SSG ships remediation for each profile, such as an Ansible playbook and a Bash script, and `oscap xccdf generate fix --fix-type ansible --result-id <id>` writes fixes for just the rules that failed in a results file; review generated fixes like any other code before an image build runs them.

For Kubernetes nodes, kube-bench runs the node and control-plane checks as a Job; version 0.16.0 has configurations up to CIS Kubernetes Benchmark 1.12 (Kubernetes 1.32 to 1.34) plus provider benchmarks, and none yet for 2.x. Bottlerocket's default settings are compliant with level 1 of the CIS Bottlerocket Benchmark, and `apiclient report cis` and `apiclient report cis-k8s` report on a node; CIS has said it plans to archive the Bottlerocket benchmark unless more community support appears. On GKE, Container-Optimized OS images from milestone 97 comply with CIS Level 1 by default. Managed control planes are the provider's to configure; /domains/runtime/containers/managed-kubernetes-hardening/ covers that split.

## Pitfalls

Most benchmark programs fail quietly rather than loudly.

- Gating on the scanner's exit code alone: exit status 2 covers every failure, accepted or not, so teams either ignore it or switch the scan off.
- Counting `error`, `unknown` or `notchecked` as passes.
- Waiving rules by tailoring them out, so they vanish from every later report.
- Deviations without an expiry, or approved by the person who asked for them.
- Upgrading SSG without reviewing the new results: new content adds rules and changes what existing rules check.
- Calling an image compliant because the benchmark's machine-only rules came back `notapplicable`; they still apply to the host or node that runs it.
- Copying CIS benchmark text into a commercial product or a customer deliverable: the free PDFs are licensed for non-commercial use only.

## Guided practice

Your platform team ships `base-ubuntu` built on `ubuntu:24.04`. Its register was reviewed against SSG 0.1.82 and the `cis_level1_server` profile. Decide what the gate does on each day and change, and why. Today is 2026-10-11 unless stated.

### Supplied material

```text
Register: package_pam_pwquality_installed (expires 2027-01-31), use_pam_wheel_group_for_su (expires 2026-10-10), root_path_no_dot and root_path_all_dirs (expire 2027-01-31), each approved by appsec-lead and owned by platform-images.
Scan A (SSG 0.1.82, cis_level1_server): fail for the four registered rules; notchecked for file_permissions_unauthorized_world_writable.
Scan B (SSG 0.1.83, cis_level1_server): the same four failures plus one new rule that fails.
Scan C (SSG 0.1.82, cis_level1_server): accounts_umask_etc_bashrc returns error; everything else as in A.
Change D: a pull request adds an entry for accounts_umask owned and approved by platform-images, expiring 2027-12-31.
```

### 1. Gate scan A

What does the gate report for scan A today?

Worked answer: FAIL: the use_pam_wheel_group_for_su entry expired on 2026-10-10. The other three failures are waived, and the notchecked rule is listed for manual review.

### 2. Gate scan B

What happens when the scanner image moves to SSG 0.1.83?

Worked answer: FAIL before any rule is judged: the scan's content version does not match the register's 0.1.82. Review the new rule and every existing entry against the new content, then update content_version in the same change as the scanner image.

### 3. Gate scan C

accounts_umask_etc_bashrc returns error. Is the image compliant with that rule?

Worked answer: Unknown, so the gate fails: an error is not evidence of compliance. Find out why the check errored, then fix the image or the scan.

### 4. Review change D

What is wrong with the pull request?

Worked answer: Three things: accounts_umask is not a rule ID (exact matching means it waives nothing, and substring matching would waive every umask rule), the owner approved their own entry, and the expiry is more than 180 days after approval. The rule has a two-line fix in the image instead.

Deliverable: A gate result for scans A to C and review comments for change D, each naming the register field or scan property that decides it.

Scope: Paper exercise. The image-benchmark-gate project runs the same checks against a real OpenSCAP scan of ubuntu:24.04.

## Executable practice

- [Gate an image build on a CIS scan and a deviation register: project ZIP](https://softwaresecurity.com/downloads/projects/image-benchmark-gate-b76ad8a05569f566.zip) — OpenSCAP 1.4.4 and SSG 0.1.82 in Docker

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [CIS Benchmarks FAQ](https://www.cisecurity.org/cis-benchmarks/cis-benchmarks-faq)
- [CIS: terms and conditions, Terms of Use for Non-Members (CIS Benchmarks free PDF use under CC BY-NC-SA 4.0)](https://www.cisecurity.org/terms-and-conditions-table-of-contents)
- [CIS SecureSuite: membership categories](https://www.cisecurity.org/cis-securesuite/pricing-and-categories)
- [CIS-CAT Assessor coverage guide: CIS-CAT Lite coverage](https://ciscat-assessor.docs.cisecurity.org/en/latest/Coverage%20Guide/)
- [CIS Ubuntu Linux Benchmarks](https://www.cisecurity.org/benchmark/ubuntu_linux). Ubuntu Linux 24.04 LTS v2.0.0.
- [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker). v1.9.0.
- [CIS: Benchmarks September 2026 update (Bottlerocket archive notice)](https://www.cisecurity.org/insights/blog/cis-benchmarks-september-2026-update)
- [DISA: Security Technical Implementation Guides downloads](https://www.cyber.mil/stigs/downloads/)
- [DISA: Container Hardening Process Guide](https://dl.dod.cyber.mil/wp-content/uploads/devsecops/pdf/Final_DevSecOps_Enterprise_Container_Hardening_Guide_1.2.pdf). Version 1, Release 2 (24 August 2022).
- [ComplianceAsCode: SCAP Security Guide 0.1.82 release](https://github.com/ComplianceAsCode/content/releases/tag/v0.1.82). 0.1.82 (1 September 2026).
- [ComplianceAsCode: Ubuntu 24.04 CIS Level 1 Server profile](https://github.com/ComplianceAsCode/content/blob/v0.1.82/products/ubuntu2404/profiles/cis_level1_server.profile). 0.1.82.
- [ComplianceAsCode developer guide: platforms and the machine CPE](https://github.com/ComplianceAsCode/content/blob/v0.1.82/docs/manual/developer/06_contributing_with_content.md). 0.1.82.
- [OpenSCAP: oscap(8) manual page](https://github.com/OpenSCAP/openscap/blob/1.4.4/utils/oscap.8). 1.4.4.
- [OpenSCAP user manual: container and offline scanning](https://static.open-scap.org/openscap-1.3/oscap_user_manual.html). 1.3.
- [NIST SP 800-70 Rev. 5: National Checklist Program for IT Products](https://csrc.nist.gov/pubs/sp/800/70/r5/final). Rev. 5 (May 2026).
- [NIST IR 7275 Rev. 4: XCCDF 1.2 specification](https://csrc.nist.gov/pubs/ir/7275/r4/upd1/final). Rev. 4 (March 2012 update).
- [kube-bench v0.16.0: benchmark versions by platform (cfg/config.yaml)](https://github.com/aquasecurity/kube-bench/blob/v0.16.0/cfg/config.yaml)
- [Bottlerocket: CIS benchmark reports](https://bottlerocket.dev/en/os/1.65.x/api/reporting/cis/). Bottlerocket 1.65.x.
- [Google Kubernetes Engine: CIS benchmarks](https://docs.cloud.google.com/kubernetes-engine/docs/concepts/cis-benchmarks)
- [Container-Optimized OS: CIS compliance](https://docs.cloud.google.com/container-optimized-os/docs/how-to/cis-compliance)
- [docker-bench-security](https://github.com/docker/docker-bench-security). v1.6.1.