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>
65 lines
2.1 KiB
TypeScript
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);
|
|
}
|
|
}
|