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) <noreply@anthropic.com>
This commit is contained in:
parent
0e81635d88
commit
fa242e4e76
2 changed files with 92 additions and 7 deletions
|
|
@ -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] },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue