Skip to main content
RunBook Academy

AnsibleXLIX · Compliance, Validation and CertificatesCompliance, validation and certificates

What check mode actually proves in an audit

Expert⏱ ~28 minansible-playbook

What you'll learn

  • State what a green compliance audit does and does not establish
  • Apply check_mode: false correctly to read-only gathering tasks
  • Recognise a compliance result derived from a skipped probe
  • Prevent an audit run from publishing managed secrets through --diff

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Part XXV covered what check mode is and how it behaves. This lesson is narrower and, for compliance, more consequential: what a check-mode audit is entitled to claim.

The claim being made when an audit reports 312 of 312 compliant is a strong one. It goes into a report, it is read by someone who was not present, and it is used to decide that a risk is closed. Four things weaken it, and each one weakens it in a different direction.

1. Module support varies, and the gaps are not obvious

Not every module simulates. The package module documents its check-mode support as depending on the underlying plugin invoked — which is to say it depends on whether the host runs apt, dnf, zypper or something else, and the audit cannot assume it is uniform across a mixed estate.

Modules with no check-mode support report skipping, and skipping in an audit recap is indistinguishable from a when condition that evaluated false. Both are one number in skipped=N.

Read-only / Safefind out what a module supports before relying on it
ansible-doc -t module ansible.builtin.package | sed -n '/ATTRIBUTES/,/EXAMPLES/p'
ansible-doc -t module ansible.builtin.lineinfile | sed -n '/ATTRIBUTES/,/EXAMPLES/p'

The compliance-specific consequence: a control implemented with a module that does not simulate is a control that produces no finding either way. It is not reported as compliant and it is not reported as non-compliant — it is absent, in a count that also contains every deliberately skipped task.

2. A skipped probe registers a result that looks fine

This is the class that produces confidently wrong compliance findings rather than missing ones, and it is worth seeing executed rather than described.

A compliance role usually begins by measuring something. If that measurement uses command, shell or script — reading a vendor tool’s output, a sysctl -n, a getent — then in an audit run it does not execute.

Read-only / Safethe same probe, with and without check_mode: false
$ ansible-playbook probes.yml --check
TASK [Read-only probe that must run even in check mode] ************************
ok: [localhost]

TASK [Probe that honours check mode] *******************************************
skipping: [localhost]

TASK [Report] ******************************************************************
ok: [localhost] => {
  "msg": "probe.stdout=probe-value probe.skipped=False | probe2.skipped=True probe2.rc=0"
}

Read the second half of that message. The skipped probe registered rc: 0 and an empty stdout. It did not raise an undefined-variable error, and it did not produce a value that looks obviously absent.

So a compliance condition written the natural way:

- name: Judge the finding
  ansible.builtin.set_fact:
    finding_compliant: "{{ probe2.rc == 0 }}"

evaluates to true during the audit, for a command that never ran. The host is reported compliant on the strength of a probe that did not execute.

3. check_mode: false on read-only gathering

The fix, and it needs the same care as the problem.

Read-only / Safegathering tasks that must run during an audit
- name: Read the effective kernel parameter
ansible.builtin.command:
  argv: [/sbin/sysctl, '-n', 'net.ipv4.ip_forward']
check_mode: false
changed_when: false
register: ip_forward

- name: Gather installed packages
ansible.builtin.package_facts:
  manager: auto
check_mode: false

- name: Judge the finding against the measured value
ansible.builtin.set_fact:
  ip_forward_compliant: "{{ ip_forward.stdout | trim == '0' }}"

changed_when: false belongs on every one of these. Without it a read-only command reports changed, which inflates the audit change count — the number the compliance report treats as the count of non-compliant hosts.

4. --diff in an audit prints secrets into the evidence

The fourth limit is not about correctness. The audit is right and that is the problem.

--diff shows the content of what would change. Over a compliance role that manages configuration files, that is the content of those files — and audit output is the output most likely to be archived, attached to a ticket, and included in an evidence package that a third party reads.

Read-only / Safean audit run publishing a managed secret
$ ansible-playbook -i inventories/prod compliance.yml --check --diff | tee /var/log/compliance/2026-08-11.log
TASK [Ensure the application configuration matches the baseline] ***************
--- before: /etc/app/app.conf
+++ after: /etc/app/app.conf
@@ -3,7 +3,7 @@
[database]
host = db01.example.com
user = appuser
-password = REDACTED
+password = REDACTED
pool_size = 8
-tls_verify = false
+tls_verify = true

changed: [web01]

Illustrative output

The finding is one line: tls_verify was false and should be true. The disclosure is the whole file, on every non-compliant host, into a log that exists to be read by people who were not in the room.

Mitigations, in order:

  1. Do not put secrets in templated files. A runtime lookup, an environment file the application reads and Ansible does not manage, or a secrets agent. Best because it removes the class.
  2. no_log: true on tasks handling secret material. The task output is suppressed entirely, so the finding for that task is lost too — which is a real trade and is usually the right one.
  3. Run --diff only over the subset of the role with no secret files, using tags. Preserves the diffs that matter.
  4. Treat audit logs as secret-bearing. The fallback, not a mitigation. If the log is already written, restricting who can read it is damage control.

So what does a green audit establish?

Stated precisely, because the value of the exercise depends on the statement being accurate.

It does establish:

  • Every host in the resolved list was reachable and authenticated.
  • For controls implemented with modules that support check mode, and whose measurements ran, the declared state matches the actual state.
  • Templates rendered without undefined variables.
  • assert guards were evaluated, since assert supports check mode fully.

It does not establish:

  • Anything about controls implemented with modules that do not simulate — those are absent from the result, not passing.
  • Anything about a control whose measurement was skipped, which may read as passing.
  • That the state it checked is the state that matters. A file can contain the right line and be overridden by a drop-in, a later Include, or a runtime flag.
  • That the estate is secure. It matches a baseline; whether the baseline is right is a different question, and lesson 1’s “311 of 312” case is what it looks like when it is not.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A compliance role probes a value with a command task, then sets finding_compliant based on probe.rc == 0. The audit run reports every host compliant. What has most likely happened?

  2. Q2. Which tasks are appropriate places for check_mode: false? Select all that apply.

  3. Q3. An audit run with --diff over a role that manages configuration files will print the contents of those files, including any credentials they contain, into whatever the output is captured to.

  4. Q4. What is the cleanest structural fix for a compliance role whose sequential dependencies make its check-mode audit meaningless?

Passing score: 75%. Answers are checked in this browser.