feat: manage automatic key rotation via --workspace-key-rotation - #53
Open
piotrek-janus wants to merge 11 commits into
Open
piotrek-janus wants to merge 11 commits into
piotrek-janus wants to merge 11 commits into
Conversation
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.
There was a problem hiding this comment.
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
Open (5)
Omit unset timestamps from automatic key rotation requests · New Preserve non-empty workspace patches without server.yaml · New Clear stale local configuration when no remote use is configured · New Honor --out for dry-run workspace key rotation pushes · New Reject root-level unknown fields in key rotation configuration · New
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, anddiffkey-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.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


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.modpins the client's master commitb8f2a1b.Adds
--workspace-key-rotation <workspace-id>, an exclusive mode onpull,pushanddifffor 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. Plainpull,pushanddiffnever read, write or send key rotation.pull --workspace-key-rotation demowrites the file from the server. A use that was never configured is omitted; the read-onlyscheduled_atis dropped.push --workspace-key-rotation demovalidates the file (unknown fields, cron syntax) and replaces each use present in it.--dry-runprints what would be sent. Uses absent from the file are left untouched.diff --workspace-key-rotation democompares the local file against the remote workspace and shows only what a push would change.--filter,--method,--sourceand--targetare rejected in this mode with a plain message. Required-flag checks for--methodand--source/--targetmoved after the mode branch, as on the secrets branch.Design notes
The server validates the cron expression even when
enabledisfalse, 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_fromis write-only: the server never returns it, sopullnever writes it anddiffignores 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 themanage_serversscope, so the cac client must be allowed that scope and list it inclient.scopesnext tomanage_configuration(documented in the README). The default scopes are unchanged, as for secrets.The client bump also adds read-only
created_atandupdated_atto the system server model. Written as zero values toserver.yaml, they made the next push fail strict decoding (unknown name "created_at").server.yamlnow leaves them out. The bump also adds the newid_jag_ttlfield, so pulledserver.yamlfiles gainid_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 -diffis clean.fix(storage)) fixes them./api/system/.../keys/automatic-key-rotation.Live checks against a development tenant, run with the built binary on an ephemeral workspace, with
manage_serversallowed for the client and listed inclient.scopes:manage_servers scope is required.pull --workspace-key-rotationon a workspace with no rotation configured reportsNo key rotation configuredand writes no file.push --workspace-key-rotationofsig(enabled, cron,starting_from) andenc(disabled, cron):--dry-runprints the settings, the real push reports both uses pushed, anddiff --workspace-key-rotationis empty afterwards.pullrecreates it with both uses as pushed (enabledandcron), withoutstarting_fromorscheduled_at.sigcron to@monthly:diffshows only that line changed; afterpushthe diff is empty andpullreturns@monthly.scheduled_atread back from the system API is the first of the next month.sigwith its cron kept and pushing:pullreturns both uses disabled. Re-enabling and pushing restores it.--workspacepullandpush --method importboth succeed, the plain pull leaveskey_rotation.yamlin place, anddiff --workspace-key-rotationstays empty after the plain push.push --workspace-key-rotation --dry-runprints the settings; a bad cron, anencuse withoutcron, and ascheduled_atfield are each rejected before any call;--methodand--filterare rejected in this mode.--workspacepullleaveskey_rotation.yamlbyte-for-byte unchanged (same mtime and checksum). Plainpush --method importof the pulled config succeeds, anddiff --source local --target remoteis empty afterwards.push --method import --dry-runfails withunknown name "created_at". With the fix it passes.