fix: TRUST_PROXY-Kommentar aktualisieren und Modus beim Start loggen
Die Kommentare beschrieben noch einen einzelnen Reverse-Proxy-Hop. Sie beschreiben jetzt die Cloudflare/Traefik-Liste. Beim Start wird einmal auf info-Level geloggt, ob Hop-Zahl, Liste oder der Default aktiv ist (nur Anzahl, keine Adressen), damit eine fehlende Produktionseinstellung auffaellt. Das Standardverhalten bleibt unveraendert. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
886b4e75e1
commit
d9670dcda5
2 files changed
+102
-8
No files matched your search
@@ -0,0 +1,67 @@
|
|||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
|
import {
|
||||||
|
configureApp,
|
||||||
|
describeTrustProxy,
|
||||||
|
trustProxySetting,
|
||||||
|
} from './app.config';
|
||||||
|
|
||||||
|
describe('trustProxySetting', () => {
|
||||||
|
const original = process.env.TRUST_PROXY;
|
||||||
|
afterEach(() => {
|
||||||
|
if (original === undefined) delete process.env.TRUST_PROXY;
|
||||||
|
else process.env.TRUST_PROXY = original;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to one hop when unset or blank', () => {
|
||||||
|
delete process.env.TRUST_PROXY;
|
||||||
|
expect(trustProxySetting()).toBe(1);
|
||||||
|
process.env.TRUST_PROXY = ' ';
|
||||||
|
expect(trustProxySetting()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses a number as hop count and anything else as a list', () => {
|
||||||
|
process.env.TRUST_PROXY = '2';
|
||||||
|
expect(trustProxySetting()).toBe(2);
|
||||||
|
process.env.TRUST_PROXY = 'loopback, 10.42.0.0/16';
|
||||||
|
expect(trustProxySetting()).toBe('loopback, 10.42.0.0/16');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('describeTrustProxy', () => {
|
||||||
|
it('reports the hop count mode and the default fallback', () => {
|
||||||
|
expect(describeTrustProxy(1, false)).toBe(
|
||||||
|
'TRUST_PROXY not set, using default of 1 trusted hop',
|
||||||
|
);
|
||||||
|
expect(describeTrustProxy(2, true)).toBe('TRUST_PROXY: 2 trusted hop(s)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports the list mode with entry count but not the entries', () => {
|
||||||
|
const text = describeTrustProxy(
|
||||||
|
'loopback, 10.42.0.0/16, 2400:cb00::/32',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(text).toBe('TRUST_PROXY: list of 3 trusted proxy range(s)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('configureApp trust proxy logging', () => {
|
||||||
|
it('logs the active mode once at startup', () => {
|
||||||
|
const log = jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
||||||
|
const app = {
|
||||||
|
set: jest.fn(),
|
||||||
|
useBodyParser: jest.fn(),
|
||||||
|
use: jest.fn(),
|
||||||
|
useGlobalFilters: jest.fn(),
|
||||||
|
enableCors: jest.fn(),
|
||||||
|
setGlobalPrefix: jest.fn(),
|
||||||
|
enableVersioning: jest.fn(),
|
||||||
|
useGlobalPipes: jest.fn(),
|
||||||
|
};
|
||||||
|
configureApp(app as unknown as NestExpressApplication);
|
||||||
|
expect(
|
||||||
|
log.mock.calls.filter(([m]) => String(m).startsWith('TRUST_PROXY')),
|
||||||
|
).toHaveLength(1);
|
||||||
|
log.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { RequestMethod, ValidationPipe, VersioningType } from '@nestjs/common';
|
import {
|
||||||
|
Logger,
|
||||||
|
RequestMethod,
|
||||||
|
ValidationPipe,
|
||||||
|
VersioningType,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
@@ -7,11 +12,15 @@ import { requestLoggerMiddleware } from './common/middleware/request-logger.midd
|
|||||||
import { noStoreMiddleware } from './common/middleware/no-store.middleware';
|
import { noStoreMiddleware } from './common/middleware/no-store.middleware';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many reverse-proxy hops to trust for X-Forwarded-For. It decides the
|
* Which proxies to trust for X-Forwarded-For. It decides the client IP behind
|
||||||
* client IP behind every rate limit and API-key IP allowlist: too few and
|
* every rate limit and API-key IP allowlist: trust too little and all users
|
||||||
* all users share the proxy's IP, too many and a client can forge its own.
|
* share the proxy's IP, trust too much and a client can forge its own.
|
||||||
* A hop count ("1", the single K8s ingress) or Express' list syntax
|
*
|
||||||
* ("loopback, 10.0.0.0/8").
|
* Production runs client -> Cloudflare -> Traefik -> backend, so the hop
|
||||||
|
* count is not stable; TRUST_PROXY is an Express list there ("loopback,
|
||||||
|
* 10.42.0.0/16, <Cloudflare IPv4+IPv6 ranges>", see
|
||||||
|
* deploy/k8s/backend/configmap.example.yaml). A plain number ("1") is a hop
|
||||||
|
* count and stays the default when the variable is unset.
|
||||||
*/
|
*/
|
||||||
export function trustProxySetting(): number | string {
|
export function trustProxySetting(): number | string {
|
||||||
const raw = process.env.TRUST_PROXY?.trim();
|
const raw = process.env.TRUST_PROXY?.trim();
|
||||||
@@ -21,11 +30,29 @@ export function trustProxySetting(): number | string {
|
|||||||
return /^\d+$/.test(raw) ? Number(raw) : raw;
|
return /^\d+$/.test(raw) ? Number(raw) : raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Startup log line for the active mode; never includes the addresses. */
|
||||||
|
export function describeTrustProxy(
|
||||||
|
setting: number | string,
|
||||||
|
isExplicit: boolean,
|
||||||
|
): string {
|
||||||
|
if (typeof setting === 'number') {
|
||||||
|
return isExplicit
|
||||||
|
? `TRUST_PROXY: ${setting} trusted hop(s)`
|
||||||
|
: `TRUST_PROXY not set, using default of ${setting} trusted hop`;
|
||||||
|
}
|
||||||
|
const entries = setting.split(',').filter((e) => e.trim()).length;
|
||||||
|
return `TRUST_PROXY: list of ${entries} trusted proxy range(s)`;
|
||||||
|
}
|
||||||
|
|
||||||
export function configureApp(app: NestExpressApplication): void {
|
export function configureApp(app: NestExpressApplication): void {
|
||||||
// Trust the single reverse-proxy hop (K8s ingress) so req.ip reflects the
|
// Trust the configured proxies (Traefik, Cloudflare) so req.ip reflects the
|
||||||
// real client IP from X-Forwarded-For — required for per-client throttling
|
// real client IP from X-Forwarded-For — required for per-client throttling
|
||||||
// to work correctly rather than rate-limiting all traffic together.
|
// to work correctly rather than rate-limiting all traffic together.
|
||||||
app.set('trust proxy', trustProxySetting());
|
const trustProxy = trustProxySetting();
|
||||||
|
app.set('trust proxy', trustProxy);
|
||||||
|
new Logger('Bootstrap').log(
|
||||||
|
describeTrustProxy(trustProxy, !!process.env.TRUST_PROXY?.trim()),
|
||||||
|
);
|
||||||
// No ETags: they let caches and proxies revalidate (and keep) personal
|
// No ETags: they let caches and proxies revalidate (and keep) personal
|
||||||
// responses that are meant to be no-store.
|
// responses that are meant to be no-store.
|
||||||
app.set('etag', false);
|
app.set('etag', false);
|
||||||
|
|||||||
Reference in new issue
Block a user