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>
This commit is contained in:
parent
b206de8dba
commit
ed7c167f15
@ -37,6 +37,7 @@
|
||||
"@nestjs/common": "^11.1.12",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.12",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/platform-express": "^11.1.12",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
"@nestjs/swagger": "^11.2.5",
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { Module, type DynamicModule, type Type } from "@nestjs/common";
|
||||
import { APP_INTERCEPTOR, APP_PIPE, RouterModule } from "@nestjs/core";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { ScheduleModule } from "@nestjs/schedule";
|
||||
import { EventEmitterModule } from "@nestjs/event-emitter";
|
||||
import { ZodSerializerInterceptor, ZodValidationPipe } from "nestjs-zod";
|
||||
|
||||
// Configuration
|
||||
import { appConfig } from "@bff/core/config/app.config.js";
|
||||
import { apiRoutes } from "@bff/core/config/router.config.js";
|
||||
|
||||
// Vault (conditional — only when VAULT_ADDR is set)
|
||||
import { VaultModule } from "@bff/core/vault/index.js";
|
||||
|
||||
// Core Modules
|
||||
import { LoggingModule } from "@bff/core/logging/logging.module.js";
|
||||
import { SecurityModule } from "@bff/core/security/security.module.js";
|
||||
@ -46,6 +50,22 @@ import { AddressModule } from "@bff/modules/address/address.module.js";
|
||||
// System Modules
|
||||
import { HealthModule } from "@bff/modules/health/health.module.js";
|
||||
|
||||
function conditionalVaultImports(): Array<DynamicModule | Type> {
|
||||
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) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
EventEmitterModule.forRoot(),
|
||||
VaultModule.forRoot({ address: addr, roleId, secretId, secretPath }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Main application module
|
||||
*
|
||||
@ -64,6 +84,9 @@ import { HealthModule } from "@bff/modules/health/health.module.js";
|
||||
ConfigModule.forRoot(appConfig),
|
||||
ScheduleModule.forRoot(),
|
||||
|
||||
// === VAULT (conditional) ===
|
||||
...conditionalVaultImports(),
|
||||
|
||||
// === INFRASTRUCTURE ===
|
||||
LoggingModule,
|
||||
SecurityModule,
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const envSchema = z.object({
|
||||
// Vault integration (optional — set in systemd EnvironmentFile, not .env)
|
||||
VAULT_ADDR: z.string().url().optional(),
|
||||
VAULT_ROLE_ID: z.string().optional(),
|
||||
VAULT_SECRET_ID: z.string().optional(),
|
||||
VAULT_SECRET_PATH: z.string().default("kv/data/services/portal-backend"),
|
||||
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
BFF_PORT: z.coerce.number().int().positive().max(65535).default(4000),
|
||||
LOG_LEVEL: z.enum(["error", "warn", "info", "debug", "trace"]).default("info"),
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { OnEvent } from "@nestjs/event-emitter";
|
||||
import { Logger } from "nestjs-pino";
|
||||
import * as crypto from "crypto";
|
||||
import { VAULT_REFRESH_EVENT } from "@bff/core/vault/vault.constants.js";
|
||||
|
||||
export interface CsrfTokenData {
|
||||
token: string;
|
||||
@ -32,7 +34,7 @@ export interface CsrfTokenStats {
|
||||
export class CsrfService {
|
||||
private readonly tokenExpiry: number;
|
||||
|
||||
private readonly secretKey: string;
|
||||
private secretKey: string;
|
||||
|
||||
private readonly cookieName: string;
|
||||
|
||||
@ -154,6 +156,18 @@ export class CsrfService {
|
||||
};
|
||||
}
|
||||
|
||||
@OnEvent(VAULT_REFRESH_EVENT)
|
||||
handleVaultRefresh({ changedKeys }: { changedKeys: string[] }): void {
|
||||
if (!changedKeys.includes("CSRF_SECRET_KEY")) return;
|
||||
|
||||
const newKey =
|
||||
this.configService.get<string>("CSRF_SECRET_KEY") ?? process.env["CSRF_SECRET_KEY"];
|
||||
if (newKey) {
|
||||
this.secretKey = newKey;
|
||||
this.logger.log("CSRF secret key updated from Vault");
|
||||
}
|
||||
}
|
||||
|
||||
private generateSecret(): string {
|
||||
return crypto.randomBytes(32).toString("base64url");
|
||||
}
|
||||
|
||||
11
apps/bff/src/core/vault/index.ts
Normal file
11
apps/bff/src/core/vault/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export { VaultModule } from "./vault.module.js";
|
||||
export { VaultService } from "./vault.service.js";
|
||||
export { VaultClient } from "./vault.client.js";
|
||||
export { VAULT_MODULE_OPTIONS, VAULT_REFRESH_EVENT } from "./vault.constants.js";
|
||||
export type {
|
||||
VaultConfig,
|
||||
VaultTokenState,
|
||||
VaultAuthResponse,
|
||||
VaultSecretResponse,
|
||||
VaultRenewResponse,
|
||||
} from "./vault.types.js";
|
||||
57
apps/bff/src/core/vault/vault.client.ts
Normal file
57
apps/bff/src/core/vault/vault.client.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import type { VaultAuthResponse, VaultSecretResponse, VaultRenewResponse } from "./vault.types.js";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 5000;
|
||||
|
||||
export class VaultClient {
|
||||
constructor(
|
||||
private readonly address: string,
|
||||
private readonly timeoutMs = DEFAULT_TIMEOUT_MS
|
||||
) {}
|
||||
|
||||
async login(roleId: string, secretId: string): Promise<VaultAuthResponse> {
|
||||
return this.request<VaultAuthResponse>("/v1/auth/approle/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ role_id: roleId, secret_id: secretId }),
|
||||
});
|
||||
}
|
||||
|
||||
async readSecret(path: string, token: string): Promise<VaultSecretResponse> {
|
||||
return this.request<VaultSecretResponse>(`/v1/${path}`, {
|
||||
method: "GET",
|
||||
headers: { "X-Vault-Token": token },
|
||||
});
|
||||
}
|
||||
|
||||
async renewToken(token: string): Promise<VaultRenewResponse> {
|
||||
return this.request<VaultRenewResponse>("/v1/auth/token/renew-self", {
|
||||
method: "POST",
|
||||
headers: { "X-Vault-Token": token },
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init: RequestInit): Promise<T> {
|
||||
const url = `${this.address}${path}`;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...init.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => "");
|
||||
throw new Error(`Vault ${init.method} ${path} failed: ${response.status} ${body}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
apps/bff/src/core/vault/vault.constants.ts
Normal file
2
apps/bff/src/core/vault/vault.constants.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const VAULT_MODULE_OPTIONS = Symbol("VAULT_MODULE_OPTIONS");
|
||||
export const VAULT_REFRESH_EVENT = "vault.secrets.refreshed";
|
||||
23
apps/bff/src/core/vault/vault.module.ts
Normal file
23
apps/bff/src/core/vault/vault.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { type DynamicModule, Global, Module } from "@nestjs/common";
|
||||
|
||||
import { VAULT_MODULE_OPTIONS } from "./vault.constants.js";
|
||||
import { VaultService } from "./vault.service.js";
|
||||
import type { VaultConfig } from "./vault.types.js";
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class VaultModule {
|
||||
static forRoot(config: VaultConfig): DynamicModule {
|
||||
return {
|
||||
module: VaultModule,
|
||||
providers: [
|
||||
{
|
||||
provide: VAULT_MODULE_OPTIONS,
|
||||
useValue: config,
|
||||
},
|
||||
VaultService,
|
||||
],
|
||||
exports: [VaultService],
|
||||
};
|
||||
}
|
||||
}
|
||||
157
apps/bff/src/core/vault/vault.service.ts
Normal file
157
apps/bff/src/core/vault/vault.service.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
type OnModuleInit,
|
||||
type OnModuleDestroy,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { EventEmitter2 } from "@nestjs/event-emitter";
|
||||
|
||||
import { VAULT_MODULE_OPTIONS, VAULT_REFRESH_EVENT } from "./vault.constants.js";
|
||||
import { VaultClient } from "./vault.client.js";
|
||||
import type { VaultConfig, VaultTokenState } from "./vault.types.js";
|
||||
|
||||
const DEFAULT_REFRESH_INTERVAL_MS = 30_000;
|
||||
const DEFAULT_TOKEN_RENEW_THRESHOLD = 0.75;
|
||||
|
||||
@Injectable()
|
||||
export class VaultService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(VaultService.name);
|
||||
private readonly client: VaultClient;
|
||||
private readonly refreshIntervalMs: number;
|
||||
private readonly tokenRenewThreshold: number;
|
||||
|
||||
private tokenState: VaultTokenState | null = null;
|
||||
private lastKvVersion: number | null = null;
|
||||
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private renewTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(VAULT_MODULE_OPTIONS) private readonly config: VaultConfig,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2
|
||||
) {
|
||||
this.client = new VaultClient(config.address, config.requestTimeoutMs);
|
||||
this.refreshIntervalMs = config.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS;
|
||||
this.tokenRenewThreshold = config.tokenRenewThreshold ?? DEFAULT_TOKEN_RENEW_THRESHOLD;
|
||||
}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
await this.authenticate();
|
||||
await this.fetchAndApplySecrets();
|
||||
this.startRefreshLoop();
|
||||
this.scheduleTokenRenewal();
|
||||
this.logger.log("Vault secrets loaded and refresh loop started");
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
if (this.renewTimer) {
|
||||
clearTimeout(this.renewTimer);
|
||||
this.renewTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async authenticate(): Promise<void> {
|
||||
const response = await this.client.login(this.config.roleId, this.config.secretId);
|
||||
this.tokenState = {
|
||||
clientToken: response.auth.client_token,
|
||||
leaseDuration: response.auth.lease_duration,
|
||||
authenticatedAt: Date.now(),
|
||||
};
|
||||
this.logger.log(`Authenticated with Vault (TTL: ${response.auth.lease_duration}s)`);
|
||||
}
|
||||
|
||||
private async fetchAndApplySecrets(): Promise<boolean> {
|
||||
if (!this.tokenState) {
|
||||
throw new Error("Vault: not authenticated");
|
||||
}
|
||||
|
||||
const response = await this.client.readSecret(
|
||||
this.config.secretPath,
|
||||
this.tokenState.clientToken
|
||||
);
|
||||
|
||||
const currentVersion = response.data.metadata.version;
|
||||
|
||||
if (this.lastKvVersion !== null && currentVersion === this.lastKvVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const secrets = response.data.data;
|
||||
const changedKeys: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(secrets)) {
|
||||
const current = this.configService.get<string>(key);
|
||||
if (current !== value) {
|
||||
changedKeys.push(key);
|
||||
// Update process.env so ConfigService picks up the new value
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.lastKvVersion = currentVersion;
|
||||
|
||||
if (changedKeys.length > 0) {
|
||||
this.logger.log(`Vault secrets updated (v${currentVersion}): ${changedKeys.join(", ")}`);
|
||||
this.eventEmitter.emit(VAULT_REFRESH_EVENT, { changedKeys });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private startRefreshLoop(): void {
|
||||
this.refreshTimer = setInterval(() => {
|
||||
void this.refreshSecrets();
|
||||
}, this.refreshIntervalMs);
|
||||
}
|
||||
|
||||
private async refreshSecrets(): Promise<void> {
|
||||
try {
|
||||
await this.fetchAndApplySecrets();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.warn(`Vault refresh failed, keeping cached values: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleTokenRenewal(): void {
|
||||
if (!this.tokenState) return;
|
||||
|
||||
const renewAfterMs = this.tokenState.leaseDuration * 1000 * this.tokenRenewThreshold;
|
||||
|
||||
this.renewTimer = setTimeout(() => {
|
||||
void this.renewToken();
|
||||
}, renewAfterMs);
|
||||
}
|
||||
|
||||
private async renewToken(): Promise<void> {
|
||||
if (!this.tokenState) return;
|
||||
|
||||
try {
|
||||
const response = await this.client.renewToken(this.tokenState.clientToken);
|
||||
this.tokenState = {
|
||||
clientToken: response.auth.client_token,
|
||||
leaseDuration: response.auth.lease_duration,
|
||||
authenticatedAt: Date.now(),
|
||||
};
|
||||
this.logger.log(`Vault token renewed (TTL: ${response.auth.lease_duration}s)`);
|
||||
this.scheduleTokenRenewal();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
this.logger.warn(`Vault token renewal failed, re-authenticating: ${message}`);
|
||||
try {
|
||||
await this.authenticate();
|
||||
this.scheduleTokenRenewal();
|
||||
} catch (authError) {
|
||||
const authMessage = authError instanceof Error ? authError.message : String(authError);
|
||||
this.logger.error(`Vault re-authentication failed: ${authMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
apps/bff/src/core/vault/vault.types.ts
Normal file
44
apps/bff/src/core/vault/vault.types.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export interface VaultConfig {
|
||||
address: string;
|
||||
roleId: string;
|
||||
secretId: string;
|
||||
secretPath: string;
|
||||
refreshIntervalMs?: number;
|
||||
tokenRenewThreshold?: number;
|
||||
requestTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface VaultTokenState {
|
||||
clientToken: string;
|
||||
leaseDuration: number;
|
||||
authenticatedAt: number;
|
||||
}
|
||||
|
||||
export interface VaultAuthResponse {
|
||||
auth: {
|
||||
client_token: string;
|
||||
lease_duration: number;
|
||||
renewable: boolean;
|
||||
policies: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface VaultSecretResponse {
|
||||
data: {
|
||||
data: Record<string, string>;
|
||||
metadata: {
|
||||
version: number;
|
||||
created_time: string;
|
||||
deletion_time: string;
|
||||
destroyed: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface VaultRenewResponse {
|
||||
auth: {
|
||||
client_token: string;
|
||||
lease_duration: number;
|
||||
renewable: boolean;
|
||||
};
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { OnEvent } from "@nestjs/event-emitter";
|
||||
import type { WhmcsApiConfig } from "../types/connection.types.js";
|
||||
import { VAULT_REFRESH_EVENT } from "@bff/core/vault/vault.constants.js";
|
||||
|
||||
/**
|
||||
* Service for managing WHMCS API configuration
|
||||
@ -8,7 +10,8 @@ import type { WhmcsApiConfig } from "../types/connection.types.js";
|
||||
*/
|
||||
@Injectable()
|
||||
export class WhmcsConfigService {
|
||||
private readonly config: WhmcsApiConfig;
|
||||
private readonly logger = new Logger(WhmcsConfigService.name);
|
||||
private config: WhmcsApiConfig;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.config = this.loadConfiguration();
|
||||
@ -44,6 +47,15 @@ export class WhmcsConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent(VAULT_REFRESH_EVENT)
|
||||
handleVaultRefresh({ changedKeys }: { changedKeys: string[] }): void {
|
||||
const whmcsKeys = ["WHMCS_API_IDENTIFIER", "WHMCS_API_SECRET", "WHMCS_BASE_URL"];
|
||||
if (!changedKeys.some(k => whmcsKeys.includes(k))) return;
|
||||
|
||||
this.config = this.loadConfiguration();
|
||||
this.logger.log("WHMCS config reloaded from Vault");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from environment variables
|
||||
*/
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { OnEvent } from "@nestjs/event-emitter";
|
||||
import { SignJWT, decodeJwt, jwtVerify, errors, type JWTPayload } from "jose";
|
||||
import { parseJwtExpiry } from "../../utils/jwt-expiry.util.js";
|
||||
import { VAULT_REFRESH_EVENT } from "@bff/core/vault/vault.constants.js";
|
||||
|
||||
@Injectable()
|
||||
export class JoseJwtService {
|
||||
private readonly signingKey: Uint8Array;
|
||||
private readonly verificationKeys: Uint8Array[];
|
||||
private readonly logger = new Logger(JoseJwtService.name);
|
||||
private signingKey: Uint8Array;
|
||||
private verificationKeys: Uint8Array[];
|
||||
private readonly issuer: string | undefined;
|
||||
private readonly audience: string | string[] | undefined;
|
||||
private readonly configService: ConfigService;
|
||||
|
||||
constructor(configService: ConfigService) {
|
||||
this.configService = configService;
|
||||
const secret = configService.get<string>("JWT_SECRET");
|
||||
if (!secret) {
|
||||
throw new Error("JWT_SECRET is required in environment variables");
|
||||
@ -136,4 +141,22 @@ export class JoseJwtService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent(VAULT_REFRESH_EVENT)
|
||||
handleVaultRefresh({ changedKeys }: { changedKeys: string[] }): void {
|
||||
if (!changedKeys.includes("JWT_SECRET")) return;
|
||||
|
||||
const secret = this.configService.get<string>("JWT_SECRET") ?? process.env["JWT_SECRET"];
|
||||
if (!secret) return;
|
||||
|
||||
this.signingKey = new TextEncoder().encode(secret);
|
||||
const previousRaw = this.configService.get<string | undefined>("JWT_SECRET_PREVIOUS");
|
||||
const previousSecrets = this.parsePreviousSecrets(previousRaw).filter(s => s !== secret);
|
||||
this.verificationKeys = [
|
||||
this.signingKey,
|
||||
...previousSecrets.map(s => new TextEncoder().encode(s)),
|
||||
];
|
||||
|
||||
this.logger.log("JWT signing/verification keys re-derived from Vault");
|
||||
}
|
||||
}
|
||||
|
||||
64
apps/bff/src/vault-preload.ts
Normal file
64
apps/bff/src/vault-preload.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@ -97,6 +97,9 @@ importers:
|
||||
"@nestjs/core":
|
||||
specifier: ^11.1.12
|
||||
version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
"@nestjs/event-emitter":
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)
|
||||
"@nestjs/platform-express":
|
||||
specifier: ^11.1.12
|
||||
version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)
|
||||
@ -1880,6 +1883,15 @@ packages:
|
||||
"@nestjs/websockets":
|
||||
optional: true
|
||||
|
||||
"@nestjs/event-emitter@3.0.1":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==,
|
||||
}
|
||||
peerDependencies:
|
||||
"@nestjs/common": ^10.0.0 || ^11.0.0
|
||||
"@nestjs/core": ^10.0.0 || ^11.0.0
|
||||
|
||||
"@nestjs/mapped-types@2.1.0":
|
||||
resolution:
|
||||
{
|
||||
@ -4829,6 +4841,12 @@ packages:
|
||||
}
|
||||
engines: { node: ">= 0.6" }
|
||||
|
||||
eventemitter2@6.4.9:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==,
|
||||
}
|
||||
|
||||
eventemitter3@5.0.4:
|
||||
resolution:
|
||||
{
|
||||
@ -9444,6 +9462,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
"@nestjs/platform-express": 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)
|
||||
|
||||
"@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)":
|
||||
dependencies:
|
||||
"@nestjs/common": 11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
"@nestjs/core": 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
eventemitter2: 6.4.9
|
||||
|
||||
"@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)":
|
||||
dependencies:
|
||||
"@nestjs/common": 11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@ -11240,6 +11264,8 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
eventemitter2@6.4.9: {}
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
events-universal@1.0.1:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user