Commit 187f7819 authored by Kourser's avatar Kourser
Browse files

Démarrage : ne plus écarter SECRETS_KEY en silence

En dessous de 32 caractères, SECRETS_KEY était ignorée et le chiffrement
des secrets stockés retombait sur une clef dérivée de JWT_SECRET — sans
un mot. L'exploitant croit alors avoir séparé ses deux clefs. Il ne l'a
pas fait, et il le découvrira le jour où il fera tourner JWT_SECRET :
ses secrets deviendront illisibles, précisément ce que cette variable
existe pour éviter.

Un réglage de sécurité posé puis écarté en silence est pire que pas de
réglage du tout : il donne une assurance fausse. Le motif est donc
signalé au démarrage, et la production refuse de démarrer — même
traitement que JWT_SECRET.

TRUSTED_PROXIES reçoit la même attention, en plus modeste : une valeur
illisible ne se confond plus avec une variable absente. L'avertissement
existant demandait de renseigner ce que l'exploitant croyait avoir
renseigné.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 20bf83ac
Loading
Loading
Loading
Loading
+25 −1
Changes for apps/api/src/common/secret-box.spec.ts: 25 added lines, 1 removed line.
Original line number Diff line number Diff line
import { openSecret, sealSecret, SecretUnreadableError } from './secret-box';
import { openSecret, sealSecret, SecretUnreadableError, secretsKeyIssue } from './secret-box';

describe('secret-box', () => {
  const OLD = process.env.SECRETS_KEY;
@@ -47,3 +47,27 @@ describe('secret-box', () => {
    expect(() => openSecret(sealed)).toThrow(SecretUnreadableError);
  });
});

describe('secretsKeyIssue', () => {
  const original = process.env.SECRETS_KEY;
  afterEach(() => {
    if (original === undefined) delete process.env.SECRETS_KEY;
    else process.env.SECRETS_KEY = original;
  });

  it('ne signale rien quand la variable est absente', () => {
    delete process.env.SECRETS_KEY;
    expect(secretsKeyIssue()).toBeNull();
  });

  it('ne signale rien quand la clef est assez longue', () => {
    process.env.SECRETS_KEY = 'x'.repeat(32);
    expect(secretsKeyIssue()).toBeNull();
  });

  it('signale une clef posée mais trop courte, qui serait écartée en silence', () => {
    process.env.SECRETS_KEY = 'trop-courte';
    // Sans ce signalement, l'exploitant croit avoir séparé ses deux clefs.
    expect(secretsKeyIssue()).toMatch(/ignoree/);
  });
});
+21 −0
Changes for apps/api/src/common/secret-box.ts: 21 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -41,6 +41,27 @@ export class SecretUnreadableError extends Error {
/** Meme seuil que le secret de signature : 32 caracteres. Cf. `auth.module.ts`. */
const MIN_SOURCE_LENGTH = 32;

/**
 * `SECRETS_KEY` est-elle renseignee mais inutilisable ?
 *
 * Elle est alors ECARTEE au profit de `JWT_SECRET`, et l'exploitant n'en sait
 * rien : il croit avoir separe les deux clefs, et decouvrira le contraire le jour
 * ou il fera tourner `JWT_SECRET` — ses secrets stockes deviendront illisibles,
 * precisement ce que cette variable existe pour eviter.
 *
 * Rend le motif a signaler, ou `null` si tout va bien.
 */
export function secretsKeyIssue(): string | null {
  const explicit = process.env.SECRETS_KEY;
  if (!explicit) return null;
  if (explicit.length >= MIN_SOURCE_LENGTH) return null;
  return (
    `SECRETS_KEY est renseignee mais trop courte (${explicit.length} caracteres, ` +
    `${MIN_SOURCE_LENGTH} minimum) : elle est ignoree, et les secrets stockes sont ` +
    'chiffres avec une clef derivee de JWT_SECRET.'
  );
}

function sourceKey(): string {
  const explicit = process.env.SECRETS_KEY;
  if (explicit && explicit.length >= MIN_SOURCE_LENGTH) return explicit;
+32 −0
Changes for apps/api/src/main.ts: 32 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ import type { NestExpressApplication } from '@nestjs/platform-express';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import { securityHeaders } from './common/security-headers';
import { secretsKeyIssue } from './common/secret-box';
import { API_PREFIX } from './common/routes';

/**
@@ -25,6 +26,14 @@ function trustedProxies(): number {
  return Number.isInteger(raw) && raw >= 0 ? raw : 0;
}

/** `TRUSTED_PROXIES` renseignee mais inexploitable : le repli a 0 doit se voir. */
function trustedProxiesInvalide(): boolean {
  const brut = process.env.TRUSTED_PROXIES;
  if (brut === undefined || brut.trim() === '') return false;
  const n = Number(brut);
  return !Number.isInteger(n) || n < 0;
}

async function bootstrap(): Promise<void> {
  /*
   * Type Express explicite : `app.set('trust proxy', ...)` n'existe que sur
@@ -38,6 +47,29 @@ async function bootstrap(): Promise<void> {
   */
  const proxies = trustedProxies();
  app.set('trust proxy', proxies);
  /*
   * Une valeur illisible ne doit PAS se confondre avec une variable absente :
   * l'exploitant qui a ecrit « TRUSTED_PROXIES=oui » lirait sinon un message lui
   * demandant de renseigner ce qu'il croit avoir renseigne.
   */
  if (trustedProxiesInvalide()) {
    new Logger('Bootstrap').warn(
      `TRUSTED_PROXIES=« ${process.env.TRUSTED_PROXIES} » n'est pas un entier positif : ` +
        'valeur ignoree, aucun proxy considere comme de confiance.',
    );
  }

  /*
   * Un reglage de securite pose et silencieusement ecarte est pire que pas de
   * reglage du tout : il donne une assurance fausse. En production on refuse de
   * demarrer, comme pour JWT_SECRET.
   */
  const secrets = secretsKeyIssue();
  if (secrets) {
    if (process.env.NODE_ENV === 'production') throw new Error(secrets);
    new Logger('Bootstrap').warn(secrets);
  }

  if (proxies === 0 && process.env.NODE_ENV === 'production') {
    new Logger('Bootstrap').warn(
      'TRUSTED_PROXIES=0 : les adresses clientes sont lues sur la connexion directe. ' +