Skip to content

feat: manage automatic key rotation via --workspace-key-rotation - #53

Open
piotrek-janus wants to merge 11 commits into
masterfrom
add-key-rotation-v2
Open

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

Conversation

@piotrek-janus

@piotrek-janus piotrek-janus commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Depends on SecureAuthCorp/ciam-client-go#76 (SecureAuthCorp/ciam-client-go#76), which adds the automatic key rotation endpoints to the system client. Merged; go.mod pins the client's master commit b8f2a1b.

Adds --workspace-key-rotation <workspace-id>, an exclusive mode on pull, push and diff for managing a workspace's automatic signing (sig) and encryption (enc) key rotation as code. It follows the same shape as workspace secrets management (#50): a dedicated root flag, its own file under the workspace directory, and its own small store and client. Plain pull, push and diff never read, write or send key rotation.

# 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 --workspace-key-rotation demo writes the file from the server. A use that was never configured is omitted; the read-only scheduled_at is dropped.
  • push --workspace-key-rotation demo validates the file (unknown fields, cron syntax) and replaces each use present in it. --dry-run prints what would be sent. Uses absent from the file are left untouched.
  • diff --workspace-key-rotation demo compares the local file against the remote workspace and shows only what a push would change.
  • --filter, --method, --source and --target are rejected in this mode with a plain message. Required-flag checks for --method and --source/--target moved after the mode branch, as on the secrets branch.

Design notes

  • The server validates the cron expression even when enabled is false, and omitting it is also rejected. Local validation mirrors that with a clear message instead of a 400.

  • Cron syntax is checked with gorhill/cronexpr, pinned to the same version the server uses, so what passes locally is what the server accepts.

  • starting_from is write-only: the server never returns it, so pull never writes it and diff ignores it. It is honored only when in the future.

  • Tenant mode is out of scope, as for secrets. The flag is per workspace.

  • Fixes a latent panic in tenant storage when a workspace directory has no server.yaml, which this mode can now produce.

  • Calls the system keys API (/api/system/{tenant}/servers/{wid}/keys/automatic-key-rotation). The admin keys API rejects the system workspace client-credentials token cac uses. The system routes require the manage_servers scope, so the cac client must be allowed that scope and list it in client.scopes next to manage_configuration (documented in the README). The default scopes are unchanged, as for secrets.

  • The client bump also adds read-only created_at and updated_at to the system server model. Written as zero values to server.yaml, they made the next push fail strict decoding (unknown name "created_at"). server.yaml now leaves them out. The bump also adds the new id_jag_ttl field, so pulled server.yaml files gain id_jag_ttl: 0s.

Supersedes #52, which carried the same feature through the workspace patch map instead of a dedicated mode.

Verification

  • make lint: only the three pre-existing findings.
  • go test ./...: all packages pass. go mod tidy -diff is clean.
  • Each commit builds. The client bump commit on its own fails the storage tests; the next commit (fix(storage)) fixes them.
  • The key rotation client tests fail against the old admin path and pass against /api/system/.../keys/automatic-key-rotation.

