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:
+91
@@ -0,0 +1,91 @@
|
||||
// Package apptoken implements a Vault / OpenBao secrets engine that issues
|
||||
// short-lived, signed JWT "app tokens" for self-made services. Each per-app
|
||||
// role mints a token whose audience is the app; services validate tokens
|
||||
// offline against the engine's unauthenticated JWKS endpoint, so no Vault
|
||||
// round-trip is needed per request.
|
||||
//
|
||||
// The engine replaces per-app static bearer Secrets with a single mechanism:
|
||||
// any principal allowed to read creds/<role> (for example the agents approle)
|
||||
// can obtain a token for that app, while signing keys stay inside Vault's
|
||||
// barrier and only the public JWKS ever leaves it.
|
||||
package apptoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const (
|
||||
configStoragePath = "config"
|
||||
keysetStoragePath = "keyset"
|
||||
roleStoragePrefix = "role/"
|
||||
)
|
||||
|
||||
type appTokenBackend struct {
|
||||
*framework.Backend
|
||||
|
||||
// keyLock serializes read-modify-write cycles on the signing keyset
|
||||
// (generation and rotation).
|
||||
keyLock sync.Mutex
|
||||
}
|
||||
|
||||
// Factory returns a configured app-token secrets backend.
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := backend()
|
||||
if err := b.Setup(ctx, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func backend() *appTokenBackend {
|
||||
b := &appTokenBackend{}
|
||||
|
||||
b.Backend = &framework.Backend{
|
||||
Help: strings.TrimSpace(backendHelp),
|
||||
BackendType: logical.TypeLogical,
|
||||
PathsSpecial: &logical.Paths{
|
||||
// The signing keyset holds private keys; keep it seal-wrapped.
|
||||
SealWrapStorage: []string{
|
||||
keysetStoragePath,
|
||||
},
|
||||
// JWKS and issuer metadata are public by design: apps fetch them
|
||||
// to validate tokens offline, with no Vault token.
|
||||
Unauthenticated: []string{
|
||||
jwksPath,
|
||||
openidConfigPath,
|
||||
},
|
||||
},
|
||||
Paths: framework.PathAppend(
|
||||
[]*framework.Path{
|
||||
pathConfig(b),
|
||||
pathConfigKeys(b),
|
||||
pathConfigKeysRotate(b),
|
||||
pathRole(b),
|
||||
pathRolesList(b),
|
||||
pathCredentials(b),
|
||||
pathJWKS(b),
|
||||
pathOpenIDConfig(b),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The apptoken secrets engine issues short-lived signed JWTs that self-made
|
||||
services accept in place of static bearer tokens.
|
||||
|
||||
Configure the issuer URL and signing algorithm on "config", define a per-app
|
||||
role under "roles/<app>", then read "creds/<app>" to mint a token whose
|
||||
audience is the app. Services validate tokens offline using the public keys at
|
||||
".well-known/jwks.json" and the metadata at ".well-known/openid-configuration",
|
||||
both of which are unauthenticated. Rotate signing keys with
|
||||
"config/keys/rotate"; previous keys stay published in the JWKS during the
|
||||
configured grace so tokens signed before the rotation keep validating.
|
||||
`
|
||||
Reference in New Issue
Block a user