Some checks failed
CI/CD / typecheck (pull_request) Successful in 55s
CI/CD / test (pull_request) Successful in 1m4s
CI/CD / lint (pull_request) Successful in 2m2s
CI/CD / smoke (pull_request) Failing after 1m36s
CI/CD / build (pull_request) Successful in 4m13s
CI/CD / publish (pull_request) Has been skipped
One-command setup replaces the 6-step manual flow — `mcpctl create
secretbackend bao --type openbao --wizard` takes the OpenBao admin token
once, provisions a narrow policy + token role, mints the first periodic
token, stores it on mcpd, verifies end-to-end, and prints the migration
command. The admin token is NEVER persisted.
The stored credential auto-rotates daily: mcpd mints a successor via the
token role (self-rotation capability is part of the policy it was issued
with), verifies the successor, writes it over the backing Secret, then
revokes the predecessor by accessor. TTL 720h means a week of rotation
failures still leaves 20+ days of runway.
Shared:
- New `@mcpctl/shared/vault` — pure HTTP wrappers (verifyHealth,
ensureKvV2, writePolicy, ensureTokenRole, mintRoleToken, revokeAccessor,
lookupSelf, testWriteReadDelete) and policy HCL builder.
mcpd:
- `tokenMeta Json @default("{}")` on SecretBackend. Self-healing schema
migration — empty default lets `prisma db push` add the column cleanly.
- SecretBackendRotator.rotateOne: mint → verify → persist → revoke-old →
update tokenMeta. Failures surface via `lastRotationError` on the row;
the old token keeps working.
- SecretBackendRotatorLoop: on startup rotates overdue backends, schedules
per-backend timers with ±10min jitter. Stops cleanly on shutdown.
- New `POST /api/v1/secretbackends/:id/rotate` (operation
`rotate-secretbackend` — added to bootstrap-admin's auto-migrated ops
alongside migrate-secrets, which was previously missing too).
CLI:
- `--wizard` on `create secretbackend` delegates to the interactive flow.
All prompts can be pre-answered via flags (--url, --admin-token,
--mount, --path-prefix, --policy-name, --token-role,
--no-promote-default) for CI.
- `mcpctl rotate secretbackend <name>` — convenience verb; hits the new
rotate endpoint.
- `describe secretbackend` renders a Token health section (healthy /
STALE / WARNING / ERROR) with generated/renewal/expiry timestamps and
last rotation error. Only shown when tokenMeta.rotatable is true — the
existing k8s-auth + static-token backends don't surface it.
Tests: 15 vault-client unit tests (shared), 8 rotator unit tests (mcpd),
3 wizard flow tests (cli, including a regression test that the admin
token never appears in stdout). Full suite 1885/1885 (+32). Completions
regenerated for the new flags.
Out of scope (explicit): kubernetes-auth wizard, Vault Enterprise
namespaces in the wizard path, rotation for non-wizard static-token
backends. See plan file for details.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
/**
|
|
* `mcpctl rotate secretbackend <name>` — force an immediate token rotation on
|
|
* a wizard-provisioned OpenBao backend.
|
|
*
|
|
* Hits `POST /api/v1/secretbackends/:id/rotate` after resolving name → id.
|
|
* Gated server-side by the `rotate-secretbackend` operation.
|
|
*/
|
|
import { Command } from 'commander';
|
|
import type { ApiClient } from '../api-client.js';
|
|
import { resolveNameOrId } from './shared.js';
|
|
|
|
export interface RotateCommandDeps {
|
|
client: ApiClient;
|
|
log: (...args: unknown[]) => void;
|
|
}
|
|
|
|
export function createRotateCommand(deps: RotateCommandDeps): Command {
|
|
const { client, log } = deps;
|
|
|
|
const cmd = new Command('rotate')
|
|
.description('Force rotation of a credential-rotating resource (currently: secretbackend)');
|
|
|
|
cmd.command('secretbackend')
|
|
.alias('sb')
|
|
.description('Rotate the vault token on an OpenBao SecretBackend (wizard-provisioned)')
|
|
.argument('<name>', 'SecretBackend name or id')
|
|
.action(async (nameOrId: string) => {
|
|
const id = await resolveNameOrId(client, 'secretbackends', nameOrId);
|
|
const res = await client.post<{ ok?: boolean; tokenMeta?: Record<string, unknown>; error?: string }>(
|
|
`/api/v1/secretbackends/${id}/rotate`,
|
|
{},
|
|
);
|
|
if (res.ok !== true) {
|
|
throw new Error(`rotation failed: ${res.error ?? 'unknown error'}`);
|
|
}
|
|
log(`secretbackend '${nameOrId}' rotated.`);
|
|
const meta = res.tokenMeta ?? {};
|
|
if (typeof meta.generatedAt === 'string') {
|
|
log(` generated: ${meta.generatedAt}`);
|
|
}
|
|
if (typeof meta.nextRenewalAt === 'string') {
|
|
log(` next renewal: ${meta.nextRenewalAt}`);
|
|
}
|
|
if (typeof meta.validUntil === 'string') {
|
|
log(` valid until: ${meta.validUntil}`);
|
|
}
|
|
});
|
|
|
|
return cmd;
|
|
}
|