2026-08-03 00:00:18 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00
2026-08-02 23:42:42 +10:00

vault-plugin-secrets-apptoken

A HashiCorp Vault / OpenBao secrets engine that issues short-lived, signed JWT "app tokens" for self-made services (keaapi, encapi, artifactapi, tomswallapi, bootapi, ...). It replaces per-app static bearer Secrets with one mechanism: define a per-app role, read creds/<app> to mint a token, and let the app validate it offline against the engine's public JWKS — no Vault round-trip per request.

Any principal allowed to read creds/<role> (for example the agents approle) can mint a token for that app. Signing keys are generated inside Vault's barrier (seal-wrapped) and never leave it; only the public JWKS is exposed.

issuer (this engine)                     consumer app (keaapi)
  ├── creds/keaapi ──▶ signed JWT ──────▶ Authorization: Bearer <jwt>
  │   (aud=keaapi, exp, sub=caller)          │
  └── .well-known/jwks.json ◀── fetch ───────┘ verify signature + aud, offline

Signing

Tokens are signed with EdDSA (Ed25519) by default — compact signatures, fast verification, first-class support in Go's stdlib and go-jose, which the whole ecosystem (and Vault itself) uses. RS256 is available via config algorithm=RS256 for consumers that need RSA interop. The kid header is the RFC 7638 JWK thumbprint of the signing key, so it is stable and matches the JWKS entry.

Paths

Path Op Purpose
config R/W issuer (external mount URL, becomes iss + base of jwks_uri), algorithm (EdDSA|RS256), retained_keys
config/keys R current signing kid + all retained kids (keys are generated lazily on first use)
config/keys/rotate W generate a new current signing key; up to retained_keys previous keys stay in the JWKS
roles/<name> R/W/D, list roles/ per-app role: audience (defaults to role name), ttl, max_ttl, allowed_subjects, claims
creds/<name> R mint a signed JWT for the role
.well-known/jwks.json R, unauthenticated public JSON Web Key Set for offline validation
.well-known/openid-configuration R, unauthenticated OIDC issuer metadata (issuer, jwks_uri)

Token claims: iss (issuer), aud (role audience), sub (requesting Vault entity id or token display name), exp/nbf/iat, jti, plus any role claims. Registered claims cannot be overridden by a role.

allowed_subjects, when set, restricts which callers (matched on Vault entity id or token display name) may mint from a role. retained_keys keeps N previous signing keys published after a rotation so tokens signed just before it keep validating during the grace window.

Usage

# register + enable (the plugin_directory must contain the binary)
sha=$(sha256sum /opt/vault-plugins/vault-plugin-secrets-apptoken | cut -d' ' -f1)
vault plugin register -sha256=$sha secret vault-plugin-secrets-apptoken
vault secrets enable -path=apptoken vault-plugin-secrets-apptoken

# configure the issuer to this mount's external URL
vault write apptoken/config issuer="https://vault.k8s.syd1.au.unkin.net/v1/apptoken"

# define a per-app role and mint a token
vault write apptoken/roles/keaapi ttl=15m max_ttl=1h claims="scope=leases"
vault read  apptoken/creds/keaapi                # -> token, aud=keaapi, exp, ...

# public validation material (no token required)
curl -s "$VAULT_ADDR/v1/apptoken/.well-known/jwks.json"
curl -s "$VAULT_ADDR/v1/apptoken/.well-known/openid-configuration"

# rotate signing keys; tokens signed before rotation still validate during grace
vault write -f apptoken/config/keys/rotate

Validating tokens in a service (Go)

Consumers only need the JWKS URL and their expected audience. Fetch and cache the JWKS (honouring cache headers / periodic refresh), then verify offline:

// keaapi middleware sketch — do NOT hit Vault per request.
import (
    "github.com/coreos/go-oidc/v3/oidc" // or MicahParks/keyfunc + golang-jwt
)

// jwks_uri = "https://vault.../v1/apptoken/.well-known/jwks.json"
keySet := oidc.NewRemoteKeySet(ctx, jwksURL)
verifier := func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
        payload, err := keySet.VerifySignature(r.Context(), raw)
        // then decode payload, check: iss == expected, aud contains "keaapi",
        // exp in the future. Reject otherwise.
        ...
    })
}

kea-operator's internal/keaapi is the first intended consumer — see Follow-ups.

Why not the built-in OIDC identity-token provider?

Vault ships an identity-token provider (identity/oidc) that also issues signed JWTs with a JWKS. This engine is deliberately parallel to it because:

  • Roles are issuable by other principals. creds/<role> is a normal secret path, so the agents approle (or CI) can mint an app's token without being the token's own identity. The identity provider ties issuance to the caller's entity and its role/client model is heavier.
  • Simpler audience model. One role = one app = one audience, no separate OIDC client/assignment objects to wire up.
  • OpenBao parity + house pattern. Same plugin/Makefile/.woodpecker/RPM shape as the other vault-plugin-secrets-* engines, and the identical binary runs on OpenBao.

If you only need tokens whose subject is always the calling entity, the built-in provider may suffice; for per-app service tokens minted by shared automation, this engine is the better fit.

Build

make build      # -> dist/vault-plugin-secrets-apptoken
make test lint  # go test -race / go vet
make rpm        # vault + openbao RPM flavours

CI (Woodpecker) runs pre-commit/build/lint/test on PRs and, on a v* tag, builds the vault + openbao RPM flavours and uploads them to the artifactapi rpm-internal repo. Cut a release with make patch|minor|major (tags + pushes).

S
Description
HashiCorp Vault / OpenBao secrets engine issuing short-lived signed JWT app tokens (per-app roles, offline JWKS validation) for self-made services
Readme 70 KiB
Languages
Go 92.7%
Makefile 3.9%
Shell 3.1%
Go Template 0.3%