CLI command

skarbiec rotate

Replace an item's secret at the system that issued it and in the vault together — on a schedule, after a leak or when someone leaves — without opening a console or pasting a secret anywhere, and never leave it changed in one place only. It is one operation for every provider; each provider is an adapter that knows how its system makes, proves and ends a secret, and `rotation providers` lists them with the flags each takes.

Invocation#

Shell
skarbiec rotate <id> --with <provider> [that provider's flags]
skarbiec rotation providers

Purpose#

Replace an item's secret at the system that issued it and in the vault together — on a schedule, after a leak or when someone leaves — without opening a console or pasting a secret anywhere, and never leave it changed in one place only. It is one operation for every provider; each provider is an adapter that knows how its system makes, proves and ends a secret, and `rotation providers` lists them with the flags each takes.

Required inputs and options#

  • The item's id and --with naming the provider; the provider's flags follow, and a flag it does not take is refused before it is asked. database: a login or host account item whose username names the account and whose password holds its current one; --url is the database's address with no user or password in it — postgresql:// (or postgres://) changed with psql, mysql:// with mysql — and --length the new password's length in letters and digits. aws: an access-key item holding an IAM user's access_key_id and secret_access_key; --delete-old deletes the old key instead of leaving it inactive; the aws CLI must be on PATH and the key needs iam:GetUser, iam:CreateAccessKey, iam:UpdateAccessKey and iam:DeleteAccessKey on its own user. gcp: a service-account item whose credential_json holds a service account key file; --delete-old deletes the old key instead of disabling it; gcloud must be on PATH and the account needs iam.serviceAccountKeys.create, disable and delete on itself. azure: an oauth-client item whose client_id is the application's id and client_secret its current secret; --tenant by id or domain; --end-date the day the new secret ends or --years how many years it lasts, which a rotation policy names because a fixed day passes; --delete-old deletes the old secret once the new one signs in; az must be on PATH, signed in as an account that may manage the application's credentials. gitlab: a token item; --url the instance's https address; --expires-at the new token's last day, else GitLab's default. cloudflare: a token item; --account names the account owning an account-owned token, else it is a user token; the token needs permission to edit API tokens. twilio: an api-key item whose api_user is the Account SID (starting with AC) and whose api_key is the current auth token. sendgrid: a token item holding a SendGrid API key, SG.<key id>.<secret>, with API Keys access. vercel: a token item holding a Vercel access token; --days makes the new token end that many days from now. ssh: a key-pair item; --comment is the new key's comment, else the old one's or the id; --hosts names the servers, as ssh takes them, whose authorized_keys hold the old key, reached in batch mode against the user's own known_hosts; --github names repositories as owner/name and --gitlab projects as group/project holding the old key as a deploy key; --github-account and --gitlab-account name the account gh or glab is signed in as, holding the old key among its SSH keys (gh needs admin:public_key), and --github-signing GitHub's list of that account's signing keys (gh needs admin:ssh_signing_key); gh or glab must be on PATH. curl must be on PATH for gitlab, cloudflare, twilio, sendgrid and vercel. POST /v1/operator/items/rotate takes id, with and the provider's flags; GET /v1/operator/rotation/providers answers rotation providers.

Output and state effects#

  • Every provider stores the new secret as the item's next revision, so the old one stays in its saved versions, and proves it before the old one ends; the answer carries provider and never a secret. database: generates the password, checks it against the vault policy and stores it first; the client signs in with the old password and from its standard input sets the new one for the account that signed in — ALTER ROLE CURRENT_USER on PostgreSQL, ALTER USER CURRENT_USER() on MySQL — and a second sign-in proves it; journals db-rotated and prints ok, item, account, url, client and revision; passwords reach the client only through PGPASSWORD or MYSQL_PWD and standard input. aws: the aws CLI signed in with the stored key names its user and makes a new key; the new key marks the old one inactive, proving AWS accepts it, or with --delete-old deletes it; journals aws-key-rotated and prints ok, item, user, key, retired, retired_as and revision; keys reach the aws CLI only through its environment with AWS_PROFILE and AWS_SESSION_TOKEN removed. gcp: gcloud signed in with the stored key makes a new key for the account; the new key disables the old one, or with --delete-old deletes it; journals gcp-key-rotated and prints ok, item, account, project, key, retired, retired_as and revision; key files reach gcloud only as owner-only files beside the vault, removed before the answer, under a gcloud configuration of the call's own, so gcloud's accounts and settings are neither read nor changed and GOOGLE_APPLICATION_CREDENTIALS is ignored. azure: az appends a secret named skarbiec with the end asked for and lists the secrets to find its key id and the end Entra recorded; a sign-in as the service principal under an Azure configuration of the call's own proves it; with --delete-old the secrets whose hint the old secret begins with are deleted; journals azure-secret-rotated and prints ok, item, client, key, ends, old_keys, deleted, revision and next. gitlab: POST /api/v4/personal_access_tokens/self/rotate revokes the token and answers a new one with the same scopes, and GET /api/v4/personal_access_tokens/self proves it; the new end is recorded as the item's expiry, as expiry set records one; journals gitlab-token-rotated and prints ok, item, url, name, scopes, expires_at, expiry and revision. cloudflare: GET /tokens/verify names the token and PUT /tokens/<id>/value ends the old value and answers a new one with every permission, verified again; an end Cloudflare sets is recorded as the item's expiry; journals cloudflare-token-rotated and prints ok, item, token_id, status, expires_on, expiry and revision. twilio: AuthTokens/Secondary makes a secondary token, a sign-in to the account proves it and AuthTokens/Promote makes it primary, which ends the old one; journals twilio-token-rotated and prints ok, item, account, revision and next. sendgrid: the key's name and scopes come from /v3/api_keys/<key id>, SendGrid makes a key with the same name and scopes, a sign-in proves it and the new key deletes the old one; journals sendgrid-key-rotated and prints ok, item, name, key_id, scopes, revision and next. vercel: /v5/user/tokens/current names the token and its team, Vercel makes a token with the same name for that team, ending --days days from now when given, /v2/user proves it and the new token deletes the old one; journals vercel-token-rotated and prints ok, item, name, token_id, team, expires_at, revision and next. ssh: ssh-keygen makes a fresh Ed25519 pair in an owner-only directory; each repository, account and signing list named gets the new key beside the old one with its title and what else it carried (read_only, can_push, usage_type), and each host gets the new line in authorized_keys and must log in with it; only when every place holds it does the vault take the pair, keeping every other field, context, tags and recipients; then the retired line is removed from each host and the old key from each place, the global git signing key moves to the new key when it named the old one, and an ssh-agent holding the retired key is given the new one; journals ssh-key-rotated and prints ok, item, revision, public_key, retired_public_key, hosts, deploy_keys, git_signing, agent and next. Curl-based providers send the secret only as a header on curl's standard input.

Refusals#

  • No id, no --with, a provider this vault does not rotate through — the refusal names the ones it does — and a flag the provider does not take exit 2 with the usage, as do each provider's own malformed flags: a database --url that is neither postgresql:// nor mysql://, lacks a host or database or names a user, an azure end given both ways, neither way, as no day or as no whole number of years, a gitlab address that is not https:// or names a user, a cloudflare --account that is not an account id, a vercel --days that is not a whole number, ssh --hosts naming no host or one starting with -, and --github or --gitlab naming no repository or one of the wrong shape. An item of another kind is refused naming its kind and the kind the provider replaces, as are an item holding no secret, an aws key with a session token or of the root account, a gcp credential that is not a service account key file, a twilio api_user that is not an Account SID and a sendgrid token that is not a SendGrid key; a lifecycle-controlled or replica item is refused as every owner write is. The provider's own refusal is named with its client's or API's status and words, and nothing changes. When the new secret does not prove itself, the providers that can take it back — database, aws, gcp, azure, twilio, sendgrid, vercel, ssh — take it back and the item holds the old secret again as a new revision, said with that revision; a new secret that could not be taken back is named for its console. gitlab and cloudflare end the old token as they answer the new one, so when the vault cannot take it the new token is written to an owner-only file beside the vault and the refusal names it with set as the next step. Each provider journals its failure (db-rotate-failed, aws-rotate-failed, …).

Examples#

skarbiec rotation providers skarbiec rotate prod-db --with database --url postgresql://db.example.com/app --length <N> skarbiec rotate ci-deployer --with aws --delete-old skarbiec rotate ci-app-entra --with azure --tenant contoso.onmicrosoft.com --years <N> skarbiec rotate ci-gitlab-token --with gitlab --url https://gitlab.example.com skarbiec rotate deploy-key --with ssh --hosts deploy@web1.example.com --github example-org/app skarbiec rotation set ci-deployer --every-days <N> --with aws --delete-old