diff --git a/ApiCore-Backend/.env.example b/ApiCore-Backend/.env.example index ddc4b12..f7bd3f7 100644 --- a/ApiCore-Backend/.env.example +++ b/ApiCore-Backend/.env.example @@ -2,7 +2,10 @@ DATABASE_URL="postgresql://user:password@localhost:5432/apicore?schema=public" -JWT_ACCESS_SECRET="change-me-access-secret" +# Mindestens 32 Zeichen (server-seitig erzwungen, siehe main.ts) - signiert +# jedes Access-/MFA-/SSO-Token UND verschluesselt gespeicherte TOTP-Secrets. +# Erzeugen z.B. mit: openssl rand -hex 32 +JWT_ACCESS_SECRET="change-me-to-a-random-value-at-least-32-characters-long" JWT_REFRESH_SECRET="change-me-refresh-secret" JWT_ACCESS_EXPIRES_IN="15m" JWT_REFRESH_EXPIRES_IN="7d" diff --git a/ApiCore-Backend/src/main.ts b/ApiCore-Backend/src/main.ts index c8116bd..4db0e53 100644 --- a/ApiCore-Backend/src/main.ts +++ b/ApiCore-Backend/src/main.ts @@ -5,6 +5,15 @@ import { AppModule } from './app.module'; import { configureApp } from './app.config'; const REQUIRED_ENV_VARS = ['DATABASE_URL', 'JWT_ACCESS_SECRET'] as const; +// JWT_ACCESS_SECRET is the root of trust for both authentication (every +// access/MFA-challenge/SSO-completion token is signed with it) and MFA +// secret confidentiality (mfa-crypto.util.ts derives the AES key that +// encrypts stored TOTP secrets from it) - a short or guessable value here +// would let an attacker forge tokens for any user AND decrypt everyone's +// stored TOTP secret, not just guess a password. 32 chars matches the +// output of "openssl rand -hex 32" / "-base64 32", the standard way to +// generate one. +const MIN_JWT_SECRET_LENGTH = 32; function assertRequiredEnvVars(): void { const missing = REQUIRED_ENV_VARS.filter((name) => !process.env[name]); @@ -13,6 +22,12 @@ function assertRequiredEnvVars(): void { `Missing required environment variable(s): ${missing.join(', ')}`, ); } + const jwtSecret = process.env.JWT_ACCESS_SECRET as string; + if (jwtSecret.length < MIN_JWT_SECRET_LENGTH) { + throw new Error( + `JWT_ACCESS_SECRET must be at least ${MIN_JWT_SECRET_LENGTH} characters (got ${jwtSecret.length}) - generate one with e.g. "openssl rand -hex 32"`, + ); + } } async function bootstrap() { diff --git a/deploy/k8s/backend/secret.example.yaml b/deploy/k8s/backend/secret.example.yaml index 82d1730..7af3e10 100644 --- a/deploy/k8s/backend/secret.example.yaml +++ b/deploy/k8s/backend/secret.example.yaml @@ -10,7 +10,10 @@ stringData: # Anderer Namespace als das Backend? Dann FQDN nutzen: # apicore-postgres..svc.cluster.local DATABASE_URL: "postgresql://apicore:change-me-postgres-password@apicore-postgres:5432/apicore?schema=public" - JWT_ACCESS_SECRET: "change-me-access-secret" + # Mindestens 32 Zeichen (server-seitig erzwungen, Start bricht sonst ab) - + # signiert jedes Access-/MFA-/SSO-Token UND verschluesselt gespeicherte + # TOTP-Secrets. Erzeugen z.B. mit: openssl rand -hex 32 + JWT_ACCESS_SECRET: "change-me-to-a-random-value-at-least-32-characters-long" JWT_REFRESH_SECRET: "change-me-refresh-secret" # Entra ID App-Registrierung (SSO). Siehe configmap.example.yaml # fuer OIDC_ISSUER_URL/OIDC_REDIRECT_URI.