Secret-Handling Mistakes
Encrypting a secret at rest is wasted if a task prints it to the terminal, the CI log, or a registered variable on the next debug. Ansible defaults to showing task arguments and results, so a decrypted vault_db_password can leak through -vvv, a registered variable, a debug of the wrong thing, or a command line that any user on the host can read in the process table.
no_log: true is the switch that stops a task from printing what it is handling. This topic is a catalog of the channels through which a secret leaks after it has been decrypted, and the one line that closes most of them. The encryption in the earlier topics protects the secret at rest; this one protects it during the run.
no_log Suppresses Task Output
no_log: true on a task replaces its arguments and result with censored in all output. It is the one line you add to any task that touches a secret, and once it is there the value never reaches the terminal or the CI log — not in the task banner, not in the result, not even at high verbosity. Add it to the task that sets vault_db_password and the password is invisible no matter how the run is invoked.
- name: Render the app environment file with the DB password ansible.builtin.template: src: app.env.j2 dest: /etc/larkspur/app.env mode: "0600" no_log: true # args + result become "censored" in all output
Verbosity Leaks
-vvv dumps full module arguments for every task, which is exactly what you want when debugging an unrelated failure and exactly what you do not want on a task that passes vault_db_password as an argument. Without no_log, that password prints in plaintext under verbose debugging — and the engineer running -vvv was chasing a different bug entirely and never thought about the secret.
This is why no_log is the only reliable guard rather than "just do not run verbose." no_log overrides verbosity: the task stays censored at -vvv as surely as at the default level. You cannot count on every future run, by every engineer, to avoid the verbose flag — so you censor the task instead and stop worrying about who runs it how.
Registered Variables and debug
The subtler leak is not the secret task — it is the innocent-looking debug two tasks later. Register a command's result into a variable and that variable holds whatever the command returned, secrets included. Then debug: var=result for ordinary troubleshooting dumps the captured value, and the password lands in the log from a task whose job had nothing to do with the secret.
The fix is discipline about what you register and what you debug. Never debug a registered variable that might hold a secret, and if you must inspect one, scope no_log to the surrounding task and print only the non-secret fields. The leak rides in on the captured result, so treat any registered result that touched a secret as itself sensitive.
command Args in the Process Table
Passing a password as a command or shell argument — psql ... --password=s3cr3t — puts it in the process list, where any user on the managed node can read it with ps while the command runs. It also lands in your shell history and in the playbook's own verbose output. The process table is the worst of these, because the exposure is to anyone with a shell on the host, not just to the run's log.
The fix is to never put a secret on a command line. Prefer a module that takes the password as a parameter — community.postgresql.postgresql_db accepts login_password — so the value goes through Ansible's argument channel rather than the process arguments, or pass it through the environment with environment:. Either way the secret stays out of ps, and you pair it with no_log to keep it out of the log too.
The Unencrypted Vault File
The last leak is at rest, and it is permanent. Commit vault.yml before encrypting it, or ansible-vault decrypt it for a quick edit and forget to re-encrypt before committing, and the cleartext lands in git. Even one such commit means the secret is in history forever — a git show on the old commit reveals it long after the current file is encrypted.
Because the exposure is permanent, the fix is rotation, not re-encryption. Encrypting the current file protects future commits; it does nothing about the plaintext already in history. The exposed vault_app_secret_key must be changed at its source so the value in git history is dead. And the way to avoid the whole situation is to use ansible-vault edit, which never writes cleartext to disk, instead of the decrypt-then-encrypt dance that leaves a plaintext file waiting to be committed by accident.
- Omitting
no_log: trueon the task that setsvault_db_password, so running with-vvvfor unrelated debugging prints the database password in plaintext into the CI build log. debug: var=db_resulton a registered result that captured the secret, leaking it through a task whose job had nothing to do with the secret.- Passing a password on a
commandline —mysql -p{{ vault_db_password }}— so it shows inpsto every user on the host and in the playbook's verbose output; use the module's password parameter or an environment variable. - Setting
no_log: trueon a whole block to be safe and then losing all useful output when an unrelated task in the block fails — scopeno_logto the tasks that actually handle the secret, or you debug blind. - Decrypting
vault.ymlto edit it and committing before re-encrypting, puttingvault_app_secret_keyin plaintext in git history where rotating the key is the only fix.
- Add
no_log: trueto every task that reads, sets, or passes a secret, so verbosity anddebugcannot leak it regardless of how the run is invoked. - Pass secrets to modules as parameters or through the environment, never as
commandorshellarguments that land inpsand shell history. - Never
debuga registered variable that may contain a secret; if you must inspect it, scope andno_logthe surrounding task and print only the non-secret fields. - Use
ansible-vault edit, which never leaves cleartext on disk, instead ofdecrypt-then-encrypt, so a plaintextvault.ymlnever exists to be committed by accident. - Treat any secret that ever reached a log or git history as compromised and rotate it, since suppressing future output does nothing about the copy already leaked.
sensitive = true the analog on outputs and variables, with the state file as its leak channel
SOPS · git-crypt prevent the at-rest commit leak
CI secret-masking catches some output leaks but not a command line in the process table
git-filter-repo · BFG what you reach for after a plaintext secret is already in history
Knowledge Check
What does no_log: true suppress, and why does it beat relying on low verbosity?
- It censors a task's arguments and result in all output and overrides verbosity, so the secret stays hidden even at
-vvvno matter who runs it - It encrypts the task's stdout and stderr with the same vault key that protects the vars file, so that only the person holding the passphrase who started the play can read the result back
- It prevents the task from running at all unless the run's verbosity is kept below
-vvv - It strips the secret out of the playbook file on disk once the run has completed
How does a secret leak through a registered variable and a later debug?
- A registered result captures whatever the command returned, secret included, and a later
debug: var=resultdumps it from a task unrelated to the secret - Registering a variable automatically writes its captured value straight into the CI runner's job log in plaintext the instant the task completes, before any later debug task has even run
- The
debugmodule decrypts any vaulted variable as a side effect of printing it to the screen - Registered variables are shared globally across plays, so any later play in the run can print them
Why is a password passed as a command argument exposed on the host?
- It appears in the process table, where any user on the managed node can read it with
pswhile the command runs - The
commandmodule writes the full argument string, secret included, to a world-readable module invocation log left behind under/tmpon the managed node after every run - SSH logs every command argument it forwards to the managed node's system journal by default
- Ansible echoes the command arguments back to the control node over an unencrypted side channel
A plaintext secret was once committed to git, then the file was encrypted. Why is rotation required rather than just re-encryption?
- The cleartext is still in git history and a
git showon the old commit reveals it, so only changing the secret's value at its source ends the exposure - Re-encrypting a file that was previously committed in cleartext corrupts the AES-256 blob's HMAC beyond repair, so the value must be regenerated from scratch before the file can be decrypted at all
- Ansible scans git history and refuses to load any file once committed in plaintext until the secret is rotated
- Rotation is optional here — re-encrypting the current file purges the leaked secret from history automatically
You got correct