Rotation providers
How server-side rotation execution works, and every provider (postgres, mysql, redis, aws-iam, generic-webhook, cloudflare, gitlab).
secrets rotate --provider <name> hands the new-value computation off to a rotation plugin
instead of you typing one in. Run vaultic rotation-providers to list every available plugin and
its description.
Where rotation actually runs
The CLI itself never connects directly to the external system being rotated (a customer's database, a payment provider, a cloud IAM service, ...) — only the Vaultic server does. "Rotate the credential" and "store the new value" are two separate steps:
Reveal the current value
The CLI reveals the secret's current value via the API.
Execute the rotation, server-side
The CLI calls the rotate-execute endpoint with the provider name, current value, and any
provider-specific options (--role, --admin-conn-string, etc.). The server connects to
the target system and performs the actual rotation there.
Store the result
The CLI stores the returned value via the ordinary rotate flow — see Secrets rotation for the grace window and webhook that applies here.
The upshot: only the Vaultic server needs network access and credentials to reach a target
database (or other external system) — not every machine that happens to run the CLI. Rotation
execution is authorized and audit-logged the same way any other write to a secret is
(secret.rotation_executed in the audit log, alongside secret.rotated for the storage step).
postgres, mysql, and redis all connect to a customer-supplied host, so they share two env
vars on the server:
| Env var | Default | Purpose |
|---|---|---|
ROTATION_ALLOWED_HOSTS | (none) | Comma-separated hostnames exempt from the public-IP check below — for scratch/dev/self-hosted targets that only resolve internally |
ROTATION_CONNECTION_TIMEOUT_MS | 5000 | Connection timeout for the rotation attempt |
Every target host is checked against assertPublicHostname (the same SSRF guard used for
outbound webhooks) unless it's in ROTATION_ALLOWED_HOSTS — a rotation secret's connection
string can't be used to reach internal infrastructure or the cloud metadata endpoint.
postgres
Treats the secret's current value as a postgres://user:password@host:port/db connection
string. The server connects (with the current value, or --admin-conn-string for different
credentials — e.g. a superuser), runs ALTER ROLE "<role>" WITH PASSWORD '<new>', and returns
an updated connection string with the new password. Single-user rotation — there's no
create-a-second-role dance, just an in-place password change.
vaultic secrets rotate DB_URL --provider postgres --admin-conn-string "$DB_ADMIN_URL"| Flag | Description |
|---|---|
--role <name> | Role to rotate (default: username parsed from the connection string) |
--admin-conn-string <url> | Connect with different credentials than the value being rotated |
--new-password <value> | Use this password instead of a random 32-byte base64url one |
Role names are validated against ^[A-Za-z_][A-Za-z0-9_]*$; only postgres:///postgresql://
connection strings are accepted.
mysql
The same idea as postgres, for MySQL: treats the current value as a
mysql://user:password@host:port/db connection string and runs
ALTER USER 'user'@'host' IDENTIFIED BY '<new>'. Single-user rotation, no blue/green account
pair. MySQL accounts are 'user'@'host' pairs, not just a username — the host part defaults to
% (the common "app user connecting from anywhere" grant) and can be overridden.
vaultic secrets rotate DB_URL --provider mysql --option host=10.0.%.%| Flag | Description |
|---|---|
--role <name> | User to rotate (default: username parsed from the connection string) |
--option host=<pattern> | The '@host' part of the account (default %) |
--admin-conn-string <url> | Connect with different credentials than the value being rotated |
--new-password <value> | Use this password instead of a random 32-byte base64url one |
redis
Treats the current value as a redis://[user:]password@host:port connection string (rediss://
for TLS) and runs ACL SETUSER <user> resetpass ><new> — requires Redis 6+ ACLs. resetpass
clears every previously-set password on the user before adding the new one; the account's other
ACL rules (key patterns, command permissions) are untouched. A connection string with no
explicit username (the legacy requirepass-only form) rotates the built-in default user.
vaultic secrets rotate REDIS_URL --provider redis| Flag | Description |
|---|---|
--role <name> | ACL username to rotate (default: username parsed from the connection string, or default) |
--admin-conn-string <url> | Connect with different credentials than the value being rotated |
--new-password <value> | Use this password instead of a random password |
aws-iam
Rotates an AWS IAM access key: creates a new key, verifies it authenticates (GetUser), then
deletes the old one — the standard two-key dance. This one's different from the database
providers above in one respect: it doesn't take a customer host at all, and it uses the secret's
own current credentials to talk to AWS, the same self-service shape as postgres/mysql
rotating their own password. Give the IAM user a policy scoped to
iam:CreateAccessKey/iam:DeleteAccessKey/iam:GetUser on
arn:aws:iam::*:user/${aws:username} (or equivalent) so it can manage its own keys.
Treats the secret's current value as JSON: {"accessKeyId":"...","secretAccessKey":"..."} —
the new value is stored in the same shape.
vaultic secrets rotate AWS_KEY --provider aws-iam --option region=eu-west-1| Flag | Description |
|---|---|
--option region=<region> | AWS region for the IAM calls (default us-east-1; IAM is a global service, but the SDK still needs one) |
If verification fails, the newly-created key is deleted and the original key is left untouched — rotation never leaves the account with zero working keys. If the final delete-the-old-key step fails, rotation still succeeds (the new key is already confirmed working), and the summary returned calls out the orphaned key so it can be cleaned up by hand.
generic-webhook
The escape hatch for anything without a rotation API. Vaultic generates a new value (or uses one
you supply), HMAC-signs it with the same scheme as outbound webhook
deliveries, and POSTs { secretKey, newValue } to an endpoint
you run. That endpoint applies the new value to whatever system it fronts and responds with a
2xx status; Vaultic then stores the same value it sent.
vaultic secrets rotate API_KEY --provider generic-webhook \
--option endpoint=https://example.com/rotate-hook \
--option secret="$ROTATE_HOOK_SECRET"| Flag | Description |
|---|---|
--option endpoint=<url> | Your endpoint (required) |
--option secret=<value> | HMAC secret — signs the request body into an x-vaultic-signature: sha256=... header, same as webhook deliveries |
--option newValue=<value> | Use this value instead of a randomly generated one |
Unlike ordinary webhook event dispatch (fire-and-forget, single attempt, a failure just recorded in the delivery log), this call is synchronous — the caller is waiting on the new value — so a failed attempt is retried with backoff (up to 3 attempts total) before rotation fails outright. The endpoint is checked with the same SSRF guard as every other outbound webhook, including across redirects.
cloudflare
Rolls a Cloudflare API token to a new value in place via PUT /user/tokens/:id/value — the
token's scopes, resources, and policies are untouched, only the secret value changes. No
create/verify/delete dance needed; Cloudflare's API does the swap atomically. Self-service: the
token authenticates its own roll request.
Treats the secret's current value as JSON: {"tokenId":"...","value":"..."} — the new value is
stored in the same shape.
vaultic secrets rotate CF_TOKEN --provider cloudflareTalks to Cloudflare's own fixed API host (api.cloudflare.com), not a customer-supplied one, so
— like aws-iam — there's no ROTATION_ALLOWED_HOSTS involvement here.
gitlab
Rotates a GitLab personal access token via POST /personal_access_tokens/self/rotate —
authenticated with the token being rotated, it invalidates that token and returns a fresh one
with the same scopes. Self-service, same shape as cloudflare/aws-iam.
Treats the secret's current value as the raw PAT (no JSON wrapping needed).
vaultic secrets rotate GITLAB_PAT --provider gitlab \
--option gitlabUrl=https://gitlab.example.com \
--option expiresAt=2027-01-01| Flag | Description |
|---|---|
--option gitlabUrl=<url> | GitLab instance base URL (default https://gitlab.com) |
--option expiresAt=<YYYY-MM-DD> | New expiry for the rotated token (optional) |
Unlike cloudflare/aws-iam, the target host here is customer-supplied (self-hosted GitLab
is common), so this goes through the same SSRF-guarded, redirect-following fetch as outbound
webhooks and generic-webhook rotation — not the ROTATION_ALLOWED_HOSTS path
postgres/mysql/redis use.
Other providers
GitHub personal access token rotation (fine-grained or classic) was considered and left out:
GitHub's API has no endpoint to create a new PAT — only list and revoke — so there's no "new
value" a plugin could actually produce. That's a GitHub API gap, not a Vaultic limitation —
gitlab above rotates real PATs precisely because GitLab exposes the endpoint GitHub doesn't. A
rotation reminder (--remind-every, see Secrets rotation) plus a manual
regeneration flow is the honest answer for GitHub today.