Rotate secrets on a schedule
Make credential rotation a task a timer can call: mint a new value, activate it, verify, and write an audit record. Keep the secret out of the workflow with a resolver, and let cron own the schedule.
blog/credential-rotationSecret rotation is the security task everyone agrees on and nobody automates. It sits in a runbook titled "rotate the signing key (quarterly)", the quarter passes, and the key is two years old when an audit finds it. The work isn't hard — mint, activate, verify, record — it just needs to be a thing a timer can run unattended.
So make it one task with a clean exit code, keep the actual secret out of the file, and let cron own the calendar.
The rotation as a task
Each phase is a step. The new value is minted, activated with retry because key stores rate-limit, verified, and then an audit line is written so there's a record of what changed and when.
name: rotation
defaults:
key_name: api-signing-key
store: vault
stage: production
tasks:
# orchstep run rotate --output json
rotate:
desc: "Rotate one credential and record an audit entry"
steps:
- name: mint
func: shell
do: echo "rotated-{{ vars.key_name }}"
outputs:
version: "{{ result.output }}"
- name: activate
func: shell
do: echo "activating {{ steps.mint.version }} in {{ vars.store }} for {{ vars.stage }}"
retry:
max_attempts: 3
interval: "2s"
backoff_rate: 2.0
- name: verify
func: assert
args:
condition: '{{ ne steps.mint.version "" }}'
message: "a new version must be minted before activation"
- name: audit
func: shell
do: echo "AUDIT key={{ vars.key_name }} version={{ steps.mint.version }} store={{ vars.store }} stage={{ vars.stage }}"
- name: retire
func: shell
do: echo "scheduling the previous {{ vars.key_name }} for revocation"The mint step captures its result as version and every later step reads that minted value back through steps.mint.version — so the activate, the audit, and the retire all talk about the same new value. The verify assert makes "we never audited a rotation that didn't actually mint" a property the workflow enforces, not a thing you hope is true.
Keep the secret out of the file
The value you're rotating should never be a literal in the workflow. OrchStep resolves secrets through a resolver and treats them as tainted — they're pulled in at run time and scrubbed from logs and the context that gets persisted, so the audit line records the version, not the material:
dotenv: ["rotation.env?"]
env:
STORE_TOKEN: <required>The <required> sentinel means the run refuses to start without the token, and the trailing ? on the dotenv file makes it optional so the same workflow runs in CI where the file isn't present. The secret flows through; it never lands in the YAML.
Let the timer own the schedule
The task is built to be called unattended. A cron line or a systemd timer runs it, and the exit code is the signal:
orchstep run rotate --output jsonIf any step fails — the store is down, the verify trips — the run exits non-zero and the timer records a failure you can alert on. --output json gives the timer's log collector something structured to ingest. And before you trust it to a schedule, dry-run it once to see the whole sequence resolve without touching the store:
orchstep run rotate --dry-runWhat you gained
| Concern | Quarterly runbook | Scheduled task |
|---|---|---|
| When it runs | when someone remembers | cron / systemd timer |
| The secret | pasted around | resolver, tainted, scrubbed |
| Same value everywhere | hope | one steps.mint.version |
| Did it mint? | trust | assert before audit |
| Record of change | maybe a ticket | AUDIT line + exit code |
| Failure visibility | silent | non-zero exit, alertable |
Where this isn't enough
If your platform already does native rotation — a managed database that rotates its own credentials, a cloud KMS with automatic key rotation — let it. This pattern is for the credentials that don't rotate themselves: signing keys, third-party API tokens, the long tail of secrets that otherwise age in place because rotating them is a manual chore.
Where to go next
- Secrets & resolvers — tainting, scrubbing, and the resolver
- Environments & dotenv —
dotenvand the required sentinel - Error Handling — retry, assert, and exit codes
The project above is a runnable, echo-only demo — no real store is touched. Wire in the resolver and your key store to make it live.
curl -fsSL https://orchstep.dev/install.sh | sh