Assist_Design/apps/bff/src/vault-preload.ts
ramirez ed7c167f15 feat: integrate OpenBao vault for secret management in BFF
Replace fragile .env backup/restore with Vault-based secret injection.
Secrets are preloaded via --import hook before NestJS modules evaluate,
with a 30s refresh loop and event-driven cache invalidation for services
that read secrets at init (JWT, CSRF, WHMCS).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:27:37 +09:00

65 lines
2.1 KiB
TypeScript

/**
* Vault secret preloader — runs via `node --import ./dist/vault-preload.js`
* BEFORE the main entry point loads. This ensures process.env is populated
* before any NestJS modules evaluate (ConfigModule.forRoot → Zod validate).
*
* Skipped when VAULT_ADDR is not set (local dev).
*/
const addr = process.env["VAULT_ADDR"];
const roleId = process.env["VAULT_ROLE_ID"];
const secretId = process.env["VAULT_SECRET_ID"];
const secretPath = process.env["VAULT_SECRET_PATH"] ?? "kv/data/services/portal-backend";
if (addr && roleId && secretId) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
// AppRole login
const authRes = await fetch(`${addr}/v1/auth/approle/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role_id: roleId, secret_id: secretId }),
signal: controller.signal,
});
if (!authRes.ok) {
throw new Error(`Vault login failed: ${authRes.status} ${await authRes.text()}`);
}
const authData = (await authRes.json()) as { auth: { client_token: string } };
const token = authData.auth.client_token;
// Read secrets
const secretRes = await fetch(`${addr}/v1/${secretPath}`, {
method: "GET",
headers: { "Content-Type": "application/json", "X-Vault-Token": token },
signal: controller.signal,
});
if (!secretRes.ok) {
throw new Error(`Vault read failed: ${secretRes.status} ${await secretRes.text()}`);
}
const secretData = (await secretRes.json()) as {
data: { data: Record<string, string>; metadata: { version: number } };
};
const secrets = secretData.data.data;
for (const [key, value] of Object.entries(secrets)) {
process.env[key] = value;
}
console.warn(
`[Vault] Secrets preloaded (${Object.keys(secrets).length} keys, v${secretData.data.metadata.version})`
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[Vault] FATAL: Failed to preload secrets: ${message}`);
process.exit(1);
} finally {
clearTimeout(timeout);
}
}