// 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/ (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/", then read "creds/" 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. `