From fa242e4e767ae353669e92f2d60166086b9a762f Mon Sep 17 00:00:00 2001 From: Andreas Niemann Date: Tue, 30 Jun 2026 21:37:26 +0200 Subject: [PATCH] feat(bootstrap): write data-plane creds to Vault KV v2 (T06) writeCredentialsToVault distributes the generated postgres + rustfs service-credentials into the Vault `foundation` kv-v2 mount at the CONTRACT_002 paths, over docker-exec/SSH (ADR-007) since 8200 isn't reachable from the operator. Secret values go in as a JSON object on the container's stdin (never argv); the root token from the vault-init output authenticates. dependsOn vault.init = GATE A. Idempotent: kv-v2 enable is guarded, `vault kv put` overwrites. Forgejo crypto secrets, the runner token, registry tokens, and backup creds are written by their own tasks (T08/T10/T12). Live on cx33 Helsinki: foundation/{postgres,rustfs}/service-credentials present with every CONTRACT_002 camelCase key non-empty; mount is kv v2. Acceptance T06 met for the data-plane slice. Co-Authored-By: Claude Opus 4.8 (1M context) --- bootstrap/components/credentials.ts | 80 +++++++++++++++++++++++++++++ bootstrap/index.ts | 19 ++++--- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/bootstrap/components/credentials.ts b/bootstrap/components/credentials.ts index 028fd73..bbcc857 100644 --- a/bootstrap/components/credentials.ts +++ b/bootstrap/components/credentials.ts @@ -13,8 +13,11 @@ // camelCase keys, no exceptions (CONTRACT_002 §2.2): the Vault write JSON-encodes // these objects, so the key names ARE the camelCase Vault keys downstream reads. import * as pulumi from "@pulumi/pulumi"; +import * as command from "@pulumi/command"; import { RandomPassword } from "@pulumi/random"; import { DeployCtx } from "../lib/context"; +import { vmConnection } from "../lib/remote"; +import { VaultOutputs } from "./vault"; /** `foundation/postgres/service-credentials` (CONTRACT_002 §2.3). */ export interface PostgresCredentials { @@ -63,3 +66,80 @@ export function generateCredentials(ctx: DeployCtx): FoundationCredentials { }, }; } + +// Writes the generated data-plane credentials into Vault KV v2 at the CONTRACT_002 +// paths. Like every control-plane op the egg can't reach 8200 directly, so this +// runs `vault kv put` inside foundation-vault over SSH (ADR-007). Secret VALUES go +// in as a JSON object on the container's stdin (never in argv); the root token +// authenticates via -e (transient, VM-trusted). Idempotent: kv-v2 enable is +// guarded and `vault kv put` overwrites. Forgejo crypto secrets, the runner token, +// registry tokens, and backup creds are written by their own tasks (T08/T10/T12) — +// this is the Phase-5 data-plane slice. dependsOn vault.init = GATE A. +const WRITE_CREDS = `set -eu +IFS= read -r ROOT_TOKEN +IFS= read -r PG_SUPER_PW +IFS= read -r PG_FORGEJO_PW +IFS= read -r RUSTFS_ADMIN_PW +IFS= read -r RUSTFS_SVC_ID +IFS= read -r RUSTFS_SVC_SECRET +C=foundation-vault +VE="-e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN" + +if ! docker exec $VE "$C" vault secrets list -format=json 2>/dev/null | jq -e 'has("foundation/")' >/dev/null; then + docker exec $VE "$C" vault secrets enable -path=foundation kv-v2 >/dev/null +fi + +put() { docker exec -i $VE "$C" vault kv put "foundation/$1" - >/dev/null; } + +jq -n --arg u "$PG_SUPER_USER" --arg p "$PG_SUPER_PW" --arg fu "$PG_FORGEJO_USER" --arg fp "$PG_FORGEJO_PW" \ + '{postgresSuperUser:$u,postgresSuperPassword:$p,forgejoDbUser:$fu,forgejoDbPassword:$fp}' \ + | put postgres/service-credentials + +jq -n --arg u "$RUSTFS_ADMIN_USER" --arg p "$RUSTFS_ADMIN_PW" --arg ki "$RUSTFS_SVC_ID" --arg ks "$RUSTFS_SVC_SECRET" \ + '{rustfsAdminUser:$u,rustfsAdminPassword:$p,rustfsServiceKeyId:$ki,rustfsServiceKeySecret:$ks}' \ + | put rustfs/service-credentials + +echo "vault: wrote postgres + rustfs service-credentials"`; + +/** + * T06 — distribute the generated data-plane credentials into Vault (CONTRACT_002). + * Depends on Vault being unsealed (GATE A) via vault.init. + */ +export function writeCredentialsToVault( + ctx: DeployCtx, + creds: FoundationCredentials, + vault: VaultOutputs, +): command.remote.Command { + // Non-secret usernames are prepended as shell vars; the 6 secret values (root + // token first, then the order the script `read`s them) arrive on stdin. + const create = pulumi.interpolate`PG_SUPER_USER='${creds.postgres.superUser}' +PG_FORGEJO_USER='${creds.postgres.forgejoDbUser}' +RUSTFS_ADMIN_USER='${creds.rustfs.adminUser}' +${WRITE_CREDS}`; + + return new command.remote.Command( + "foundation-vault-credentials", + { + connection: vmConnection(ctx), + create, + update: create, + stdin: pulumi.interpolate`${vault.rootToken} +${creds.postgres.superPassword} +${creds.postgres.forgejoDbPassword} +${creds.rustfs.adminPassword} +${creds.rustfs.serviceKeyId} +${creds.rustfs.serviceKeySecret} +`, + addPreviousOutputInEnv: false, + triggers: [ + vault.init.id, + creds.postgres.superPassword, + creds.postgres.forgejoDbPassword, + creds.rustfs.adminPassword, + creds.rustfs.serviceKeyId, + creds.rustfs.serviceKeySecret, + ], + }, + { dependsOn: [vault.init] }, + ); +} diff --git a/bootstrap/index.ts b/bootstrap/index.ts index 94a981c..e8a5702 100644 --- a/bootstrap/index.ts +++ b/bootstrap/index.ts @@ -9,7 +9,10 @@ import { loadConfig } from "./config"; import { buildBaseContext, DeployCtx } from "./lib/context"; import { deployNetwork } from "./components/network"; import { deployDns } from "./components/dns"; -import { generateCredentials } from "./components/credentials"; +import { + generateCredentials, + writeCredentialsToVault, +} from "./components/credentials"; import { deployPostgres } from "./components/postgres"; import { deployRustfs } from "./components/rustfs"; import { deployVault } from "./components/vault"; @@ -35,11 +38,10 @@ const credentials = generateCredentials(ctx); const postgres = deployPostgres(ctx, credentials.postgres); const rustfs = deployRustfs(ctx, credentials.rustfs); const vault = deployVault(ctx); -// -// --- GATE A: Vault init + unseal (T05) → run.sh captures unseal keys to encrypted -// config; credentials.ts (T06) dependsOn vault.init. -// writeCredentialsToVault(ctx, credentials, { vault }); -// + +// --- GATE A: Vault init + unseal (T05). T06 writes the generated data-plane creds +// into Vault (CONTRACT_002), dependsOn vault.init so it runs only once unsealed. +const vaultCreds = writeCredentialsToVault(ctx, credentials, vault); // ============================================================================= // PHASE 6 — FORGE (depends on: credentials, GATE A) // T07 caddy · T08 forgejo · T10 runner @@ -51,7 +53,10 @@ const vault = deployVault(ctx); // ============================================================================= // Stack outputs (extended as phases land). -export const phase = "T05-vault"; // data-plane complete: postgres, rustfs, vault +// vaultCreds (T06) is a gate for Forgejo (T08) — it has no output to export yet. +void vaultCreds; + +export const phase = "T06-credentials"; // data-plane + creds-in-Vault export const networkName = network.name; export const vmTarget = `${cfg.vm.user}@${cfg.vm.host}`; export const postgresEndpoint = postgres.endpoint;