Rotating secrets
Give a secret a new value in every place that uses it. Preview the writes, then watch each deployment pick them up.
Rotation is an admin write. Use a writer master token from your one writer machine, as described in the CLI configuration.
One new value everywhere it lives
dynv rotate AUTH_SECRETBy default, Dynv finds every service's own AUTH_SECRET and every shared group that provides it. One new value replaces all of them, even if their current values differ. Use --service more than once to limit the rotation; prefix a group name with group:.
dynv rotate AUTH_SECRET --service api --service group:authA variable with the same current value under another name is a suggested alias, not an automatic target. Add each alias you want with --also SERVICE:KEY:
dynv rotate AUTH_SECRET --also billing:PROJECT_A_AUTH_SECRETTemplates that use the rotated value follow automatically. Dynv does not rewrite the template; it resolves it with the new value. The plan lists every affected template and every service that will restart.
Here is a real rotation of two own copies, one shared group and one alias. web also has a SESSION_URL template that uses AUTH_SECRET:
$ dynv rotate AUTH_SECRET --also billing:PROJECT_A_AUTH_SECRET --yes
Rotate AUTH_SECRET (one new value everywhere), generated base64:32
places (4):
group auth -> 1 service
api
web
billing PROJECT_A_AUTH_SECRET
note: AUTH_SECRET has 3 different values today; after this, every place shares one
4 services change; 4 restart: api, billing, web, worker
templates that follow: web SESSION_URL
4 writes, plan 4ee9635a5082
job 8Z2QS_ZZ1UxX0Yg1: 4 writes done; AUTH_SECRET rotated
propagated to 4/4 live deploymentsWithout --yes, the CLI shows the same plan and asks for confirmation. The generated value is never printed.
Preview first, apply the same plan
--dry-run prints the plan and writes nothing. It shows the places, aliases, warnings, writes, template effects and restarts before you approve anything.
dynv rotate AUTH_SECRET --dry-runFor scripts and agents, use --dry-run --json. The JSON plan contains no secret values. Its full hash binds the request, the value source (the generator spec, or a keyed commitment to a --stdin value), the version and content of every service and group Dynv read, and everything the preview showed. Apply with --plan HASH and the same rotation flags and value; if anything changed first, Dynv refuses and asks for a new plan.
Each write goes to its object by id and lands only if the object is still exactly at the version and content that was planned. If something else writes to one of them while the rotation runs, the job stops there: nothing more is written, the job is marked failed, and you plan again. A service that needs a re-key (after an emergency revoke) is re-keyed first, and the rotation is planned again on the new state.
dynv rotate AUTH_SECRET --generate base64:32 --dry-run --json > plan.json
hash=$(jq -r .hash plan.json)
# Inspect plan.json, then apply exactly that plan.
dynv rotate AUTH_SECRET --generate base64:32 --plan "$hash" --yesUse the full hash from JSON. The hash in the human-readable plan is shortened for display.
Same everywhere or unique per service
| Mode | What changes |
|---|---|
| Same everywhere (default) | Every selected own copy, group and --also alias gets the same new value. |
--unique | Each service gets a different generated value under the same key name. Aliases and --stdin cannot be used. |
A shared group cannot hold a different value for each service. In unique mode, Dynv first adds an own copy to each service that uses the group, then removes the group's value. No service is left without the key during that conversion.
dynv rotate AUTH_SECRET --unique --generate base64:32Generators
Pass a generator spec with --generate SPEC, or use dynv gen SPEC to generate a value without rotating anything. Values come from your operating system's cryptographic random generator.
| Spec | Output and use |
|---|---|
base64:N | N random bytes as standard base64 with padding. Use base64:32 for Auth.js / NextAuth AUTH_SECRET and NEXTAUTH_SECRET. |
base64url:N | N random bytes as unpadded, URL-safe base64. |
hex:N | N random bytes as 2N lowercase hex characters. |
alnum:L | L letters and digits. |
password:L | L characters. The default classes are upper-case, lower-case and digits. |
password:L,upper,lower,digits,symbols,require=each | Choose character classes and require at least one from each. Add exclude=… to remove characters. |
jwt:hs256, jwt:hs384, jwt:hs512 | Base64url HMAC keys of 32, 48 or 64 random bytes. jwt:hs256 uses 32 bytes, meeting the HS256 minimum in RFC 7518 §3.2. |
hmac:sha256, hmac:sha384, hmac:sha512 | The same key sizes and encoding for other HMAC uses. |
laravel | Laravel APP_KEY: base64: followed by base64 of 32 random bytes. |
django | A 50-character Django SECRET_KEY, using Django's alphabet. |
rails | Rails SECRET_KEY_BASE: 64 random bytes as 128 hex characters. |
uuid | A UUID v4. Useful for identifiers, not as a general-purpose secret. |
ed25519 | dynv gen prints a PKCS#8 private-key PEM and writes its SPKI public-key PEM to stderr. |
Byte lengths are 16–1024; string lengths are 8–1024. password and alnum must reach 128 bits of entropy (length × log2 of the alphabet size: alnum:22, or password:22 with the default classes) from an alphabet of at least 10 characters, so password:8,digits,exclude=012345678 is refused. To accept a weaker value on purpose (a legacy system that only takes 12 digits, say), lower the floor explicitly with min_entropy=N (32 to 1024 bits). Ed25519 is the only keypair the CLI can generate. P-256, P-384 and RSA are not available in the CLI. rotate --generate does not accept keypairs because they produce two variables; supply an existing private key with --stdin instead.
Well-known names have presets: AUTH_SECRET and NEXTAUTH_SECRET use base64:32, APP_KEY uses laravel, SECRET_KEY uses django, SECRET_KEY_BASE uses rails, names containing JWT use jwt:hs256, and names containing PASSWORD use password:32. For any other name, pass --generate or --stdin.
dynv gen refuses to print into a pipe or file unless you explicitly pass --stdout, so a secret does not land in logs by accident.
dynv gen base64:32
dynv gen password:32,upper,lower,digits,symbols,require=each
dynv gen uuid --stdoutRotate an upstream secret with stdin
When Stripe or another provider gives you the new value, pipe it to --stdin. Never put a secret in command arguments: argv can end up in shell history and is visible to other users.
printf %s "$NEW_STRIPE_KEY" | dynv rotate STRIPE_KEY --stdin --yes--stdin supplies one value for every selected place. It requires piped or redirected input and refuses an interactive terminal, where typing would echo the secret. It cannot be combined with --unique or --generate.
Propagation
Finishing the writes and applying them in running deployments are separate steps. By default, the CLI waits up to 30 seconds for deployment reports. Change that with --wait SECONDS; --wait 0 returns immediately after the writes.
| State | Meaning |
|---|---|
applied | The deployment reports the current service and shared-group state. |
pending | The deployment is live but has not reported the new state yet. |
refused | The deployment rejected the newest state it was offered. Its report includes a reason code. |
offline | No recent report. The deployment gets the current state when it next boots or reconnects. |
refused (rollback) means the deployment kept its last good environment. It was offered an older version than one it had already accepted; Dynv did not roll the application back.
Use --json on the rotation for a machine-readable job, plan and propagation report. It contains no generated secret values.
Jobs and status
Each rotation is recorded as a job. A running job locks the objects it touches, so another rotation cannot change them at the same time.
dynv rotate status
dynv rotate status <job-id>
dynv rotate status <job-id> --jsonWith no job id, status lists recent jobs. With an id, it includes every deployment's propagation state. This is the status of the rotation above:
$ dynv rotate status 8Z2QS_ZZ1UxX0Yg1
job 8Z2QS_ZZ1UxX0Yg1: AUTH_SECRET done (generated), 4/4 writes
propagated to 4/4 live deployments
worker docs: applied
api docs: applied
web docs: applied
billing docs: appliedIf a job gets stuck, dynv rotate abort <job-id> marks it aborted and releases its locks. Writes that already landed stay in place; abort does not restore old values. A job that is still running but hasn't moved for a day is flagged by status with this advice. Its client is gone, so another rotation may take its objects over. After an abort or a failed job, run dynv rotate KEY --dry-run: some places may already have the new value, and a new rotation brings them all to one.
The job's state and progress are sealed inside the job record, and each client remembers the newest version of each job it saw. The server can't report a job as done when it isn't, or replay an older version of it; status refuses both.
Rotate versus re-key
Zero-knowledge rotation
Generated values are created on your machine, and every value is encrypted there. The server stores ciphertext and job metadata, including which opaque object ids the job touched and its progress. It never receives plaintext variable names or values. A job record holds no values at all, old or new, encrypted or not, at any point: it only lists the planned writes and their progress. The server replaces each service's encrypted document when it changes and keeps no history. Server backups are different: a backup keeps whatever the database held when it was taken, including the encrypted documents with the old values, until it expires. Plan backup retention with that in mind.