feat(openbao): kubernetes ServiceAccount auth — no static token in DB
Some checks failed
Some checks failed
Why: requiring a static OpenBao root token to live (even once-bootstrap) on
the plaintext backend is the weakest link in the chain. With the bao-side
Kubernetes auth method enabled, mcpd's pod can authenticate using its own
projected SA token, exchange it for a short-lived Vault client token, and
keep the database free of any vault credentials at all.
Driver changes (src/mcpd/src/services/secret-backends/openbao.ts):
- New `OpenBaoConfig.auth = 'token' | 'kubernetes'`. Defaults to 'token' so
existing rows keep working. Both shapes share url + mount + pathPrefix +
namespace; auth-specific fields are mutually exclusive in the config schema.
- Kubernetes auth flow: read JWT from /var/run/secrets/.../token, POST to
/v1/auth/<authMount>/login {role, jwt}, cache the returned client_token
for `lease_duration - 60s` (grace window), then re-login.
- One-shot 403-retry: if a request comes back 403 (revoked / clock skew),
purge cache and retry the original request once with a fresh login.
- Reads + writes go through the same getToken() path so token-auth is
unchanged for existing deployments.
CLI (src/cli/src/commands/create.ts):
- `mcpctl create secretbackend bao --type openbao --auth kubernetes \
--url https://bao.example:8200 --role mcpctl`
- Optional `--auth-mount` (default 'kubernetes') + `--sa-token-path` (default
the standard projected-token path) for non-default deployments.
- Token-auth path unchanged: `--auth token --token-secret SECRET/KEY`
(or omit `--auth` since 'token' is the default).
Validation (factory.ts) gates on the auth strategy: each path enforces its
own required fields and produces a clear error if misconfigured.
Tests: 6 new k8s-auth unit cases (login wire shape, lease-based caching,
custom authMount, 403-on-login, missing-role rejection, missing-tokenSecretRef
rejection). Full suite 1859/1859. Completions regenerated for the new flags.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -319,7 +319,11 @@ export function createCreateCommand(deps: CreateCommandDeps): Command {
|
||||
.option('--namespace <ns>', 'openbao: X-Vault-Namespace header value')
|
||||
.option('--mount <mount>', 'openbao: KV v2 mount point (default: secret)')
|
||||
.option('--path-prefix <prefix>', 'openbao: path prefix under mount (default: mcpctl)')
|
||||
.option('--token-secret <ref>', 'openbao: token secret reference in SECRET/KEY form (e.g. bao-creds/token)')
|
||||
.option('--auth <method>', "openbao: auth method — 'token' (default) or 'kubernetes'")
|
||||
.option('--token-secret <ref>', 'openbao token auth: token secret reference in SECRET/KEY form (e.g. bao-creds/token)')
|
||||
.option('--role <name>', "openbao kubernetes auth: vault role to login as (e.g. 'mcpctl')")
|
||||
.option('--auth-mount <path>', "openbao kubernetes auth: vault auth method mount path (default: 'kubernetes')")
|
||||
.option('--sa-token-path <path>', "openbao kubernetes auth: filesystem path to projected SA token (default: '/var/run/secrets/kubernetes.io/serviceaccount/token')")
|
||||
.option('--config <entry>', 'Extra config as key=value (repeat for multiple)', collect, [])
|
||||
.option('--force', 'Update if already exists')
|
||||
.action(async (name: string, opts) => {
|
||||
@@ -328,14 +332,28 @@ export function createCreateCommand(deps: CreateCommandDeps): Command {
|
||||
|
||||
if (type === 'openbao') {
|
||||
if (!opts.url) throw new Error('--url is required for openbao backend');
|
||||
if (!opts.tokenSecret) throw new Error('--token-secret is required for openbao backend (format: SECRET/KEY)');
|
||||
const slashIdx = (opts.tokenSecret as string).indexOf('/');
|
||||
if (slashIdx < 1) throw new Error(`Invalid --token-secret '${opts.tokenSecret as string}'. Expected SECRET_NAME/KEY_NAME`);
|
||||
const auth = (opts.auth as string | undefined) ?? 'token';
|
||||
if (auth !== 'token' && auth !== 'kubernetes') {
|
||||
throw new Error(`--auth must be 'token' or 'kubernetes' (got '${auth}')`);
|
||||
}
|
||||
config.url = opts.url;
|
||||
config.tokenSecretRef = {
|
||||
name: (opts.tokenSecret as string).slice(0, slashIdx),
|
||||
key: (opts.tokenSecret as string).slice(slashIdx + 1),
|
||||
};
|
||||
config.auth = auth;
|
||||
|
||||
if (auth === 'token') {
|
||||
if (!opts.tokenSecret) throw new Error('--token-secret is required for openbao token auth (format: SECRET/KEY)');
|
||||
const slashIdx = (opts.tokenSecret as string).indexOf('/');
|
||||
if (slashIdx < 1) throw new Error(`Invalid --token-secret '${opts.tokenSecret as string}'. Expected SECRET_NAME/KEY_NAME`);
|
||||
config.tokenSecretRef = {
|
||||
name: (opts.tokenSecret as string).slice(0, slashIdx),
|
||||
key: (opts.tokenSecret as string).slice(slashIdx + 1),
|
||||
};
|
||||
} else {
|
||||
if (!opts.role) throw new Error("--role is required for openbao kubernetes auth (the vault role bound to this pod's ServiceAccount)");
|
||||
config.role = opts.role;
|
||||
if (opts.authMount) config.authMount = opts.authMount;
|
||||
if (opts.saTokenPath) config.serviceAccountTokenPath = opts.saTokenPath;
|
||||
}
|
||||
|
||||
if (opts.namespace) config.namespace = opts.namespace;
|
||||
if (opts.mount) config.mount = opts.mount;
|
||||
if (opts.pathPrefix) config.pathPrefix = opts.pathPrefix;
|
||||
|
||||
Reference in New Issue
Block a user