feat(credentials): mirror Forgejo crypto secrets into Vault (CONTRACT_002)
Close the known gap: foundation/forgejo/service-credentials held only the admin user/pw; the crypto secrets Forgejo auto-generates into app.ini were never captured. Make that path single-owned at GATE B and write admin + crypto together. - credentials.ts: drop the forgejo block from the GATE-A writer (its crypto secrets don't exist until Forgejo first-starts) and add writeForgejoCredentialsToVault — runs after forgejo.ready, reads SECRET_KEY, INTERNAL_TOKEN, LFS_JWT_SECRET ([server]) and oauth2 JWT_SECRET straight off the live app.ini via docker-exec (ADR-007), and puts the full path. One writer per Vault path avoids a put/patch race on re-runs. - index.ts: wire it at GATE B (dependsOn vault.init + forgejo.ready). Keys: forgejoAdminUser, forgejoAdminPassword, forgejoSecretKey, forgejoInternalToken, forgejoJwtSecret, forgejoOauth2JwtSecret. Validated live: forgejo path now has all six; postgres/rustfs paths intact through the GATE-A writer replacement; idempotent at 43 unchanged. FINDING: forgejoSecretKey mirrors EMPTY — skipping the web installer (INSTALL_LOCK) left Forgejo's [security] SECRET_KEY unset. Fixed next commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2ef9bc922
commit
fbd1ad4d1d
2 changed files with 76 additions and 10 deletions
|
|
@ -99,7 +99,6 @@ IFS= read -r PG_FORGEJO_PW
|
||||||
IFS= read -r RUSTFS_ADMIN_PW
|
IFS= read -r RUSTFS_ADMIN_PW
|
||||||
IFS= read -r RUSTFS_SVC_ID
|
IFS= read -r RUSTFS_SVC_ID
|
||||||
IFS= read -r RUSTFS_SVC_SECRET
|
IFS= read -r RUSTFS_SVC_SECRET
|
||||||
IFS= read -r FORGEJO_ADMIN_PW
|
|
||||||
C=foundation-vault
|
C=foundation-vault
|
||||||
VE="-e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN"
|
VE="-e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN"
|
||||||
|
|
||||||
|
|
@ -117,11 +116,12 @@ jq -n --arg u "$RUSTFS_ADMIN_USER" --arg p "$RUSTFS_ADMIN_PW" --arg ki "$RUSTFS_
|
||||||
'{rustfsAdminUser:$u,rustfsAdminPassword:$p,rustfsServiceKeyId:$ki,rustfsServiceKeySecret:$ks}' \
|
'{rustfsAdminUser:$u,rustfsAdminPassword:$p,rustfsServiceKeyId:$ki,rustfsServiceKeySecret:$ks}' \
|
||||||
| put rustfs/service-credentials
|
| put rustfs/service-credentials
|
||||||
|
|
||||||
jq -n --arg u "$FORGEJO_ADMIN_USER" --arg p "$FORGEJO_ADMIN_PW" \
|
echo "vault: wrote postgres + rustfs service-credentials"`;
|
||||||
'{forgejoAdminUser:$u,forgejoAdminPassword:$p}' \
|
// NOTE: foundation/forgejo/service-credentials is NOT written here. Its crypto
|
||||||
| put forgejo/service-credentials
|
// secrets (SECRET_KEY/INTERNAL_TOKEN/JWT) only exist after Forgejo first-starts
|
||||||
|
// and writes app.ini, so the whole forgejo path (admin + crypto) is single-owned
|
||||||
echo "vault: wrote postgres + rustfs + forgejo service-credentials"`;
|
// by writeForgejoCredentialsToVault at GATE B — keeping one writer per Vault path
|
||||||
|
// avoids a put/patch race on re-runs (CONTRACT_002 "single source of truth").
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T06 — distribute the generated data-plane credentials into Vault (CONTRACT_002).
|
* T06 — distribute the generated data-plane credentials into Vault (CONTRACT_002).
|
||||||
|
|
@ -132,12 +132,11 @@ export function writeCredentialsToVault(
|
||||||
creds: FoundationCredentials,
|
creds: FoundationCredentials,
|
||||||
vault: VaultOutputs,
|
vault: VaultOutputs,
|
||||||
): command.remote.Command {
|
): command.remote.Command {
|
||||||
// Non-secret usernames are prepended as shell vars; the 6 secret values (root
|
// Non-secret usernames are prepended as shell vars; the 5 secret values (root
|
||||||
// token first, then the order the script `read`s them) arrive on stdin.
|
// token first, then the order the script `read`s them) arrive on stdin.
|
||||||
const create = pulumi.interpolate`PG_SUPER_USER='${creds.postgres.superUser}'
|
const create = pulumi.interpolate`PG_SUPER_USER='${creds.postgres.superUser}'
|
||||||
PG_FORGEJO_USER='${creds.postgres.forgejoDbUser}'
|
PG_FORGEJO_USER='${creds.postgres.forgejoDbUser}'
|
||||||
RUSTFS_ADMIN_USER='${creds.rustfs.adminUser}'
|
RUSTFS_ADMIN_USER='${creds.rustfs.adminUser}'
|
||||||
FORGEJO_ADMIN_USER='${creds.forgejo.adminUser}'
|
|
||||||
${WRITE_CREDS}`;
|
${WRITE_CREDS}`;
|
||||||
|
|
||||||
return new command.remote.Command(
|
return new command.remote.Command(
|
||||||
|
|
@ -152,7 +151,6 @@ ${creds.postgres.forgejoDbPassword}
|
||||||
${creds.rustfs.adminPassword}
|
${creds.rustfs.adminPassword}
|
||||||
${creds.rustfs.serviceKeyId}
|
${creds.rustfs.serviceKeyId}
|
||||||
${creds.rustfs.serviceKeySecret}
|
${creds.rustfs.serviceKeySecret}
|
||||||
${creds.forgejo.adminPassword}
|
|
||||||
`,
|
`,
|
||||||
addPreviousOutputInEnv: false,
|
addPreviousOutputInEnv: false,
|
||||||
triggers: [
|
triggers: [
|
||||||
|
|
@ -162,7 +160,6 @@ ${creds.forgejo.adminPassword}
|
||||||
creds.rustfs.adminPassword,
|
creds.rustfs.adminPassword,
|
||||||
creds.rustfs.serviceKeyId,
|
creds.rustfs.serviceKeyId,
|
||||||
creds.rustfs.serviceKeySecret,
|
creds.rustfs.serviceKeySecret,
|
||||||
creds.forgejo.adminPassword,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ dependsOn: [vault.init] },
|
{ dependsOn: [vault.init] },
|
||||||
|
|
@ -230,3 +227,62 @@ ${backup.ageIdentity}
|
||||||
{ dependsOn: [vault.init] },
|
{ dependsOn: [vault.init] },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single owner of foundation/forgejo/service-credentials (CONTRACT_002 §2.3). The
|
||||||
|
// admin user/pw are generated (@pulumi/random); the crypto secrets (SECRET_KEY,
|
||||||
|
// INTERNAL_TOKEN, the LFS + OAuth2 JWT secrets) are auto-generated by Forgejo into
|
||||||
|
// app.ini on first start — they only exist post-boot, so the whole path is written
|
||||||
|
// here at GATE B (dependsOn forgejo.ready), read straight off the live app.ini via
|
||||||
|
// docker-exec (ADR-007). Admin pw on stdin (D2); crypto values are read on the VM
|
||||||
|
// and never transit the Pulumi command string. Idempotent put.
|
||||||
|
//
|
||||||
|
// NOTE: SECRET_KEY can be EMPTY when the bootstrap skips the web installer
|
||||||
|
// (INSTALL_LOCK) — it is mirrored as-is (faithful), and flagged for hardening.
|
||||||
|
const WRITE_FORGEJO_CREDS = `set -eu
|
||||||
|
IFS= read -r ROOT_TOKEN
|
||||||
|
IFS= read -r ADMIN_PW
|
||||||
|
C=foundation-vault
|
||||||
|
F=foundation-forgejo
|
||||||
|
VE="-e VAULT_ADDR=http://127.0.0.1:8200 -e VAULT_TOKEN=$ROOT_TOKEN"
|
||||||
|
gv() { docker exec "$F" sh -c "sed -n 's/^$1 *= *//p' /data/gitea/conf/app.ini" | head -1; }
|
||||||
|
SECRET_KEY=$(gv SECRET_KEY)
|
||||||
|
INTERNAL_TOKEN=$(gv INTERNAL_TOKEN)
|
||||||
|
LFS_JWT=$(gv LFS_JWT_SECRET)
|
||||||
|
OAUTH2_JWT=$(gv JWT_SECRET)
|
||||||
|
jq -n --arg au "$ADMIN_USER" --arg ap "$ADMIN_PW" \
|
||||||
|
--arg sk "$SECRET_KEY" --arg it "$INTERNAL_TOKEN" --arg jt "$LFS_JWT" --arg oj "$OAUTH2_JWT" \
|
||||||
|
'{forgejoAdminUser:$au,forgejoAdminPassword:$ap,forgejoSecretKey:$sk,forgejoInternalToken:$it,forgejoJwtSecret:$jt,forgejoOauth2JwtSecret:$oj}' \
|
||||||
|
| docker exec -i $VE "$C" vault kv put foundation/forgejo/service-credentials - >/dev/null
|
||||||
|
echo "vault: wrote forgejo/service-credentials (admin + crypto secrets)"`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirror the Forgejo admin + crypto secrets into Vault (CONTRACT_002 §2.3).
|
||||||
|
* Runs at GATE B: needs Vault unsealed (vault.init) AND Forgejo healthy
|
||||||
|
* (forgejoReady) so app.ini exists to read the crypto secrets from.
|
||||||
|
*/
|
||||||
|
export function writeForgejoCredentialsToVault(
|
||||||
|
ctx: DeployCtx,
|
||||||
|
vault: VaultOutputs,
|
||||||
|
forgejoCreds: ForgejoCredentials,
|
||||||
|
forgejoReady: command.remote.Command,
|
||||||
|
): command.remote.Command {
|
||||||
|
const create = pulumi.interpolate`ADMIN_USER='${forgejoCreds.adminUser}'
|
||||||
|
${WRITE_FORGEJO_CREDS}`;
|
||||||
|
|
||||||
|
return new command.remote.Command(
|
||||||
|
"foundation-forgejo-credentials",
|
||||||
|
{
|
||||||
|
connection: vmConnection(ctx),
|
||||||
|
create,
|
||||||
|
update: create,
|
||||||
|
stdin: pulumi.interpolate`${vault.rootToken}
|
||||||
|
${forgejoCreds.adminPassword}
|
||||||
|
`,
|
||||||
|
addPreviousOutputInEnv: false,
|
||||||
|
// forgejoReady.id changes when the container is (re)created → app.ini (hence
|
||||||
|
// the crypto secrets) regenerated → re-mirror.
|
||||||
|
triggers: [vault.init.id, forgejoReady.id, forgejoCreds.adminPassword],
|
||||||
|
},
|
||||||
|
{ dependsOn: [vault.init, forgejoReady] },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
generateCredentials,
|
generateCredentials,
|
||||||
writeCredentialsToVault,
|
writeCredentialsToVault,
|
||||||
writeBackupCredentialsToVault,
|
writeBackupCredentialsToVault,
|
||||||
|
writeForgejoCredentialsToVault,
|
||||||
} from "./components/credentials";
|
} from "./components/credentials";
|
||||||
import { deployPostgres } from "./components/postgres";
|
import { deployPostgres } from "./components/postgres";
|
||||||
import { deployRustfs } from "./components/rustfs";
|
import { deployRustfs } from "./components/rustfs";
|
||||||
|
|
@ -75,12 +76,21 @@ const forgejoBootstrap = bootstrapForgejo(ctx, {
|
||||||
sshPublicKey,
|
sshPublicKey,
|
||||||
});
|
});
|
||||||
const runner = cfg.features.runner ? deployRunner(ctx, forgejo) : undefined;
|
const runner = cfg.features.runner ? deployRunner(ctx, forgejo) : undefined;
|
||||||
|
// Mirror Forgejo's admin + app.ini crypto secrets into Vault (CONTRACT_002 §2.3);
|
||||||
|
// GATE B — needs app.ini, which exists only once Forgejo has started.
|
||||||
|
const forgejoCreds = writeForgejoCredentialsToVault(
|
||||||
|
ctx,
|
||||||
|
vault,
|
||||||
|
credentials.forgejo,
|
||||||
|
forgejo.ready,
|
||||||
|
);
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
// Stack outputs (extended as phases land).
|
// Stack outputs (extended as phases land).
|
||||||
// vaultCreds (T06) is a gate for Forgejo (T08) — it has no output to export yet.
|
// vaultCreds (T06) is a gate for Forgejo (T08) — it has no output to export yet.
|
||||||
void vaultCreds;
|
void vaultCreds;
|
||||||
void backupCreds; // CONTRACT_002 backup/backup-credentials mirror; no secret output
|
void backupCreds; // CONTRACT_002 backup/backup-credentials mirror; no secret output
|
||||||
|
void forgejoCreds; // CONTRACT_002 forgejo/service-credentials mirror; no secret output
|
||||||
|
|
||||||
export const phase = "T10-runner"; // forge + CI runner live
|
export const phase = "T10-runner"; // forge + CI runner live
|
||||||
export const caddyImageId = proxy.imageId;
|
export const caddyImageId = proxy.imageId;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue