Add app-token JWT secrets engine
Implement a generic Vault/OpenBao secrets engine that issues short-lived signed JWTs for self-made services, replacing per-app static bearer Secrets. Per-app roles set the audience, TTLs, subject allowlist and custom claims; creds/<role> mints an EdDSA (or RS256) token. Apps validate offline against the unauthenticated JWKS + OIDC-metadata paths, so there is no Vault round-trip per request. Signing keys are seal-wrapped in the barrier and rotate with a configurable JWKS grace window. Mirrors the other vault-plugin-secrets-* engines: cmd ServeMultiplex, Makefile with patch|minor|major tags, .woodpecker CI (k8s resources + SA), dual-flavour nfpm RPM to artifactapi rpm-internal. Tests (-race) cover issuance+JWKS validation for both algorithms, rotation grace/trim, role isolation, subject allowlist and the unauthenticated/seal-wrap wiring. Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package apptoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathConfigKeys(b *appTokenBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config/keys$",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "apptoken",
|
||||
OperationSuffix: "keys",
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{Callback: b.pathConfigKeysRead},
|
||||
},
|
||||
HelpSynopsis: "Inspect the signing keyset.",
|
||||
HelpDescription: "Read the current signing key id plus every key id retained in the JWKS. Signing keys are generated lazily on first use.",
|
||||
}
|
||||
}
|
||||
|
||||
func pathConfigKeysRotate(b *appTokenBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config/keys/rotate$",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "apptoken",
|
||||
OperationSuffix: "keys-rotate",
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{Callback: b.pathConfigKeysRotate},
|
||||
},
|
||||
HelpSynopsis: "Rotate the signing key.",
|
||||
HelpDescription: "Generate a new current signing key. Up to retained_keys previous keys stay published in the JWKS so tokens signed before the rotation keep validating.",
|
||||
}
|
||||
}
|
||||
|
||||
func (b *appTokenBackend) pathConfigKeysRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
cfg, err := b.getConfigOrDefault(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.keyLock.Lock()
|
||||
defer b.keyLock.Unlock()
|
||||
|
||||
ks, err := b.getOrCreateKeyset(ctx, req.Storage, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys := make([]map[string]interface{}, 0, len(ks.Keys))
|
||||
for i := range ks.Keys {
|
||||
keys = append(keys, map[string]interface{}{
|
||||
"key_id": ks.Keys[i].KeyID,
|
||||
"algorithm": ks.Keys[i].Algorithm,
|
||||
"created": ks.Keys[i].Created.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
return &logical.Response{Data: map[string]interface{}{
|
||||
"current_key_id": ks.current().KeyID,
|
||||
"keys": keys,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (b *appTokenBackend) pathConfigKeysRotate(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
cfg, err := b.getConfigOrDefault(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.keyLock.Lock()
|
||||
defer b.keyLock.Unlock()
|
||||
|
||||
ks, err := b.getOrCreateKeyset(ctx, req.Storage, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ks.rotate(cfg.Algorithm, cfg.RetainedKeys, time.Now()); err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
if err := b.putKeyset(ctx, req.Storage, ks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logical.Response{Data: map[string]interface{}{
|
||||
"current_key_id": ks.current().KeyID,
|
||||
}}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user