ec12dfb84f
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
93 lines
2.8 KiB
Go
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)
|
|
}
|