chore: add new csrf path and scheduler

This commit is contained in:
2026-04-19 12:18:00 +03:00
parent 517df542e1
commit 08a38182c9
6 changed files with 114 additions and 8 deletions

View File

@@ -23,6 +23,7 @@ VAULT_SECRET_PATH=database
VAULT_JWT_KID_PATH=jwt/kid
VAULT_JWT_KIDS_PREFIX=jwt/kids
VAULT_CSRF_SECRET_PATH=cryptowallet/csrf
VAULT_SECRETS_REFRESH_MS=3600000
# CSRF (min 32 chars if not using Vault CSRF path)
CSRF_SECRET_KEY=change-me-to-at-least-32-chars-long!!

View File

@@ -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);

View File

@@ -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();
});
}

View 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;
}
}

View File

@@ -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) {
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;
}

View File

@@ -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)`);
}