diff --git a/bootstrap/components/credentials.ts b/bootstrap/components/credentials.ts index 3818e54..dce3796 100644 --- a/bootstrap/components/credentials.ts +++ b/bootstrap/components/credentials.ts @@ -37,14 +37,18 @@ export interface RustfsCredentials { } /** - * `foundation/forgejo/service-credentials` — the admin slice (CONTRACT_002 §2.3). - * The crypto secrets (forgejoSecretKey/InternalToken/Jwt*) are auto-generated by - * Forgejo into its app.ini (format-constrained — JWTs, not free random), so they - * are not generated here; capturing them into Vault is a later refinement. + * `foundation/forgejo/service-credentials` — admin slice + SECRET_KEY (CONTRACT_002 + * §2.3). The OTHER crypto secrets (forgejoInternalToken/Jwt*) are format-constrained + * (JWTs, not free random) so Forgejo auto-generates them into app.ini and the GATE-B + * mirror captures them. SECRET_KEY, by contrast, is a plain high-entropy string, so + * we generate it here (@pulumi/random, per CONTRACT_002) and inject it via env — + * Forgejo leaves it EMPTY when the web installer is skipped (INSTALL_LOCK), which + * would weaken at-rest encryption of 2FA/mirror/oauth secrets. */ export interface ForgejoCredentials { adminUser: string; // cfg.forgejo.adminUser (deterministic) adminPassword: pulumi.Output; + secretKey: pulumi.Output; // [security] SECRET_KEY (injected via env) } /** Everything generateCredentials() produces; grows as Wave-2 tasks land. */ @@ -80,6 +84,7 @@ export function generateCredentials(ctx: DeployCtx): FoundationCredentials { forgejo: { adminUser: ctx.cfg.forgejo.adminUser, // "platform-admin" adminPassword: secret("forgejo-admin-password"), + secretKey: secret("forgejo-secret-key", 40), // [security] SECRET_KEY }, }; } diff --git a/bootstrap/components/forgejo.ts b/bootstrap/components/forgejo.ts index 9711fcb..1b9d761 100644 --- a/bootstrap/components/forgejo.ts +++ b/bootstrap/components/forgejo.ts @@ -26,6 +26,7 @@ export interface ForgejoDeps { rustfs: RustfsOutputs; pgCreds: PostgresCredentials; rustfsCreds: RustfsCredentials; + forgejoCreds: ForgejoCredentials; } export interface ForgejoOutputs { @@ -88,6 +89,11 @@ export function deployForgejo( // Go SSH server colliding on :22. SSH_PORT is the clone-URL port; the sshd is // published on host :22 (scp-form goal) + :2222 (CONTRACT_003). "FORGEJO__server__START_SSH_SERVER=false", + // [security] SECRET_KEY — Forgejo leaves this EMPTY when the installer is + // skipped (INSTALL_LOCK); set it explicitly so at-rest encryption of 2FA / + // mirror / oauth secrets is keyed. env-to-ini overwrites it in the volume's + // app.ini (INTERNAL_TOKEN + JWT secrets are left untouched — Forgejo's own). + pulumi.interpolate`FORGEJO__security__SECRET_KEY=${deps.forgejoCreds.secretKey}`, "FORGEJO__server__SSH_LISTEN_PORT=22", `FORGEJO__server__SSH_PORT=${cfg.forgeSshPort}`, `FORGEJO__server__SSH_DOMAIN=${cfg.hosts.git}`, diff --git a/bootstrap/index.ts b/bootstrap/index.ts index 72d7039..9978788 100644 --- a/bootstrap/index.ts +++ b/bootstrap/index.ts @@ -61,6 +61,7 @@ const forgejo = deployForgejo(ctx, { rustfs, pgCreds: credentials.postgres, rustfsCreds: credentials.rustfs, + forgejoCreds: credentials.forgejo, }); // --- GATE B: Forgejo healthy → headless admin + org + repo + operator SSH key