Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,27 @@ map[string]any{
- "ciba_authentication_service": map[string]any{"type": string("mock")},
```

## Key rotation

Automatic key rotation is configured per workspace in `workspaces/<workspace-id>/key_rotation.yaml`:

```yaml
sig:
enabled: true
cron: "0 0 1 * *"
starting_from: "2026-10-01T00:00:00Z" # optional, write-only
enc:
enabled: false
cron: "0 0 1 * *" # required even when disabled
```

- `sig` (signing keys) and `enc` (encryption keys) are both optional. A use that is absent from the file is left untouched on the server, and `pull` omits a use that SecureAuth reports as never configured.
- `enabled` and `cron` are required for every use present in the file. SecureAuth validates the cron expression even when `enabled` is `false`, so disabling rotation still requires a valid one.
- `cron` uses [gorhill/cronexpr](https://github.com/gorhill/cronexpr) syntax: five fields (minute, hour, day of month, month, day of week), an optional sixth field for the year, or a seven-field form with seconds first. The descriptors `@yearly`, `@annually`, `@monthly`, `@weekly`, `@daily` and `@hourly` are supported, as are `L`, `W` and `#`. `@every` is not.
- `starting_from` is optional and write-only. SecureAuth never returns it, so `pull` never writes it and `diff` ignores it. It is honored only when it is in the future; a past value is ignored.
- `scheduled_at` is computed by SecureAuth, is read-only, and is rejected if present in the file.
- `--filter key_rotation` restricts a `push` or a `diff` to this file. It only applies in workspace mode: in tenant mode the whole workspace configuration, key rotation included, is selected with `--filter servers`.

## Templates

Templates are used to generate configuration files. They are using [Go template language](https://golang.org/pkg/text/template/).
Expand Down
2 changes: 1 addition & 1 deletion cmd/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Example: --only-present`)
diffCmd.PersistentFlags().StringSliceVar(&diffConfig.Filters, "filter", []string{}, `Restrict the comparison to selected top-level resources (comma-separated or repeated).
Workspace resources: clients, idps, claims, custom_apps, gateways, policies, policy_execution_points,
pools, scopes (alias of scopes_without_service), scripts, script_execution_points,
server_consent, servers_bindings, services, theme_binding, webhooks,
server_consent, servers_bindings, services, theme_binding, key_rotation, webhooks,
ciba (alias of ciba_authentication_service)
Tenant resources: pools, schemas, mfa_methods, themes, servers
Reserved: root (only root-level tenant/workspace config, excluding nested resources)
Expand Down
2 changes: 1 addition & 1 deletion cmd/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Example: --with-secrets`)
pullCmd.PersistentFlags().StringSliceVar(&pullConfig.Filters, "filter", []string{}, `Restrict the pull to selected top-level resources (comma-separated or repeated).
Workspace resources: clients, idps, claims, custom_apps, gateways, policies, policy_execution_points,
pools, scopes (alias of scopes_without_service), scripts, script_execution_points,
server_consent, servers_bindings, services, theme_binding, webhooks,
server_consent, servers_bindings, services, theme_binding, key_rotation, webhooks,
ciba (alias of ciba_authentication_service)
Tenant resources: pools, schemas, mfa_methods, themes, servers
Reserved: root (only root-level tenant/workspace config, excluding nested resources)
Expand Down
12 changes: 6 additions & 6 deletions cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ Examples:
},
}
pushConfig struct {
DryRun bool
Out string
Mode string
Method string
Filters []string
DryRun bool
Out string
Mode string
Method string
Filters []string
NoLocalValidate bool
}
)
Expand Down Expand Up @@ -135,7 +135,7 @@ Example: --no-validate`)
pushCmd.PersistentFlags().StringSliceVar(&pushConfig.Filters, "filter", []string{}, `Restrict the push to selected top-level resources (comma-separated or repeated).
Workspace resources: clients, idps, claims, custom_apps, gateways, policies, policy_execution_points,
pools, scopes (alias of scopes_without_service), scripts, script_execution_points,
server_consent, servers_bindings, services, theme_binding, webhooks,
server_consent, servers_bindings, services, theme_binding, key_rotation, webhooks,
ciba (alias of ciba_authentication_service)
Tenant resources: pools, schemas, mfa_methods, themes, servers
Reserved: root (only root-level tenant/workspace config, excluding nested resources)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/go-openapi/strfmt v0.24.0
github.com/goccy/go-yaml v1.12.0
github.com/google/go-cmp v0.7.0
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75
github.com/imdario/mergo v0.3.16
github.com/mitchellh/mapstructure v1.5.0
github.com/pkg/errors v0.9.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
Expand Down
33 changes: 29 additions & 4 deletions internal/cac/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/cloudentity/acp-client-go/clients/hub/client/workspace_configuration"
"github.com/cloudentity/acp-client-go/clients/hub/models"
"github.com/cloudentity/cac/internal/cac/api"
"github.com/cloudentity/cac/internal/cac/keyrotation"
"github.com/cloudentity/cac/internal/cac/utils"
"github.com/pkg/errors"
"golang.org/x/exp/slog"
Expand Down Expand Up @@ -79,6 +80,20 @@ func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc739
return nil, errors.Wrap(err, "failed to convert tree server to patch")
}

if filterSelects(options.Filters, keyrotation.Key) {
var rotation *keyrotation.Config

if rotation, err = readKeyRotation(ctx, c.acp, workspace); err != nil {
return nil, err
}

if rotation != nil {
if data[keyrotation.Key], err = keyRotationToPatch(rotation); err != nil {
return nil, err
}
}
}

if data, err = utils.FilterPatch(data, options.Filters, utils.ServerRootKeys); err != nil {
return nil, errors.Wrap(err, "failed to filter patch")
}
Expand All @@ -89,6 +104,7 @@ func (c *Client) Read(ctx context.Context, opts ...api.SourceOpt) (models.Rfc739
func (c *Client) Write(ctx context.Context, data models.Rfc7396PatchOperation, opts ...api.SourceOpt) error {
var (
options = &api.Options{}
rotation *keyrotation.Config
workspace string
err error
)
Expand All @@ -101,20 +117,29 @@ func (c *Client) Write(ctx context.Context, data models.Rfc7396PatchOperation, o
return errors.New("workspace is required to write using server client")
}

switch options.Method {
case "import":
// Key rotation has its own endpoint and is not part of the tree models, so it leaves the patch
// before either method sees it.
if rotation, err = keyrotation.Pop(data); err != nil {
return err
}

switch {
case len(data) == 0:
// a push filtered to key rotation alone leaves the configuration api nothing to do
slog.Debug("No workspace configuration to push", "workspace", workspace)
case options.Method == "import":
if err = c.Import(ctx, workspace, options.Mode, data); err != nil {
return err
}
case "patch":
case options.Method == "patch":
if err = c.Patch(ctx, workspace, options.Mode, data); err != nil {
return err
}
default:
return fmt.Errorf("unknown method: %v", options.Method)
}

return nil
return writeKeyRotation(ctx, c.acp, workspace, rotation)
}

func (c *Client) Patch(ctx context.Context, workspace string, mode string, data models.Rfc7396PatchOperation) error {
Expand Down
Loading
Loading