Skip to content

feat: manage automatic key rotation as code (key_rotation.yaml) - #52

Draft
piotrek-janus wants to merge 5 commits into
masterfrom
add-key-rotation
Draft

piotrek-janus wants to merge 5 commits into
masterfrom
add-key-rotation

Conversation

@piotrek-janus

Copy link
Copy Markdown
Contributor

Summary

Workspace signing and encryption key rotation can now be managed as code through a new optional per-workspace file, workspaces/<workspace-id>/key_rotation.yaml:

sig:
  enabled: true
  cron: "0 0 1 * *"
  starting_from: "2026-10-01T00:00:00Z"
enc:
  enabled: false
  cron: "0 0 1 * *"
  • pull reads the sig and enc rotation settings and writes the file. The read-only scheduled_at is dropped, and a use that was never configured is omitted.
  • push (both --method patch and --method import) updates each use present in the file. A missing file or a missing use means no change, so existing repositories are unaffected.
  • diff and push --dry-run include rotation changes in their output.
  • Validation rejects unknown fields and invalid cron expressions before anything is sent.

Design notes

  • Key rotation is not part of the workspace tree model, so it travels in the patch under a key_rotation key and is popped out right before every strict model conversion. That keeps diff, --dry-run, --filter key_rotation, merged sources and tenant mode working without extra plumbing. --filter root excludes it, like the other resources that live in their own file.
  • The server validates the cron expression even when enabled is false, so a disabled entry still needs a valid cron. Local validation mirrors that with a clear message instead of a 400 from the server.
  • starting_from is write-only: the server never returns it, so pull never writes it and diff ignores that one field. It is honored only when it is in the future.
  • Cron expressions are validated with gorhill/cronexpr, pinned to the same version the server uses, so what passes locally is what the server accepts.
  • push --filter key_rotation skips the workspace config call when nothing else remains in the patch.

Verification

  • make lint: only the three pre-existing findings.
  • go test ./...: all packages pass, 155 tests (18 new test functions and table cases covering the new package, storage round trips, client GET/PUT against the mock server, validators, diff filtering and dry-run).
  • Each commit builds and passes tests on its own.

Not yet exercised against a live tenant.

Introduces the cac-owned schema for workspaces/<wid>/key_rotation.yaml
({sig,enc} -> {enabled, cron, starting_from}) and the helpers the rest of
the pipeline uses to carry it in a patch under the key_rotation key:
Pop/Get (strict decode, unknown fields rejected), Validate (cron required
for every present use and parsed with gorhill/cronexpr, the same library
and version the server uses), and conversions to and from the admin
AutomaticKeyRotation model. The read-only scheduled_at and the
never-echoed starting_from are dropped when converting from the server.

Adds utils.AsPatch for the recurring any -> patch map conversion.
Key rotation is not part of TreeServer, so ServerStorage pops it from the
patch before the strict model conversion and writes it to its own file,
and reads it back under the key_rotation key so filters, diff and merged
sources see it like any other workspace resource. TenantStorage does the
same per workspace. The file is optional on read.
Client.Read fetches use=sig and use=enc from the admin keys endpoint and
adds them to the patch, omitting a use the server reports as never
configured (empty cron). Client.Write pops key_rotation before the hub
import or patch call, skips that call when nothing else remains
(push --filter key_rotation), and PUTs each present use afterwards.
TenantClient does the same for every workspace in sorted order.
Validators strict-decode and validate key_rotation, then run the tree
validation on a copy of the patch without it. The caller's patch is
still cleaned of id and tenant_id so push sends the same body as before.

The server never returns starting_from, so diff ignores
key_rotation.<use>.starting_from unconditionally; otherwise every diff
would report it as an addition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant