The guide · how-to
Encrypt secrets in a playbook
Goal
Put a password, token or key into playbook.wcl without committing it in the clear, run check and apply against it, and change the password later. You need a playbook you can edit and a password you can supply through the environment, standard input or a file. There is never a prompt: a run that needs a password and has none fails with exit 2 instead of waiting on a terminal.
1. Write the value as a secret call
Wrap the plaintext in secret() where the value belongs. It is a builtin function, not a block, so it is legal anywhere an expression is: a vars entry, a step's properties, a gather's params or a condition. Its argument must be a plain string literal. A variable or an interpolated string cannot be encrypted in place.
vars {
db_password = secret("hunter2")
}
At this point the playbook does not validate. validate, check, apply and test all refuse to run while a secret() call still holds plaintext, and the error points at the call and names the command that fixes it.
$ config-weave validate ./my-playbook
× this secret has not been encrypted yet
╭─[./my-playbook/playbook.wcl:12:19]
12 │ db_password = secret("hunter2")
· ────────┬────────
· ╰── run `config-weave secrets encrypt` to encrypt it in place
╰────
validation failed with 1 error
2. Encrypt in place
Supply the password and run the encrypt subcommand on the playbook directory. The directory defaults to the current one.
$ export CONFIG_WEAVE_PASSWORD='correct horse battery staple'
$ config-weave secrets encrypt ./my-playbook
./my-playbook/playbook.wcl: encrypted 1 secret(s)
The command rewrites the byte range of each plaintext call with an encrypted blob and re-parses the file before writing it. Comments, indentation and formatting elsewhere are untouched, so the diff is one line per secret. The line now reads db_password = secret("CWENC1.…"), and validate passes.
When you add another secret() later, run the same command with the same password. It decrypts every value already in the file first to prove the password matches, and refuses with a message pointing at secrets rekey if it does not. Running it when every value is already encrypted reports that and verifies the password.
3. Check and apply with the password
Every run that loads the playbook needs the password. Supply it through exactly one of three sources.
| Source | Use |
|---|---|
| $CONFIG_WEAVE_PASSWORD | Set in the environment. Ignored when empty. |
| --password-stdin | Read one line from standard input. |
| --password-file PATH | Read from a file, for a secrets mount. |
$ config-weave check ./my-playbook baseline
$ config-weave apply ./my-playbook baseline --password-stdin < pw.txt
$ config-weave apply ./my-playbook baseline --password-file /run/secrets/pw
Giving more than one source is an error, not a silent choice. A playbook with no secret() calls never asks for a password. The run decrypts every value before executing anything, so a wrong password fails at once with exit 2, even if no step reads the secret. Decrypted values are scrubbed from every report and log line.
4. Edit or re-key later
To change a secret, decrypt the file, edit the plaintext, and encrypt again. To change the password, run rekey with the old password in the usual place and the new one in $CONFIG_WEAVE_NEW_PASSWORD or --new-password-file PATH. The new password cannot come from standard input, because that is where the old one may arrive.
$ config-weave secrets decrypt ./my-playbook
./my-playbook/playbook.wcl: decrypted 1 secret(s) — they are now in the clear on disk
$ config-weave secrets encrypt ./my-playbook
./my-playbook/playbook.wcl: encrypted 1 secret(s)
$ CONFIG_WEAVE_NEW_PASSWORD='a new one' config-weave secrets rekey ./my-playbook
./my-playbook/playbook.wcl: re-encrypted 1 secret(s) under the new password
decrypt writes plaintext to disk
secrets decrypt puts the real values back in the file so you can edit them. Encrypt again before committing.
rekey decrypts every value with the old password, generates a fresh salt, and re-encrypts everything under the new one. Any secret() call still holding plaintext is encrypted in the same pass, and the old password is only requested when there is at least one encrypted value to unlock.
Done
Searching the playbook for the plaintext finds nothing, config-weave validate passes, and check with the password reports the play normally.
Next steps
- How encrypted values fit the variable scheme, and what a script sees: Variables and secrets.
- The three subcommands and their options: config-weave secrets. The password flags are global options, listed in config-weave.
- The run the password unlocks: Check, then apply.