Credential keyring operations
This portable contract describes its linked source snapshot. For current deployment instructions, use current installation, authentication and native Cloudflare guides. Historical receipts do not verify later builds.
Golf has a bounded operator workflow for re-encrypting stored PAT/OAuth/local session credentials, pending OAuth PKCE verifiers, persistent local-account forge credentials, immutable infrastructure profile-version credentials, delegated automation tokens and personal/instance native Codex snapshots. It retains the existing AES-256-GCM codec, adds immutable key labels and uses a database active-key policy. It does not rotate the underlying provider token, OAuth client secret, database password or Git key.
The earlier three-family native qualification follows product commit 15dc1e5. The historical keyring contract and implementation ledger distinguish the implemented maintenance workflow from unqualified rolling deployment, live OAuth exchange across activation and backup recovery.
Supply configuration securely
| Environment variable | Purpose |
|---|---|
DATABASE_URL | Golf's database connection with migration/operator permissions; never Forgejo's database. |
GOLF_ENCRYPTION_KEY | Existing canonical padded base64 32-byte secret, configured under reserved label legacy. |
GOLF_ENCRYPTION_KEYS | JSON object mapping labels to canonical padded base64 32-byte secrets; maximum 16 total keys and 16 KiB JSON. |
GOLF_ACTIVE_ENCRYPTION_KEY | Configured write label, required with the JSON keyring and present in it. Legacy-only configuration defaults to legacy. |
Labels are 1–64 ASCII characters: start with a letter or digit, then letters, digits, ., _ or -. Duplicate JSON labels are rejected. If both configuration forms specify legacy material, the bytes must match. Keep secrets out of command arguments and committed files.
The database permanently binds registered labels to their material. Never replace the secret behind an existing label. Runtime needs every key still used by any retained encrypted credential family, including expired sessions/verifiers and disabled local accounts, and its configured active label must match database policy. Standby labels become bound on explicit activation. On a fresh JSON-only setup, explicitly activate a nonlegacy label before starting the normal server.
Compose forwards these variables. Helm reads individually optional entries from the existing Secret so either configuration form works, but runtime still requires a valid complete keyring. Every replica must agree on the material and active policy. A Secret change does not update running processes; the chart does not automate key cutover.
Use the server/operator binary
Build from the product checkout with make build. These are server commands, distinct from the user-facing read-only bin/golf CLI. Securely supply the database and key environment first:
./bin/golf-server keys status
./bin/golf-server keys activate
./bin/golf-server keys rotate --batch-size 100 --max-batches 10
./bin/golf-server keys retirement-check --key-id legacy
The container binary is /usr/local/bin/golf. With an already-built image and running database, a one-off command can use Compose's configured environment:
docker compose run --rm --no-deps golf keys status
Status is not strictly read-only: opening the database applies migrations, and the first legacy bootstrap authenticates all existing session/verifier/local-account ciphertext before registering the material binding. It reads 500 rows per page under one locked transaction; this bounds memory, not total duration. A wrong key or corrupt ciphertext fails without erasing credentials. Later status is not a full nonlegacy integrity scan.
All commands accept --timeout from 1s to 5m, default 60s. Rotation defaults to 100 total rows per batch and 10 batches per invocation; supported maxima are 1,000 rows and 100 batches. Results use schema_version: "golf.keys.v1", separate from golf.cli.v1, with safe labels/counts and states complete, partial, eligible, failed or interrupted. A partial rotation can exit zero. Inspect the state and remaining counts; an interrupted command can have durable earlier batches. No output contains key material, fingerprints, ciphertext or provider credentials.
Change keys with a maintenance boundary
- Preserve the current database/key recovery path. Prepare a new label and random 32-byte key while retaining all old material still needed by live rows or backups.
- Stop admission, drain every old Golf writer and in-flight OAuth exchange, then stop old application instances. An operator command does not discover or drain your fleet.
- Give the operator the complete keyring with the new configured active label. Inspect status, run
activate, and inspect its result. Activation changes write policy; it does not rewrite existing ciphertext. - Start only correctly configured instances with the new active label and all required read keys. Check availability and existing authenticated workflows.
- Run bounded
rotatecalls until status shows no uses of the old label. Busy locked rows may yield a partial result; resume later. Each nonempty batch commits rewritten rows and its operator event together. - Run
retirement-checkfor the old inactive label. It requires zero uses in all four credential families, including expired sessions/attempts and disabled local accounts. Only then review removal from active configuration, separately preserving secrets needed to restore older backups.
A database trigger rejects ciphertext written with a nonactive label, and policy locks order activation against writes. That fence cannot undo a provider refresh already completed outside PostgreSQL. Draining old issuers is required; this is not transparent rolling rotation. A stale in-flight issuer can still force a later sign-in if provider rotation succeeds but persistence fails.
Session IDs, cookies, expiry and provider credentials are preserved by re-encryption. Operator events are separate from user Account activity. Their actor is the PostgreSQL role, not a verified human; ordinary history/material-binding edits are guarded, but a database owner can alter those guards.
Status additionally reports local_accounts per key; rotation reports rotated_local_accounts. Disabled accounts still retain recoverable ciphertext and block retirement. Local-account ciphertext has its own authenticated domain and immutable random binding, including under the legacy label. Re-encryption preserves passwords, mappings, roles and credential versions. The isolated native local-authentication proof rotates active and disabled accounts, removes the legacy key and verifies subsequent password/source access.
Retirement and recovery limits
An eligible retirement check records evidence. It never erases memory, environment files, backups or external secrets, and does not permanently disable the label or revoke provider access. The same registered material can be explicitly activated again. The migration refuses downgrade while policy or any row uses a nonlegacy label; a permitted downgrade still drops crypto-event history and registered bindings. Do not equate binary rollback with a safe schema/key rollback.
The qualified old-binary trial creates PAT sessions and a pending PKCE attempt on product f79bf6f, then checks byte-identical ciphertext/IDs/bindings/expiry/kinds after migration. PAT cookies survive drained activation, partial rotation, restart/resume and legacy-key removal; the pending verifier decrypts/consumes on a denied callback. Real PostgreSQL race tests cover material/domain binding, rollback, busy rows, expired records, metadata guards and OAuth HTTP fixtures. The full real-PostgreSQL race suite passes, including the callback’s database-time expiry check after a held-row wait. The ordinary actual-provider OAuth refresh regression also passes on the final image using legacy-key configuration, with provider settings restored afterward. Neither result proves a live OAuth token exchange across a key change.
The canonical Docker recovery increment captures every configured key and preserves registry/policy/crypto history while invalidating restored session and PKCE ciphertext. Its native restore/restart qualification passes for the bounded canonical Docker profile; it does not prove a live OAuth exchange across recovery. Kubernetes cutovers, mixed-version rolling activation, broader recovery, emergency compromise procedures, application age/envelope/KMS migration, secret erasure and FIPS assurance remain separate work. Backup age encryption does not replace the application credential codec. Retain historical material before discarding any key used by older backups.
Infrastructure profile credentials
Current key availability, rotation, retirement checks and append-only crypto events also include encrypted infrastructure connection versions. Each version has a separate authenticated domain and immutable profile/version binding. Historical versions remain required by object-backed runs after a newer profile is saved or activated. Do not discard their credentials or key material merely because the head is no longer active. Re-encryption preserves endpoint/bucket/prefix/provider bindings and does not rotate underlying S3/R2 credentials. See infrastructure settings and the current authoritative keyring contract. The earlier pinned qualifications above remain historical evidence, not proof of every current storage/recovery combination.
Managed native Codex snapshots
Schema022 includes encrypted native Codex auth snapshots in the existing infrastructure_connection domain with a distinct golf:codex-account:v1 connection binding. Availability/bootstrap checks, bounded rotation, usage counting and retirement include them. The existing infrastructure_connections and rotated_infrastructure_connections counters aggregate saved infrastructure profile credentials and native Codex snapshots; they are not counts solely of provider profiles. This rotates encryption at rest, not provider access/refresh tokens. Native Codex owns renewal. Account/key lifecycle database checks pass; Named instance connections retain per-connection encrypted bindings at schema023. Full schema022/schema023 encrypted backup/fresh restore remains unqualified. See personal coding-agent accounts.