From e55a7f1f3748ce3962a0736cc33e21a9161f2062 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 8 May 2026 16:57:41 +0700 Subject: [PATCH] Load backend secrets from Vault --- Dockerfile | 1 - src/config.ts | 4 +++ src/vault/env.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/vault/env.ts diff --git a/Dockerfile b/Dockerfile index 74a3530..b84fe7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,6 @@ RUN npm ci FROM node:22-alpine AS build WORKDIR /app -ENV DATABASE_URL="postgresql://mapflow:mapflow@localhost:5432/mapflow" COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run prisma:generate diff --git a/src/config.ts b/src/config.ts index b66d960..51117db 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,9 @@ import 'dotenv/config'; +import { loadVaultEnvironment } from './vault/env.js'; + +await loadVaultEnvironment(); + export const config = { host: process.env.HOST ?? '0.0.0.0', port: Number(process.env.PORT ?? '4000'), diff --git a/src/vault/env.ts b/src/vault/env.ts new file mode 100644 index 0000000..0db6ad0 --- /dev/null +++ b/src/vault/env.ts @@ -0,0 +1,65 @@ +type VaultConfig = { + address: string; + token: string; + mount: string; + sharedPath: string; + projectPath: string; +}; + +function requireEnv(name: string) { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} is required when VAULT_ENABLED=true.`); + } + return value; +} + +function vaultConfig(): VaultConfig { + return { + address: requireEnv('VAULT_ADDR').replace(/\/$/, ''), + token: requireEnv('VAULT_TOKEN'), + mount: requireEnv('VAULT_KV_MOUNT'), + sharedPath: requireEnv('VAULT_SHARED_PATH'), + projectPath: requireEnv('VAULT_PROJECT_PATH'), + }; +} + +async function readVaultPath(config: VaultConfig, path: string) { + const response = await fetch( + `${config.address}/v1/${config.mount}/data/${path}`, + { headers: { 'X-Vault-Token': config.token } }, + ); + + if (!response.ok) { + throw new Error(`Vault read failed for ${path}: ${response.status}.`); + } + + const payload = (await response.json()) as { + data?: { data?: Record }; + }; + const data = payload.data?.data; + if (!data) { + throw new Error(`Vault path ${path} has no KV v2 data.`); + } + + return data; +} + +function applyEnvironment(values: Record) { + for (const [key, value] of Object.entries(values)) { + if (typeof value !== 'string') { + throw new Error(`Vault value ${key} must be a string.`); + } + process.env[key] = value; + } +} + +export async function loadVaultEnvironment() { + if (process.env.VAULT_ENABLED !== 'true') { + return; + } + + const config = vaultConfig(); + applyEnvironment(await readVaultPath(config, config.sharedPath)); + applyEnvironment(await readVaultPath(config, config.projectPath)); +}