Skip to main content
RunBook Academy

AnsibleXXI · Secrets ManagementSecrets management

no_log, and what it costs you

Advanced⏱ ~24 minansible-core

What you'll learn

  • State what no_log suppresses at task, block and play level, and what it leaves visible
  • Avoid the failure-message route by which a no_log task prints its own secret
  • Reproduce the documented ANSIBLE_DEBUG exception and say when it applies
  • Scope no_log so that a failure remains diagnosable without exposing the credential

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.

Vault covers the repository. no_log covers the run’s output. Between them they close two of the fourteen hops in this part’s leak table, which is why they belong together and why neither one finishes the job.

no_log is a boolean keyword settable on a task, a block or a play:

Read-only / Safethe three scopes
- name: One task
ansible.builtin.uri:
  url: https://api.example.com/v2/deploy
  headers:
    Authorization: "Bearer {{ vault_api_token }}"
no_log: true

- name: A block of related tasks
block:
  - name: Write the credential file
    ansible.builtin.template:
      src: credentials.j2
      dest: /etc/app/credentials
      mode: '0600'
no_log: true

# And at play level - which this lesson argues against.

There is also a configuration setting that turns it on globally:

Read-only / Safethe global switch, off by default
$ ansible-config dump | grep DEFAULT_NO_LOG
DEFAULT_NO_LOG(default) = False

Its documented description is “Toggle Ansible’s display and logging of task details, mainly used to avoid security disclosures.” Setting it globally makes every run undiagnosable, which is why the rest of this lesson is about scoping.

What it does suppress

Read-only / Safethe result is replaced, not redacted
$ ansible-playbook -i localhost, leak2.yml
fatal: [localhost]: FAILED! => {"censored": "the output has been hidden
due to the fact that 'no_log: true' was specified for this result",
"changed": false}

The entire result dictionary is replaced by that one key. Not the secret field — the whole thing.

And it holds under verbosity. Verified on 2.21.3 with a module carrying the credential in its arguments:

Read-only / Safecounting occurrences of the secret across verbosity levels
$ for v in '' '-vvv'; do ansible-playbook -i localhost, nolog4.yml $v | grep -c REPLACE_ME; done
0
0

So no_log covers task arguments and task results, in normal output and in -v through -vvv. That is a real and useful control.

The route it does not cover, measured

Now the part that is not in most treatments of this keyword.

Read-only / Safethree no_log tasks, all failing
$ ansible-playbook -i localhost, leak2.yml | grep -E 'TASK|ERROR|fatal'
TASK [fail module with the secret in the message] ******************
[ERROR]: Task failed: Action failed: could not connect using REPLACE_ME
fatal: [localhost]: FAILED! => {"censored": "the output has been hidden ..."}

TASK [assert whose fail_msg names the secret] *********************
[ERROR]: Task failed: Action failed: bad credential REPLACE_ME
fatal: [localhost]: FAILED! => {"censored": "the output has been hidden ..."}

TASK [assert whose fail_msg does NOT name the secret] *************
[ERROR]: Task failed: Action failed: credential check failed
fatal: [localhost]: FAILED! => {"censored": "the output has been hidden ..."}

The documented ANSIBLE_DEBUG exception, reproduced

The upstream documentation states that no_log does not prevent data being shown when debugging Ansible itself through ANSIBLE_DEBUG. Verified on 2.21.3, and the detail of when it applies is worth having:

Read-only / Safea real module carrying the secret in its arguments
$ ansible-playbook -i localhost, nolog4.yml | grep -c REPLACE_ME
ansible-playbook -i localhost, nolog4.yml -vvv | grep -c REPLACE_ME
ANSIBLE_DEBUG=1 ansible-playbook -i localhost, nolog4.yml | grep -c REPLACE_ME
0
0
1
Read-only / Safeand here is the line
$ ANSIBLE_DEBUG=1 ansible-playbook -i localhost, nolog4.yml | grep REPLACE_ME
4182625 1786484060.79640: done with _execute_module (ansible.builtin.stat,
{'path': '/nonexistent/REPLACE_ME', '_ansible_check_mode': False,
 '_ansible_no_log': True, '_ansible_debug': True, ...

Note '_ansible_no_log': True sitting in the very structure that is being printed. The flag is being carried to the module correctly and the developer-level debug output ignores it, which is what “debugging Ansible itself” means.

What it cannot cover at all

no_log controls what Ansible prints. Three things are outside that entirely:

A task that prints the secret itself. A shell task that echoes a credential, or a script that logs its own arguments, produces output that Ansible faithfully suppresses in its result — while the program has already written it to the target’s stdout, its own log file, or wherever else it chose.

A file the task writes. If the point of the task is to place a credential in /etc/app/credentials, the credential is in /etc/app/credentials afterwards. no_log has no opinion about file modes. Set mode: '0600' and an owner, and take the file-permissions material from the Linux course seriously — this is the hop where most credentials on a managed node actually sit.

The managed node’s own logging. sudo logs commands. auditd logs what it is configured to log. The service you just configured may log its connection string on startup. Ansible cannot reach any of that.

The cost, and how to keep it small

Read-only / Safewhat a failed no_log task tells you
$ ansible-playbook -i localhost, leak2.yml | grep censored
fatal: [localhost]: FAILED! => {"censored": "the output has been hidden
due to the fact that 'no_log: true' was specified for this result",
"changed": false}

A task name and the word censored. No return code, no stderr, no module message, nothing about which host condition caused it.

Which is the argument against play-level no_log, and it is not a theoretical one. A play with no_log: true at play level, failing in production, gives the on-call operator a task name and nothing else. They cannot tell a network failure from a permission failure from a genuinely wrong credential, at three in the morning, on a system they may not have seen before.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task has no_log: true and an assert whose fail_msg interpolates the credential. The assertion fails. What appears in the output?

  2. Q2. Which arrangement keeps a credential censored while leaving a failure diagnosable?

  3. Q3. Which are outside what no_log can control? Select all that apply.

  4. Q4. A module that declares a password parameter sensitive in its argument specification is preferable to task-level no_log, because it censors that parameter while leaving the rest of the result readable.

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