From 1c5301289c2a040405797f5e59edd40da2312c5e Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 20 Apr 2026 17:27:09 +0100 Subject: [PATCH] =?UTF-8?q?refactor(wizard):=20rename=20--admin-token=20?= =?UTF-8?q?=E2=86=92=20--setup-token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any token with policy-write + auth/token admin works; root is a convenient default but a scoped service account is fine too. The previous naming misrepresented the permission floor as root-only. - flag: --admin-token → --setup-token - wizard field: adminToken → setupToken - prompt label: "OpenBao admin / root token" → "OpenBao setup token (needs policy write + auth/token admin perms; root is fine)" - file doc + one comment reworded - tests updated for the new label - regression test (token-absent-from-stdout) kept unchanged Co-Authored-By: Claude Opus 4.7 (1M context) --- .../commands/create-secretbackend-wizard.ts | 22 ++++++++++--------- src/cli/src/commands/create.ts | 4 ++-- .../create-secretbackend-wizard.test.ts | 14 ++++++------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/cli/src/commands/create-secretbackend-wizard.ts b/src/cli/src/commands/create-secretbackend-wizard.ts index 2f99461..96d7750 100644 --- a/src/cli/src/commands/create-secretbackend-wizard.ts +++ b/src/cli/src/commands/create-secretbackend-wizard.ts @@ -15,7 +15,9 @@ * 10. (Optional) promotes the new backend to default. * 11. Prints the migration command for the user to run. * - * Admin token is used only for steps 2–6 and is never persisted. + * Setup token (only needs policy-write + auth/token admin on OpenBao; root is + * a convenient default but a scoped service account works too) is used only + * for steps 2–6 and is never persisted. * * All prompts go through `ConfigSetupPrompt` (from `config-setup.ts`) so the * wizard is testable without real stdin. @@ -46,7 +48,7 @@ export interface WizardInput { name: string; /** Pre-filled via flags for CI; falls back to prompt. */ url?: string | undefined; - adminToken?: string | undefined; + setupToken?: string | undefined; mount?: string | undefined; pathPrefix?: string | undefined; policyName?: string | undefined; @@ -64,15 +66,15 @@ export async function runSecretBackendOpenbaoWizard( const log = deps.log; const url = input.url ?? await prompt.input('OpenBao URL', 'https://bao.ad.itaz.eu'); - const adminToken = input.adminToken ?? await prompt.password('OpenBao admin / root token'); - if (adminToken === '') throw new Error('admin token is required'); + const setupToken = input.setupToken ?? await prompt.password('OpenBao setup token (needs policy write + auth/token admin perms; root is fine)'); + if (setupToken === '') throw new Error('setup token is required'); const vaultDeps: VaultDeps = {}; if (deps.fetch !== undefined) vaultDeps.fetch = deps.fetch; // 1. Health check. log(' → checking OpenBao health …'); - const health = await verifyHealth(url, adminToken, vaultDeps); + const health = await verifyHealth(url, setupToken, vaultDeps); if (!health.initialized || health.sealed) { throw new Error(`OpenBao is not ready (initialized=${String(health.initialized)}, sealed=${String(health.sealed)})`); } @@ -85,18 +87,18 @@ export async function runSecretBackendOpenbaoWizard( // 2. Enable KV v2 if needed. log(` → ensuring KV v2 at ${mount}/ …`); - const created = await ensureKvV2(url, adminToken, mount, vaultDeps); + const created = await ensureKvV2(url, setupToken, mount, vaultDeps); log(` ${created ? 'mounted' : 'already mounted'}`); // 3. Write policy. log(` → writing policy '${policyName}' …`); const hcl = buildAppMcpdPolicyHcl({ mount, pathPrefix, tokenRole }); - await writePolicy(url, adminToken, policyName, hcl, vaultDeps); + await writePolicy(url, setupToken, policyName, hcl, vaultDeps); log(` written (scope: ${mount}/{data,metadata}/${pathPrefix}/* + self-rotation paths)`); // 4. Ensure token role. log(` → ensuring token role '${tokenRole}' (period=720h, renewable) …`); - await ensureTokenRole(url, adminToken, tokenRole, { + await ensureTokenRole(url, setupToken, tokenRole, { allowedPolicies: [policyName], period: 720 * 3600, renewable: true, @@ -104,9 +106,9 @@ export async function runSecretBackendOpenbaoWizard( }, vaultDeps); log(' ok'); - // 5. Mint the first periodic token using the admin token. + // 5. Mint the first periodic token using the setup token. log(' → minting first periodic token …'); - const minted = await mintRoleToken(url, adminToken, tokenRole, vaultDeps); + const minted = await mintRoleToken(url, setupToken, tokenRole, vaultDeps); if (!minted.renewable) { throw new Error(`minted token is not renewable — the role '${tokenRole}' config is wrong`); } diff --git a/src/cli/src/commands/create.ts b/src/cli/src/commands/create.ts index 41d6182..9bec976 100644 --- a/src/cli/src/commands/create.ts +++ b/src/cli/src/commands/create.ts @@ -326,7 +326,7 @@ export function createCreateCommand(deps: CreateCommandDeps): Command { .option('--sa-token-path ', "openbao kubernetes auth: filesystem path to projected SA token (default: '/var/run/secrets/kubernetes.io/serviceaccount/token')") .option('--config ', 'Extra config as key=value (repeat for multiple)', collect, []) .option('--wizard', 'Interactive wizard (openbao only): provision policy + token role, mint token, store on mcpd, suggest migration') - .option('--admin-token ', "openbao wizard: OpenBao admin/root token (prompted if omitted). Used only for provisioning; NEVER persisted.") + .option('--setup-token ', "openbao wizard: OpenBao token with provisioning perms (policy write + auth/token admin). Root works; a scoped SA token works too. Prompted if omitted. Used only for provisioning; NEVER persisted.") .option('--policy-name ', "openbao wizard: name for the policy created on OpenBao (default: 'app-mcpd')") .option('--token-role ', "openbao wizard: name for the token role created on OpenBao (default: 'app-mcpd-role')") .option('--no-promote-default', 'openbao wizard: do not promote this backend to default after creation') @@ -341,7 +341,7 @@ export function createCreateCommand(deps: CreateCommandDeps): Command { const { runSecretBackendOpenbaoWizard } = await import('./create-secretbackend-wizard.js'); const wizardInput: Parameters[0] = { name }; if (opts.url !== undefined) wizardInput.url = opts.url as string; - if (opts.adminToken !== undefined) wizardInput.adminToken = opts.adminToken as string; + if (opts.setupToken !== undefined) wizardInput.setupToken = opts.setupToken as string; if (opts.mount !== undefined) wizardInput.mount = opts.mount as string; if (opts.pathPrefix !== undefined) wizardInput.pathPrefix = opts.pathPrefix as string; if (opts.policyName !== undefined) wizardInput.policyName = opts.policyName as string; diff --git a/src/cli/tests/commands/create-secretbackend-wizard.test.ts b/src/cli/tests/commands/create-secretbackend-wizard.test.ts index c5257e1..490c2eb 100644 --- a/src/cli/tests/commands/create-secretbackend-wizard.test.ts +++ b/src/cli/tests/commands/create-secretbackend-wizard.test.ts @@ -83,7 +83,7 @@ describe('runSecretBackendOpenbaoWizard', () => { 'Token role name': 'app-mcpd-role', }, password: { - 'OpenBao admin / root token': 'root.admin.token', + 'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': 'root.admin.token', }, confirm: { "Promote 'bao' to default backend?": true, @@ -99,7 +99,7 @@ describe('runSecretBackendOpenbaoWizard', () => { const firstCallInit = fetchFn.mock.calls[0]![1] as RequestInit; expect((firstCallInit.headers as Record)['X-Vault-Token']).toBe('root.admin.token'); - // Secret was created with the minted token value (hvs.AAA), not the admin token + // Secret was created with the minted token value (hvs.AAA), not the setup token expect(created.secret).toMatchObject({ name: 'bao-creds', data: { token: 'hvs.AAA' } }); // SecretBackend created with rotation config @@ -119,19 +119,19 @@ describe('runSecretBackendOpenbaoWizard', () => { expect(fullLog).toContain("You have 2 secret(s) on 'default'"); expect(fullLog).toContain('mcpctl --direct migrate secrets --from default --to bao'); - // Admin token never appears in the log (critical) + // Setup token never appears in the log (critical) expect(fullLog).not.toContain('root.admin.token'); }); - it('rejects when admin token is empty', async () => { + it('rejects when setup token is empty', async () => { const prompt = scriptedPrompt({ input: { 'OpenBao URL': 'http://x' }, - password: { 'OpenBao admin / root token': '' }, + password: { 'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': '' }, }); await expect(runSecretBackendOpenbaoWizard( { name: 'bao' }, { client: mockClient({}), log: () => {}, prompt, fetch: vi.fn() as unknown as typeof fetch }, - )).rejects.toThrow(/admin token is required/); + )).rejects.toThrow(/setup token is required/); }); it('rejects when vault is sealed', async () => { @@ -140,7 +140,7 @@ describe('runSecretBackendOpenbaoWizard', () => { ]); const prompt = scriptedPrompt({ input: { 'OpenBao URL': 'http://x' }, - password: { 'OpenBao admin / root token': 't' }, + password: { 'OpenBao setup token (needs policy write + auth/token admin perms; root is fine)': 't' }, }); await expect(runSecretBackendOpenbaoWizard( { name: 'bao' },