Files
vault-plugin-secrets-apptoken/path_wellknown.go
T
unkinben ec12dfb84f
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
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
2026-08-02 23:42:42 +10:00

93 lines
2.8 KiB
Go

package apptoken
import (
"context"
"encoding/json"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
const (
jwksPath = ".well-known/jwks.json"
openidConfigPath = ".well-known/openid-configuration"
)
func pathJWKS(b *appTokenBackend) *framework.Path {
return &framework.Path{
Pattern: jwksPath + "$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "apptoken",
OperationSuffix: "jwks",
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{Callback: b.pathJWKSRead},
},
HelpSynopsis: "Public JSON Web Key Set for offline token validation.",
HelpDescription: "Unauthenticated. Returns the public signing keys (current plus retained) so services can verify app tokens without contacting Vault per request.",
}
}
func pathOpenIDConfig(b *appTokenBackend) *framework.Path {
return &framework.Path{
Pattern: openidConfigPath + "$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "apptoken",
OperationSuffix: "openid-configuration",
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{Callback: b.pathOpenIDConfigRead},
},
HelpSynopsis: "OIDC issuer metadata.",
HelpDescription: "Unauthenticated. Advertises the issuer and jwks_uri so standard OIDC/JWT clients can discover the validation keys.",
}
}
// rawJSONResponse renders a body as an unauthenticated raw JSON HTTP response,
// the form OIDC/JWKS clients expect (rather than Vault's data-wrapped JSON).
func rawJSONResponse(body interface{}) (*logical.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
return &logical.Response{
Data: map[string]interface{}{
logical.HTTPContentType: "application/json",
logical.HTTPRawBody: data,
logical.HTTPStatusCode: 200,
},
}, nil
}
func (b *appTokenBackend) pathJWKSRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
ks, err := b.getKeyset(ctx, req.Storage)
if err != nil {
return nil, err
}
jwks, err := ks.jwks()
if err != nil {
return nil, err
}
return rawJSONResponse(jwks)
}
func (b *appTokenBackend) pathOpenIDConfigRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
cfg, err := b.getConfig(ctx, req.Storage)
if err != nil {
return nil, err
}
issuer := ""
if cfg != nil {
issuer = cfg.Issuer
}
meta := map[string]interface{}{
"issuer": issuer,
"jwks_uri": issuer + "/" + jwksPath,
"response_types_supported": []string{"token"},
"subject_types_supported": []string{"public"},
"id_token_signing_alg_values_supported": []string{algEdDSA, algRS256},
}
return rawJSONResponse(meta)
}