// components/credentials.ts // // CONTRACT_002 single owner of credential GENERATION + Vault distribution. // // Generation (`generateCredentials`) is pure @pulumi/random — no Vault, no // network — so it can be created in Phase 3 and consumed by the data-plane // components (postgres.ts needs its password at container boot). Distribution // (`writeCredentialsToVault`, T06) is the half that depends on Vault being // unsealed (Gate A); it writes every value to the KV paths in CONTRACT_002 §2.3. // Splitting the two halves resolves the ordering tension between "Postgres up in // Phase 3" and "secrets in Vault in Phase 5" without giving up single ownership. // // 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 { superUser: string; // "postgres" — image default superuser (deterministic) superPassword: pulumi.Output; forgejoDbUser: string; // "forgejo" (deterministic) forgejoDbPassword: pulumi.Output; } /** `foundation/rustfs/service-credentials` (CONTRACT_002 §2.3). */ export interface RustfsCredentials { adminUser: string; // "rustfsadmin" — root access key name (deterministic) adminPassword: pulumi.Output; // root secret key (RUSTFS_SECRET_KEY) serviceKeyId: pulumi.Output; // scoped S3 key Forgejo/backup use serviceKeySecret: pulumi.Output; } /** Everything generateCredentials() produces; grows as Wave-2 tasks land. */ export interface FoundationCredentials { postgres: PostgresCredentials; rustfs: RustfsCredentials; } /** * High-entropy alphanumeric secret. special:false keeps values safe to drop into * connection strings / app.ini / psql literals without escaping (len 28 ≈ 166 bits). */ function secret(name: string, length = 28): pulumi.Output { return new RandomPassword(name, { length, special: false }).result; } /** Generate all egg credentials (pure; no dependencies). CONTRACT_002 writer. */ export function generateCredentials(ctx: DeployCtx): FoundationCredentials { return { postgres: { superUser: "postgres", superPassword: secret("postgres-super-password"), forgejoDbUser: "forgejo", forgejoDbPassword: secret("forgejo-db-password"), }, rustfs: { adminUser: "rustfsadmin", adminPassword: secret("rustfs-admin-password"), serviceKeyId: secret("rustfs-service-key-id", 20), // S3 access-key id serviceKeySecret: secret("rustfs-service-key-secret", 40), // S3 secret }, }; } // 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] }, ); }