chore: add new csrf path and scheduler
This commit is contained in:
@@ -36,6 +36,7 @@ export let env = {
|
||||
jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid',
|
||||
jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids',
|
||||
csrfSecretPath: p.VAULT_CSRF_SECRET_PATH || 'cryptowallet/csrf',
|
||||
secretsRefreshMs: parseInt(p.VAULT_SECRETS_REFRESH_MS || '3600000', 10),
|
||||
},
|
||||
csrf: {
|
||||
cookieSecure: p.CSRF_COOKIE_SECURE === 'true',
|
||||
@@ -86,6 +87,7 @@ export function getVaultToken(): string | null {
|
||||
return vaultToken;
|
||||
}
|
||||
|
||||
|
||||
export async function initEnv(): Promise<void> {
|
||||
const { addr, roleId, secretId, mount, secretPath } = env.vault;
|
||||
|
||||
@@ -111,7 +113,7 @@ export async function initEnv(): Promise<void> {
|
||||
|
||||
logger.info('Loaded DB secrets from Vault');
|
||||
|
||||
const maybeCsrf = secrets.CSRF_SECRET_KEY;
|
||||
const maybeCsrf = secrets.CSRF_SECRET_KEY || secrets.key;
|
||||
if (maybeCsrf && maybeCsrf.length >= 32) {
|
||||
const mod = await import('../services/csrf.service');
|
||||
mod.setCsrfSigningKey(maybeCsrf);
|
||||
|
||||
@@ -4,8 +4,17 @@ import app from './app';
|
||||
import { env, initEnv, getVaultToken } from './config/env';
|
||||
import { loadJwtKeysFromVault } from './services/jwt.service';
|
||||
import { loadCsrfSecretFromVault, finalizeCsrfConfigFromEnv } from './services/csrf.service';
|
||||
import { startVaultSecretsScheduler, stopVaultSecretsScheduler } from './scheduler/vault-secrets.scheduler';
|
||||
import { logger } from './lib/logger';
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
stopVaultSecretsScheduler();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
stopVaultSecretsScheduler();
|
||||
});
|
||||
|
||||
async function main() {
|
||||
logger.info(`Wallet service instance started with id ${logger.instanceId}`);
|
||||
|
||||
@@ -37,6 +46,7 @@ async function main() {
|
||||
|
||||
app.listen(env.port, () => {
|
||||
logger.info(`Server running on port ${env.port}`);
|
||||
startVaultSecretsScheduler();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
59
apps/api/src/scheduler/vault-secrets.scheduler.ts
Normal file
59
apps/api/src/scheduler/vault-secrets.scheduler.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { env, getVaultToken } from '../config/env';
|
||||
import {
|
||||
loadCsrfSecretFromVault,
|
||||
loadCsrfSecretFromPrimaryVault,
|
||||
hasCsrfSigningKey,
|
||||
} from '../services/csrf.service';
|
||||
import { loadJwtKeysFromVault } from '../services/jwt.service';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
let intervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
|
||||
async function runVaultSecretsRefresh(): Promise<void> {
|
||||
if (!env.vault.addr || !env.vault.roleId || !env.vault.secretId) {
|
||||
return;
|
||||
}
|
||||
const token = getVaultToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const dedicatedOk = await loadCsrfSecretFromVault({ silent: true });
|
||||
if (!dedicatedOk) {
|
||||
const primaryOk = await loadCsrfSecretFromPrimaryVault({ silent: true });
|
||||
if (!primaryOk && hasCsrfSigningKey()) {
|
||||
logger.warn('Scheduled CSRF reload found no valid secret in Vault, keeping existing key');
|
||||
}
|
||||
}
|
||||
await loadJwtKeysFromVault(
|
||||
env.vault.addr,
|
||||
token,
|
||||
env.vault.mount,
|
||||
env.vault.jwtKidPath,
|
||||
env.vault.jwtKidsPrefix,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function startVaultSecretsScheduler(): void {
|
||||
if (intervalHandle) {
|
||||
return;
|
||||
}
|
||||
if (!env.vault.addr || !env.vault.roleId || !env.vault.secretId) {
|
||||
return;
|
||||
}
|
||||
const ms = env.vault.secretsRefreshMs;
|
||||
const period = Number.isFinite(ms) && ms >= 60000 ? ms : 3600000;
|
||||
intervalHandle = setInterval(() => {
|
||||
void runVaultSecretsRefresh();
|
||||
}, period);
|
||||
logger.info(`Vault secrets refresh scheduled every ${period}ms`);
|
||||
}
|
||||
|
||||
|
||||
export function stopVaultSecretsScheduler(): void {
|
||||
if (intervalHandle) {
|
||||
clearInterval(intervalHandle);
|
||||
intervalHandle = null;
|
||||
}
|
||||
}
|
||||
@@ -83,19 +83,39 @@ export function getCsrfCookieMaxAgeMs(): number {
|
||||
}
|
||||
|
||||
|
||||
export async function loadCsrfSecretFromVault(): Promise<boolean> {
|
||||
export async function loadCsrfSecretFromVault(opts?: { silent?: boolean }): Promise<boolean> {
|
||||
const silent = opts?.silent ?? false;
|
||||
const token = getVaultToken();
|
||||
if (!token || !env.vault.addr) {
|
||||
return false;
|
||||
}
|
||||
const data = await fetchVaultKV2(env.vault.addr, token, env.vault.mount, env.vault.csrfSecretPath);
|
||||
const key = data?.CSRF_SECRET_KEY;
|
||||
if (!key || key.length < 32) {
|
||||
logger.warn('Vault CSRF secret missing or too short');
|
||||
const secret = data?.CSRF_SECRET_KEY || data?.key;
|
||||
if (!secret || secret.length < 32) {
|
||||
if (!silent) {
|
||||
logger.warn('Vault CSRF secret missing or too short');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
setCsrfSigningKey(key);
|
||||
logger.info('CSRF signing key loaded from Vault');
|
||||
setCsrfSigningKey(secret);
|
||||
logger.info(silent ? 'CSRF signing key reloaded from Vault (dedicated path)' : 'CSRF signing key loaded from Vault');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export async function loadCsrfSecretFromPrimaryVault(opts?: { silent?: boolean }): Promise<boolean> {
|
||||
const silent = opts?.silent ?? false;
|
||||
const token = getVaultToken();
|
||||
if (!token || !env.vault.addr) {
|
||||
return false;
|
||||
}
|
||||
const data = await fetchVaultKV2(env.vault.addr, token, env.vault.mount, env.vault.secretPath);
|
||||
const secret = data?.CSRF_SECRET_KEY || data?.key;
|
||||
if (!secret || secret.length < 32) {
|
||||
return false;
|
||||
}
|
||||
setCsrfSigningKey(secret);
|
||||
logger.info(silent ? 'CSRF signing key reloaded from Vault (primary secret)' : 'CSRF signing key loaded from Vault (primary secret)');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export async function loadJwtKeysFromVault(
|
||||
return;
|
||||
}
|
||||
|
||||
const nextMap = new Map<string, Awaited<ReturnType<typeof jose.importSPKI>>>();
|
||||
|
||||
for (const kid of kids) {
|
||||
const kidSecret = await fetchVaultKV2(vaultAddr, vaultToken, mount, `${kidsPrefix}/${kid}`);
|
||||
if (!kidSecret?.public_key) {
|
||||
@@ -54,13 +56,25 @@ export async function loadJwtKeysFromVault(
|
||||
|
||||
try {
|
||||
const key = await jose.importSPKI(kidSecret.public_key, env.jwt.algorithm);
|
||||
keyMap.set(kid, key);
|
||||
nextMap.set(kid, key);
|
||||
logger.info(`Loaded JWT public key for kid=${kid}`);
|
||||
} catch (err: any) {
|
||||
logger.error(`Failed to import public key for kid=${kid}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMap.size === 0) {
|
||||
if (keyMap.size > 0) {
|
||||
logger.warn('JWT Vault reload produced no importable keys, keeping previous cache');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
keyMap.clear();
|
||||
for (const [k, v] of nextMap) {
|
||||
keyMap.set(k, v);
|
||||
}
|
||||
|
||||
logger.info(`JWT key store loaded: ${keyMap.size} key(s)`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user