The guide · how-to

Encrypt secrets in a playbook

5 min read · 2026-09-02 · config-weave 0.1.0

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.

playbook.wclwcl
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.

console
$ 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.

console
$ 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.

SourceUse
$CONFIG_WEAVE_PASSWORDSet in the environment. Ignored when empty.
--password-stdinRead one line from standard input.
--password-file PATHRead from a file, for a secrets mount.
console
$ 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.

console
$ 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