Live checks against a development tenant, run with the built binary on an ephemeral workspace, with manage_servers allowed for the client and listed in client.scopes:

  • With the default scopes, the system route returns 403 manage_servers scope is required.
  • pull --workspace-key-rotation on a workspace with no rotation configured reports No key rotation configured and writes no file.
  • push --workspace-key-rotation of sig (enabled, cron, starting_from) and enc (disabled, cron): --dry-run prints the settings, the real push reports both uses pushed, and diff --workspace-key-rotation is empty afterwards.
  • With the local file deleted, pull recreates it with both uses as pushed (enabled and cron), without starting_from or scheduled_at.
  • Changing the sig cron to @monthly: diff shows only that line changed; after push the diff is empty and pull returns @monthly. scheduled_at read back from the system API is the first of the next month.
  • Disabling sig with its cron kept and pushing: pull returns both uses disabled. Re-enabling and pushing restores it.
  • With key rotation configured, plain --workspace pull and push --method import both succeed, the plain pull leaves key_rotation.yaml in place, and diff --workspace-key-rotation stays empty after the plain push.
  • Steps that make no API call behave as expected: push --workspace-key-rotation --dry-run prints the settings; a bad cron, an enc use without cron, and a scheduled_at field are each rejected before any call; --method and --filter are rejected in this mode.
  • Isolation: plain --workspace pull leaves key_rotation.yaml byte-for-byte unchanged (same mtime and checksum). Plain push --method import of the pulled config succeeds, and diff --source local --target remote is empty afterwards.
  • With the client bump but without the storage fix, the same plain pull followed by push --method import --dry-run fails with unknown name "created_at". With the fix it passes.

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 gets its own file store, mirroring workspace secrets:
workspaces/<wid>/key_rotation.yaml is taken whole from the first storage
directory that has it, rendered through templates and strictly decoded
(unknown fields, including the read-only scheduled_at, are rejected).
Writes go to the first directory. The patch-map helpers (Pop/Get) and
utils.AsPatch are removed; nothing carries key rotation in a patch anymore.
Reads use=sig and use=enc, omitting a use the server reports as never
configured, and replaces each present use on write. Built from the
existing client and exposed as Application.KeyRotation, like the secrets
store.
A workspace directory holding only key_rotation.yaml (as produced by a
key-rotation pull into a tenant layout) yielded an empty map and a nil
interface conversion panic on the id field.
Exclusive root flag, mutually exclusive with --workspace and --tenant and
required as one of the three. Each command returns early into its own
function: pull writes the file from the server, push validates and
replaces each present use (--dry-run prints the YAML), diff compares the
local file against the remote workspace and shows only what a push would
change. --filter, --method, --source and --target are rejected in this
mode. The cobra required-flag markers for --method and --source/--target
are replaced by checks after the mode branch, as on the secrets branch.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical serialization and tenant-storage regressions, plus unresolved validation, stale-state, and flag-contract issues, must be fixed.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity · 3 Medium severity

Open (5)
What changed in this PR

Adds dedicated workspace key-rotation management through YAML storage, API integration, and exclusive CLI modes.

Changes:

  • Adds key-rotation models, cron validation, storage, and API support.
  • Adds pull, push, and diff key-rotation workflows.
  • Updates documentation, tests, dependencies, and tenant storage handling.
File Review
README.md Documents key-rotation usage and schema.
internal/​cac/​storage/​tenant_storage.go Adds missing-server handling, but critically discards non-empty layered workspace patches.
internal/​cac/​storage/​tenant_storage_test.go Tests workspace-directory storage behavior.
internal/​cac/​keyrotation/​keyrotation.go Defines models and validation, but critically serializes unset timestamps and a read-only field.
internal/​cac/​keyrotation/​keyrotation_test.go Tests conversion and cron validation.
internal/​cac/​keyrotation/​dir_store.go Implements YAML storage, but root id and tenant_id fields bypass strict validation.
internal/​cac/​keyrotation/​dir_store_test.go Tests directory storage behavior.
internal/​cac/​client/​mock_server_test.go Extends mock API support.
internal/​cac/​client/​key_rotation_api.go Implements key-rotation API access.
internal/​cac/​client/​key_rotation_api_test.go Tests API reads and writes.
internal/​cac/​app.go Exposes the key-rotation API store.
go.sum Records dependency checksums.
go.mod Adds the cron parser dependency.
cmd/​root.go Adds the workspace key-rotation flag.
cmd/​push.go Implements push and dry-run, but ignores --out and --no-validate.
cmd/​pull.go Implements pull, but leaves stale local settings when the remote configuration is empty.
cmd/​flags.go Removes the obsolete required-flag helper.
cmd/​diff.go Implements diff, but does not apply push-equivalent cron validation.
cmd/​diff_key_rotation_test.go Tests diff behavior and flag handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/cac/keyrotation/keyrotation.go Outdated
Comment thread internal/cac/storage/tenant_storage.go
Comment thread cmd/pull.go
Comment thread cmd/push.go
Comment thread internal/cac/keyrotation/dir_store.go Outdated
The bumped client adds read-only created_at and updated_at to the
system ServerDump model. Writing their zero values made the pulled
server.yaml fail strict decoding into the hub TreeServer on push, and
broke the read round trip. Also expect the new id_jag_ttl field.
The admin keys API rejects the system workspace client-credentials
token cac uses. Switch KeyRotationAPIStore and the keyrotation model
conversions to the system keys service, which sits behind the
manage_servers scope, and document that scope in the README.

This branch has not been deployed

No deployments
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.

2 participants