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_SVC_ID
|
||||
IFS= read -r RUSTFS_SVC_SECRET
|
||||
IFS= read -r FORGEJO_ADMIN_PW
|
||||
C=foundation-vault
|
||||
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}' \
|
||||
| put rustfs/service-credentials
|
||||
|
||||
jq -n --arg u "$FORGEJO_ADMIN_USER" --arg p "$FORGEJO_ADMIN_PW" \
|
||||
'{forgejoAdminUser:$u,forgejoAdminPassword:$p}' \
|
||||
| put forgejo/service-credentials
|
||||
|
||||
echo "vault: wrote postgres + rustfs + forgejo service-credentials"`;
|
||||
echo "vault: wrote postgres + rustfs service-credentials"`;
|
||||
// NOTE: foundation/forgejo/service-credentials is NOT written here. Its crypto
|
||||
// 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
|
||||
// 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).
|
||||
|
|
@ -132,12 +132,11 @@ export function writeCredentialsToVault(
|
|||
creds: FoundationCredentials,
|
||||
vault: VaultOutputs,
|
||||
): 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.
|
||||
const create = pulumi.interpolate`PG_SUPER_USER='${creds.postgres.superUser}'
|
||||
PG_FORGEJO_USER='${creds.postgres.forgejoDbUser}'
|
||||
RUSTFS_ADMIN_USER='${creds.rustfs.adminUser}'
|
||||
FORGEJO_ADMIN_USER='${creds.forgejo.adminUser}'
|
||||
${WRITE_CREDS}`;
|
||||
|
||||
return new command.remote.Command(
|
||||
|
|
@ -152,7 +151,6 @@ ${creds.postgres.forgejoDbPassword}
|
|||
${creds.rustfs.adminPassword}
|
||||
${creds.rustfs.serviceKeyId}
|
||||
${creds.rustfs.serviceKeySecret}
|
||||
${creds.forgejo.adminPassword}
|
||||
`,
|
||||
addPreviousOutputInEnv: false,
|
||||
triggers: [
|
||||
|
|
@ -162,7 +160,6 @@ ${creds.forgejo.adminPassword}
|
|||
creds.rustfs.adminPassword,
|
||||
creds.rustfs.serviceKeyId,
|
||||
creds.rustfs.serviceKeySecret,
|
||||
creds.forgejo.adminPassword,
|
||||
],
|
||||
},
|
||||
{ dependsOn: [vault.init] },
|
||||
|
|
@ -230,3 +227,62 @@ ${backup.ageIdentity}
|
|||
{ 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,
|
||||
writeCredentialsToVault,
|
||||
writeBackupCredentialsToVault,
|
||||
writeForgejoCredentialsToVault,
|
||||
} from "./components/credentials";
|
||||
import { deployPostgres } from "./components/postgres";
|
||||
import { deployRustfs } from "./components/rustfs";
|
||||
|
|
@ -75,12 +76,21 @@ const forgejoBootstrap = bootstrapForgejo(ctx, {
|
|||
sshPublicKey,
|
||||
});
|
||||
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).
|
||||
// vaultCreds (T06) is a gate for Forgejo (T08) — it has no output to export yet.
|
||||
void vaultCreds;
|
||||
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 caddyImageId = proxy.imageId;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue