Secret rotation is the most skipped security control in production. The rotation script exists, but "nobody approved the swap" becomes "let's do it next quarter" — until the credential lands in a leak and the auditor asks why the rotation policy was ignored.
The fix isn't more automation. It's automation that stages the new credential, asks a human to approve the production swap, and records the approval — so rotation happens on schedule with evidence, not instead of it.
Secret rotation approval workflow: stage, ask, swap, record
# 1. Rotate the credential in the vault (safe, no prod impact)
vault kv put secret/db/DATABASE_URL password=$(openssl rand -hex 32)
# 2. Ask for production sign-off, with context
actionbox ask "Swap DATABASE_URL to the rotated credential in production?" \
--option swap="Swap now" \
--option defer="Defer 24h" \
--context-json '[{"type":"key_value","items":{"secret":"DATABASE_URL","rotation_policy":"90d","staging_tested":"true","incident_ticket":"none","expiry":"managed-by-vault"}}]' \
--callback-url https://sec.acme.com/actionbox/swap-callbackThe vault already has the new value; the human decides when the production swap happens. The callback performs the swap and writes the audit record.
Step 1: never let the gate block the safe part
Rotate in the vault first, gate the swap second. If nobody approves, the new credential just sits there — safe, unexpired, ready. Nothing broke, and the audit trail shows "rotation staged, swap deferred".
# The callback must first verify X-Actionbox-Timestamp and
# X-Actionbox-Signature over "timestamp.raw_body" with the webhook secret.
case "$(jq -r '.data.response.value // .data.decision // empty' "$PAYLOAD")" in
swap)
./scripts/swap-secret.sh DATABASE_URL
./scripts/record-rotation.sh DATABASE_URL "approved by on-call"
;;
defer)
./scripts/record-rotation.sh DATABASE_URL "deferred 24h by on-call"
;;
esacStep 2: compliance evidence without a spreadsheet
Auditors want three things: rotation happened on schedule, it was authorized, and the evidence wasn't altered. Every decision in Actionbox gives you all three:
- Who approved (and that the approver is a named human, not the pipeline)
- When (timestamped decision, not a Slack "done")
- What ran (the decision payload, signed webhook, and audit trail)
# Weekly audit summary: use the Actionbox History/API view filtered to the
# Source and resolved status. The CLI `get` command requires one Action ID.This feeds SOC 2 CC8.1 / PCI-DSS 6.4.2 evidence collection directly — no manual compilation.
Step 3: emergency rotation still needs a gate
Compromised-credential rotations are urgent — but "urgent" doesn't mean "unapproved". Give the emergency path its own decision with a shorter SLA:
actionbox ask "CRITICAL: rotate leaked API key for checkout service NOW?" \
--option rotate="Rotate immediately" \
--option verify="Verify leak first" \
--context-json '[{"type":"key_value","items":{"secret":"checkout_api_key","leak_source":"public repo scan","revoked_elsewhere":"false"}}]' \
--callback-url https://sec.acme.com/actionbox/emergency-rotateFail-closed on timeout: --on-expire-json '{"type":"return_expired"}' means no swap happens silently — the incident page fires instead.
The anti-pattern to avoid
Don't put the decision after the swap, or in a separate approval system nobody checks. The most common rotation failure is the "approve in the ticket system" flow: the ticket sits for two weeks, the rotation policy lapses, and the audit trail is a ticket status. Keep the decision in the rotation pipeline where the action actually happens.
Try it
- Create a Source →
ACTIONBOX_TOKEN(rotate your Source token on a schedule too — it's an Actionbox Action) - Stage a rotation in your vault, then run the
askcommand above - Watch the dashboard countdown and the audit trail record the swap
Create a free Source · Webhook + callback reference · Cron jobs that ask before acting