fix: remove JWT_SECRET env var — server manages it exclusively
Setting JWT_SECRET via environment variable was broken by design: the admin panel rotation updates the in-memory binding and persists the new value to data/.jwt_secret, but an env var would silently override it on the next restart, reverting the rotation. The server now always loads JWT_SECRET from data/.jwt_secret (auto-generating it on first start), making the file the single source of truth. Rotation is handled exclusively through the admin panel. - config.ts: drop process.env.JWT_SECRET fallback and JWT_SECRET_IS_GENERATED export; always read from / write to data/.jwt_secret - index.ts: remove the now-obsolete JWT_SECRET startup warning - .env.example, docker-compose.yml, README: remove JWT_SECRET entries - Helm chart: remove JWT_SECRET from secretEnv, secret.yaml, and deployment.yaml; rename generateJwtSecret → generateEncryptionKey and update NOTES.txt and README accordingly
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
PORT=3001 # Port to run the server on
|
||||
NODE_ENV=development # development = development mode; production = production mode
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production # Auto-generated if not set; persist across restarts for stable sessions
|
||||
# ENCRYPTION_KEY=<random-256-bit-hex> # Separate key for encrypting stored secrets (API keys, MFA, SMTP, OIDC, etc.)
|
||||
# Auto-generated and persisted to ./data/.encryption_key if not set.
|
||||
# Upgrade: set to your old JWT_SECRET value if you have existing encrypted data from a previous installation.
|
||||
# Upgrade from a version that used JWT_SECRET for encryption: set to your old JWT_SECRET value so
|
||||
# existing encrypted data remains readable, then re-save credentials via the admin panel.
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
TZ=UTC # Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin)
|
||||
LOG_LEVEL=info # info = concise user actions; debug = verbose admin-level details
|
||||
|
||||
@@ -2,29 +2,28 @@ import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
let _jwtSecret: string = process.env.JWT_SECRET || '';
|
||||
const dataDir = path.resolve(__dirname, '../data');
|
||||
|
||||
if (!_jwtSecret) {
|
||||
const dataDir = path.resolve(__dirname, '../data');
|
||||
const secretFile = path.join(dataDir, '.jwt_secret');
|
||||
// JWT_SECRET is always managed by the server — auto-generated on first start and
|
||||
// persisted to data/.jwt_secret. Use the admin panel to rotate it; do not set it
|
||||
// via environment variable (env var would override a rotation on next restart).
|
||||
const jwtSecretFile = path.join(dataDir, '.jwt_secret');
|
||||
let _jwtSecret: string;
|
||||
|
||||
try {
|
||||
_jwtSecret = fs.readFileSync(jwtSecretFile, 'utf8').trim();
|
||||
} catch {
|
||||
_jwtSecret = crypto.randomBytes(32).toString('hex');
|
||||
try {
|
||||
_jwtSecret = fs.readFileSync(secretFile, 'utf8').trim();
|
||||
} catch {
|
||||
_jwtSecret = crypto.randomBytes(32).toString('hex');
|
||||
try {
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(secretFile, _jwtSecret, { mode: 0o600 });
|
||||
console.log('Generated and saved JWT secret to', secretFile);
|
||||
} catch (writeErr: unknown) {
|
||||
console.warn('WARNING: Could not persist JWT secret to disk:', writeErr instanceof Error ? writeErr.message : writeErr);
|
||||
console.warn('Sessions will reset on server restart. Set JWT_SECRET env var for persistent sessions.');
|
||||
}
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(jwtSecretFile, _jwtSecret, { mode: 0o600 });
|
||||
console.log('Generated and saved JWT secret to', jwtSecretFile);
|
||||
} catch (writeErr: unknown) {
|
||||
console.warn('WARNING: Could not persist JWT secret to disk:', writeErr instanceof Error ? writeErr.message : writeErr);
|
||||
console.warn('Sessions will reset on server restart.');
|
||||
}
|
||||
}
|
||||
|
||||
const JWT_SECRET_IS_GENERATED = !process.env.JWT_SECRET;
|
||||
|
||||
// export let so TypeScript's CJS output keeps exports.JWT_SECRET live
|
||||
// (generates `exports.JWT_SECRET = JWT_SECRET = newVal` inside updateJwtSecret)
|
||||
export let JWT_SECRET = _jwtSecret;
|
||||
@@ -48,7 +47,6 @@ export function updateJwtSecret(newSecret: string): void {
|
||||
let ENCRYPTION_KEY: string = process.env.ENCRYPTION_KEY || '';
|
||||
|
||||
if (!ENCRYPTION_KEY) {
|
||||
const dataDir = path.resolve(__dirname, '../data');
|
||||
const keyFile = path.join(dataDir, '.encryption_key');
|
||||
|
||||
try {
|
||||
@@ -66,4 +64,4 @@ if (!ENCRYPTION_KEY) {
|
||||
}
|
||||
}
|
||||
|
||||
export { JWT_SECRET_IS_GENERATED, ENCRYPTION_KEY };
|
||||
export { ENCRYPTION_KEY };
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dotenv/config';
|
||||
import { JWT_SECRET_IS_GENERATED } from './config';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import { enforceGlobalMfaPolicy } from './middleware/mfaPolicy';
|
||||
import cors from 'cors';
|
||||
@@ -268,9 +267,6 @@ const server = app.listen(PORT, () => {
|
||||
'──────────────────────────────────────',
|
||||
];
|
||||
banner.forEach(l => console.log(l));
|
||||
if (JWT_SECRET_IS_GENERATED) {
|
||||
sLogWarn('[SECURITY WARNING] JWT_SECRET was auto-generated. Sessions will not persist across restarts. Set JWT_SECRET env var for production use.');
|
||||
}
|
||||
if (process.env.DEMO_MODE === 'true') sLogInfo('Demo mode: ENABLED');
|
||||
if (process.env.DEMO_MODE === 'true' && process.env.NODE_ENV === 'production') {
|
||||
sLogWarn('SECURITY WARNING: DEMO_MODE is enabled in production!');
|
||||
|
||||
Reference in New Issue
Block a user