AnsibleXLIX · Compliance, Validation and CertificatesCompliance, validation and certificates
What check mode actually proves in an audit
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
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.
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.
$ ansible-playbook probes.yml --checkTASK [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.
- 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.
$ ansible-playbook -i inventories/prod compliance.yml --check --diff | tee /var/log/compliance/2026-08-11.logTASK [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:
- 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.
no_log: trueon 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.- Run
--diffonly over the subset of the role with no secret files, using tags. Preserves the diffs that matter. - 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.
assertguards were evaluated, sinceassertsupports 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
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?
Q2. Which tasks are appropriate places for check_mode: false? Select all that apply.
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.
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.