Masking secrets in Ansible output
Added in version 2.22.
Ansible can register values as secrets and mask them wherever Ansible writes output.
Once a value is registered, it is replaced with the placeholder $REDACTED$ on the screen, in the log_path log file, in callback output, and in module logs on the managed node.
The value itself is not changed. Tasks, templates, and modules keep working with the real value, only the output is masked.
This page explains what masking covers, which values Ansible registers automatically, how to register your own secrets in a playbook, and the limits of the feature. If you are writing modules or plugins, see Handling secrets in modules and plugins for the developer API.
Where masking happens
Masking happens at an egress boundary: a place where data leaves Ansible and could be seen or stored by something else. Data that stays inside Ansible, such as variables, module arguments, and task results, is never altered. Ansible masks registered secrets at the following boundaries:
Display output, which covers the screen and the
log_pathlog file.Callback plugins, which receive task results with registered secrets already masked.
Module logging, which covers syslog and the Windows Event Log on the managed node.
Display output includes task banners, debug messages, warnings, deprecation messages, error messages, tracebacks, and verbose (-v) module invocation output.
Everything the Display object writes is masked, both on the screen and in the file set by the log_path configuration setting.
Task results handed to a callback plugin are masked before the plugin sees them.
Values, dictionary keys, and command output lines are all masked, at any depth of the result.
Anything else a callback writes, such as a task name, is masked when it goes through Display, and a callback that writes to a file or an external service is expected to mask that output itself.
See Callback plugins for the details.
Module logging covers messages that a module writes through AnsibleModule.log(), including the module invocation entry.
These are masked on the managed node before they reach syslog or the Event Log.
Because masking is applied at the boundary rather than to the data, a value registered late in a play is still masked in any later output, and the registered value can still be used by its true value in conditionals, templates, and module arguments.
What is registered automatically
Ansible registers the following values as secrets without any action from you:
Vault-encrypted content, once it is decrypted.
Vault passwords supplied by prompt, password file, or password script.
Module options that the module author marked
no_log: truein the module’s argument spec.Prompted values, such as passwords entered for
vars_promptor--ask-pass.Plugin options marked
secret: true, such as connection and become passwords.Generated and decrypted secrets from the
passwordandunvaultlookups and thevaultandunvaultfilters.
How vault-encrypted content is registered depends on how it is loaded:
A vault-encrypted file loaded as variables is parsed and each value in it is registered on its own.
An inline
!vaultvalue is registered as a single string when it is decrypted.The
unvaultlookup registers the entire decrypted content of each file as a single string.The
unvaultfilter registers the decrypted text it returns as a single string, and thevaultfilter registers the plaintext it encrypts as a single string.
A vault-encrypted file is loaded as variables through vars_files, include_vars, host_vars, and similar mechanisms.
Ansible walks into lists and dictionaries and registers each nested string, integer, and float value, with numbers registered in their string form.
Dictionary keys are not registered, only the values.
The difference matters when the decrypted text is structured. With a vaulted vars file, a password inside it is masked wherever that password appears on its own. With the unvault lookup or filter, only the complete decrypted text is registered, including any internal newlines. This means a value inside it is masked only when the whole text appears in output. To mask individual values from an unvault result, parse it and register the values you need with the register_secret filter.
Module authors can declare an option with no_log: true in the module’s argument spec to say that its value is sensitive, for example the password option of the user module.
The value of such an option is registered when the module validates its arguments, including values that came from a default, a fallback, or a sub-option.
This is a setting in the module’s code, not the no_log task keyword that playbook authors set on a task.
See How masking changes no_log for how masking changes the module option, and how it differs from the task keyword.
Prompted values are registered for a vars_prompt with private: true (the default), the pause module with echo: false, and passwords entered for --ask-pass, --ask-become-pass, and --ask-vault-pass.
Plugin authors can mark configuration options with secret: true so the resolved value is registered no matter which source set it.
In ansible-core this includes the ssh, winrm, and psrp connection passwords, the ssh private key and passphrase, the sudo, su, and runas become passwords, and the url lookup password.
The password lookup registers the plaintext password it generates.
The vault and unvault filters register the vault password passed to them. The vault filter also registers the plaintext it encrypts, and the unvault filter registers the plaintext it decrypts, so the data that passes through either filter is masked in output.
Registering your own secrets
Use the ansible.builtin.register_secret filter to register any string value from a playbook. The filter returns the value unchanged so you can use it inline:
- name: Fetch an API token from an external system
ansible.builtin.uri:
url: https://example.com/token
return_content: true
register:
api_token: _task.result.json.token | register_secret
no_log: true # Not required but recommended for defense in depth
- name: The token is still usable but does not appear in output
ansible.builtin.debug:
msg: "Using token {{ api_token }}"
The last task prints Using token $REDACTED$.
The filter accepts a single string. To register every string in a list, apply the filter in a loop or with map:
- name: Register several secrets at once
ansible.builtin.set_fact:
db_passwords: "{{ raw_passwords | map('register_secret') | list }}"
The filter validates its input. A value that is not a string, or that is empty once leading and trailing whitespace is removed, cannot be masked and will raise an error by default. Use the validation_action option to change this; warn displays a warning and ignore stays silent. In both cases the value is returned unchanged and is not registered:
- name: Register an optional token that may be empty
ansible.builtin.set_fact:
api_token: "{{ raw_token | default('') | register_secret(validation_action='ignore') }}"
See How registered secrets are matched for how registered values are matched and Short secrets for the effect of registering a very short value.
Use the ansible.builtin.mask_secrets filter to redact registered secrets from a string yourself. This is useful when you write output somewhere Ansible does not control, such as a file on the managed node or a message sent to an external service:
- name: Write a sanitized copy of a command's output
ansible.builtin.copy:
content: "{{ command_result.stdout | mask_secrets }}"
dest: /var/log/deploy-summary.log
- name: Use a custom placeholder
ansible.builtin.debug:
msg: "{{ 'token=' ~ api_token | mask_secrets(mask_placeholder='***') }}"
How masking changes no_log
The name no_log is used for two different things in Ansible, and secret masking affects them differently:
The
no_logmodule option is set by a module author in the module’s argument spec to mark an option, such as a password, as sensitive. Playbook authors do not set it, they benefit from it automatically when they use the module. Masking changes how this works.The
no_logtask keyword is set by a playbook author on a task or play to hide that task’s entire result from output. Masking does not change this.
Both are covered below.
The no_log module option
Before ansible-core 2.22, a module option declared with no_log: true had its value replaced with the string VALUE_SPECIFIED_IN_NO_LOG_PARAMETER in the module result. This meant that a task could not register the result and use the real value in a later task.
Starting with ansible-core 2.22, the real value is kept in the module result and is registered as a secret instead.
The value is masked in callback output and module logs, but a registered result still holds the true value.
In the following example the password option of the user module is declared with no_log: true by the module, so nothing extra is needed in the task:
- name: Create a user with a generated password
ansible.builtin.user:
name: deploy
password: "{{ generated_hash }}"
register: user_result
- name: The password option is masked in output but is intact in the registered result
ansible.builtin.assert:
that:
- user_result.invocation.module_args.password == generated_hash
Because the option value is now protected by masking rather than by a placeholder, it is masked wherever it appears in later output, not only in the module result. A very short value, such as a single character, is masked inside unrelated words and values as well, and the pattern of placeholders can reveal it. See Short secrets for details. Prefer a longer value where the system accepting it allows, and where that is not possible set the no_log task keyword on the task, as described in the next section.
The no_log task keyword
The no_log task keyword is unchanged by secret masking.
Setting no_log: true on a task or play still hides the entire task result from callbacks and replaces it with a message explaining that the output was censored.
It applies to any task regardless of which module it runs and is unrelated to the module option of the same name.
It remains the right choice when:
A task’s output could contain a secret in a form that masking cannot recognize, such as an encoded or hashed copy of a registered value.
A task hits one of the known limitations of masking, for example a third-party callback plugin that writes data sourced outside the task result to an external service.
You want to be cautious about output that may contain sensitive data even if you cannot say exactly which part is sensitive.
Unlike the module option, the no_log task keyword does not register anything as a secret, it only censors the result of the task it is set on.
Ansible does not know which value inside a task result is the sensitive one, and registering every value in the result would cause common strings such as present to be redacted throughout the rest of the run.
This means a result registered from a task with no_log: true still holds the sensitive value in plain form, and any later task that displays it does so unmasked:
- name: Generate a database password
ansible.builtin.command: openssl rand -base64 32
register: password_result
no_log: true
changed_when: false
- name: The password is visible here because no_log only covered the task above
ansible.builtin.debug:
var: password_result.stdout
The first task is censored, but the debug task prints the generated password in full.
To hide a value from every later task as well, register it as a secret with the register_secret filter.
The simplest way to do this from the task that produces the value is a register projection, the dictionary form of register described in Registering variables.
Because register_secret returns its input unchanged, the projection both stores the value in a variable and marks it as a secret.
You can register the full task result in the same projection, and the sensitive value is masked wherever it appears inside it:
- name: Generate a database password
ansible.builtin.command: openssl rand -base64 32
register:
db_password: _task.result.stdout | register_secret
password_result: _task.result
no_log: true
changed_when: false
- name: The full result can be shown, the password inside it is masked
ansible.builtin.debug:
var: password_result
- name: The value is usable but does not appear in output
ansible.builtin.debug:
msg: "Setting database password to {{ db_password }}"
The second task prints the result with the generated password shown as $REDACTED$ wherever it appears, and the last task prints Setting database password to $REDACTED$.
The db_password variable still holds the real value for use in module arguments and templates.
It is still recommended to keep no_log: true on the task that produces the value to ensure that any failure or edge case scenarios do not inadvertently expose the secret before it is registered.
How registered secrets are matched
Note
These rules describe the current implementation but the implementation itself may change in a future release to improve accuracy or performance. Do not write content that depends on a particular value being masked or left visible.
Masking works by searching output for the registered strings. There is no minimum or maximum secret length and no word boundary rule. A registered value is masked wherever it appears in the output, including where it is part of a longer word or value.
Leading and trailing whitespace is not part of a secret, spaces, tabs, carriage returns, and newlines are stripped from both ends of a value before it is registered. A value that arrives with a trailing newline, such as the content of a vaulted file or a password read from a file, is masked whether or not the newline appears in the output. A value that is empty or only whitespace is ignored.
Only string values can be registered; booleans and None are never registered. Numbers are registered only in the specific cases noted above, such as numeric values in a vault-encrypted file, where they are registered as their string form.
When several registered secrets overlap or sit directly next to each other in the output, the whole run is replaced with a single placeholder. This hides how many secrets the run contained and leaves no part of any secret visible.
The following examples show how these rules apply:
Registered secrets |
Output text |
Masked result |
Why |
|---|---|---|---|
|
|
|
The secret is masked where it appears. |
|
|
|
The secret is masked even though it is part of a longer word. |
|
|
|
The two matches are adjacent, so they merge into one run that is replaced with a single placeholder. |
|
|
|
The two matches overlap on |
|
|
|
The space keeps the two matches apart, so each is replaced with its own placeholder. |
|
|
|
The overlapping repeats of the secret merge into one run. |
|
|
|
The |
|
|
|
Every occurrence of a short secret is masked. |
Short secrets
Because a short value is masked wherever it appears, registering one can redact large parts of unrelated output. For example, a no_log module option set to ab causes every ab in later output to be replaced with $REDACTED$, including inside words such as about and table.
The pattern of placeholders can also reveal the secret. In the banana example above, anyone reading the output can work out that the secret is a. The shorter and more common a value is, the easier it is to guess from the text around each placeholder.
Prefer a longer value where the system accepting it allows to avoid this problem as short values like this are easily guessed from the surrounding text.
Limitations
Warning
Secret masking is best effort. It reduces the chance of a secret appearing in output, but it cannot guarantee that a secret never leaks. Treat it as one layer of defense alongside the no_log task keyword, Ansible Vault, and careful handling of sensitive data, and review logs and callback output before sharing them. The matching algorithm and the rules described on this page may also change in future releases.
Masking is a safety net for output that Ansible controls, not a replacement for handling secrets carefully. Be aware of the following limits:
Only exact matches are masked.
Output that bypasses Ansible is not masked.
Log messages from other Python libraries are not masked.
Persistent connection logging is not masked.
Callback plugins must mask data that is not part of the task result.
Fact and inventory caches are not masked.
Modules only know the secrets in their input.
Secrets are not shared between running workers.
Connection plugins may show raw module output.
A transformed copy of a secret, such as a base64 encoded, URL encoded, hashed, or upper-cased version, is a different string.
It is not masked unless it is registered as well.
The one exception is JSON string escaping.
Because most of the output Ansible produces is JSON, a secret is also masked where it appears in its JSON-escaped form, for example a secret containing a double quote, a backslash, a control character, or a non-ASCII character, both with and without \uXXXX escapes.
You do not need to register the escaped JSON form yourself.
Anything a module writes to a file on the managed node, a plugin prints directly to standard output, or a task sends to an external service does not pass through a masked boundary.
Use the mask_secrets filter, or the developer API described in Handling secrets in modules and plugins, if you need to sanitize such output.
The log_path log file is configured through the standard Python logging module on the root logger, so any Python library that Ansible or a plugin imports and that logs at INFO level or above writes into the same file.
Only the messages that Ansible itself writes through Display are masked.
Messages logged by other libraries, such as an HTTP client that logs request headers, reach the file unmasked.
The persistent_log_messages option used by network connection plugins logs every interaction from the separate ansible-connection process.
That process does not receive the secrets registered by the controller, so passwords and other sensitive configuration sent over the connection are written to the log in plain text.
Only enable this option while debugging and treat the resulting log as sensitive.
A callback plugin receives the task result with registered secrets already masked, but the task and play names, warnings, and any value the callback derives itself are only masked when written through Display.
A callback that writes such data to a file or an external service is responsible for masking it.
Review third-party callbacks that write output outside of Display before relying on them with sensitive data.
Cache plugins write the real values of facts and cached inventory to disk or an external service, since caching is not an output boundary. If a secret can end up in a fact, treat the cache location as sensitive or avoid caching that data.
The controller only sends a module the registered secrets that appear in the module’s own options. A module that reads or derives a sensitive value some other way, such as from a file on the managed node or from an API it calls, does not know that value is a secret and does not mask it in its logs, even if the same value has been registered on the controller. The value is still masked once the result reaches the controller. Module authors can register such values themselves, see Handling secrets in modules and plugins.
Ansible runs tasks in worker processes forked from the controller. A secret registered inside a worker, for example by a lookup or an action plugin, is sent back to the controller process only and is only inherited by workers forked after that point. Workers that are already running for other hosts do not learn about it, so output from those hosts in the same task may still show the value. Later tasks inherit the secret as expected.
A secret that a module registers during its run travels back to the controller inside the module’s raw JSON result, and it is only registered on the controller once that result is processed.
A connection plugin that displays the raw standard output or standard error of the module, for example at high verbosity, shows that JSON before the new secret is known and so shows the value unmasked.
Secrets the module received in its options are already registered and are masked in that output.
The connection plugins shipped with ansible-core only show raw module output when ANSIBLE_DEBUG is enabled.
Third-party connection plugins may not have this protection, so check how a plugin handles raw output before relying on masking with it.
See also
- Using encrypted variables and files
Encrypting sensitive data at rest with Ansible Vault
- How do I keep secret data in my playbook?
Hiding an entire task result with the
no_logtask keyword- Logging Ansible output
Logging Ansible output
- Handling secrets in modules and plugins
The secret masking API for module and plugin developers