Kurs NestJS · Moduł 9: Deployment i infrastruktura
SSL/TLS i HTTPS Configuration - tarcza obronna Imperium
W tej lekcji6
Legionisto cyberbezpieczeństwa! Hasło logującego się legionisty leci przez kawiarniane Wi-Fi jawnym tekstem, a obca strona po cichu wysyła żądania w jego imieniu. Architekt Vitruvius wie, że każda prowincja potrzebuje fortyfikacji: jak mury chroniły Rzym przed najeźdźcami, tak TLS i zabezpieczenia HTTP chronią aplikację NestJS przed atakami z sieci.
SSL (Secure Sockets Layer) i jego następca TLS (Transport Layer Security) to protokoły kryptograficzne zapewniające bezpieczną komunikację. SSL 2.0 i 3.0 są dziś zakazane jako niebezpieczne, a używa się TLS 1.2 i 1.3 - nazwa „certyfikat SSL” została z przyzwyczajenia. HTTPS to HTTP działający przez TLS, który szyfruje dane między klientem a serwerem. Zbudujemy cztery pierścienie murów, od transportu do aplikacji: TLS szyfruje drogę, Helmet ustawia nagłówki bezpieczeństwa, CORS decyduje, które strony mogą wołać API, a rate limiting liczy żądania klientów.
Generowanie certyfikatów SSL
Pierwszym krokiem jest pozyskanie certyfikatów. W środowisku deweloperskim możemy wygenerować certyfikaty samopodpisane, a na produkcji korzystamy z Let's Encrypt lub komercyjnych dostawców.
Certyfikaty deweloperskie (self-signed)
Skrypt wywołuje openssl przez execSync i zapisuje klucz oraz certyfikat w katalogu certs:
1// scripts/generate-dev-cert.ts
2import { execSync } from 'node:child_process';
3import * as fs from 'node:fs';
4import * as path from 'node:path';
5
6// Katalog liczony od katalogu projektu - w modułach ES nie ma __dirname
7const certsDir = path.join(process.cwd(), 'certs');
8
9// Tworzenie katalogu na certyfikaty
10if (!fs.existsSync(certsDir)) {
11 fs.mkdirSync(certsDir, { recursive: true });
12}
13
14// Generowanie klucza prywatnego i certyfikatu z nazwami w subjectAltName
15execSync(`openssl req -x509 -newkey rsa:4096 \
16 -keyout ${certsDir}/key.pem \
17 -out ${certsDir}/cert.pem \
18 -days 365 -nodes \
19 -subj "/C=PL/ST=Roma/L=Roma/O=Imperium/CN=localhost" \
20 -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"`);
21
22console.log('Certyfikaty deweloperskie wygenerowane w katalogu certs/');-nodes zostawia klucz bez hasła (OpenSSL 3 nazywa tę opcję -noenc, ale stara nazwa działa też w LibreSSL na macOS). -addext dopisuje nazwy w polu subjectAltName: przeglądarki od lat ignorują samo CN, więc certyfikat bez niego odrzucą. Katalog liczymy od process.cwd(), bo nowy projekt NestJS 12 jest modułem ES, w którym __dirname nie istnieje.
Konfiguracja HTTPS w NestJS
NestFactory.create() przyjmuje httpsOptions z kluczem i certyfikatem:
1// src/main.ts - uruchomienie serwera z HTTPS
2import { NestFactory } from '@nestjs/core';
3import { NestExpressApplication } from '@nestjs/platform-express';
4import { AppModule } from './app.module';
5import * as fs from 'node:fs';
6import * as path from 'node:path';
7
8async function bootstrap() {
9 const isProduction = process.env.NODE_ENV === 'production';
10
11 // Opcje HTTPS - certyfikaty SSL
12 const httpsOptions = isProduction
13 ? {
14 key: fs.readFileSync(process.env.SSL_KEY_PATH),
15 cert: fs.readFileSync(process.env.SSL_CERT_PATH),
16 ca: fs.readFileSync(process.env.SSL_CA_PATH), // Chain certyfikatów
17 }
18 : {
19 key: fs.readFileSync(path.join(process.cwd(), 'certs', 'key.pem')),
20 cert: fs.readFileSync(path.join(process.cwd(), 'certs', 'cert.pem')),
21 };
22
23 const app = await NestFactory.create<NestExpressApplication>(AppModule, { httpsOptions });
24
25 // Za reverse proxy: ufaj nagłówkom X-Forwarded-* od pierwszego pośrednika
26 app.set('trust proxy', 1);
27
28 // Wymuszenie przekierowania HTTP -> HTTPS
29 if (isProduction) {
30 app.use((req, res, next) => {
31 if (!req.secure) {
32 // Adres z konfiguracji, a nie z nagłówka Host, który ustawia klient
33 return res.redirect(301, `https://${process.env.PUBLIC_HOST}${req.originalUrl}`);
34 }
35 next();
36 });
37 }
38
39 await app.listen(process.env.PORT || 3000);
40}
41bootstrap();TLS kończy się albo w samym Node.js (httpsOptions), albo w reverse proxy, które przekazuje ruch zwykłym HTTP z nagłówkiem X-Forwarded-Proto. trust proxy każe Expressowi ufać temu nagłówkowi, więc req.secure działa w obu modelach. Pierwsza wersja sprawdzała sam nagłówek, a bez proxy go nie ma - każde żądanie dostawało przekierowanie i powstawała pętla. Adres docelowy bierzemy z konfiguracji, bo nagłówek Host ustawia klient.
Helmet.js - nagłówki bezpieczeństwa HTTP
Helmet to jak hełm centuriona - chroni głowę, czyli nagłówki HTTP, przed najczęstszymi atakami:
1// src/main.ts - konfiguracja Helmet
2import helmet from 'helmet';
3
4async function bootstrap() {
5 const app = await NestFactory.create(AppModule);
6
7 // Podstawowa konfiguracja Helmet - wystarcza większości API:
8 // app.use(helmet());
9
10 // Zaawansowana konfiguracja Helmet (zamiast podstawowej)
11 app.use(helmet({
12 // Content-Security-Policy - kontroluje skąd ładowane są zasoby
13 contentSecurityPolicy: {
14 directives: {
15 defaultSrc: ["'self'"],
16 scriptSrc: ["'self'"],
17 styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
18 fontSrc: ["'self'", 'https://fonts.gstatic.com'],
19 imgSrc: ["'self'", 'data:', 'https:'],
20 connectSrc: ["'self'", ...(process.env.API_URL ? [process.env.API_URL] : [])],
21 frameSrc: ["'none'"],
22 objectSrc: ["'none'"],
23 },
24 },
25 // Strict-Transport-Security - wymusza HTTPS
26 strictTransportSecurity: {
27 maxAge: 31536000, // 1 rok
28 includeSubDomains: true,
29 preload: true,
30 },
31 // X-Frame-Options - zapobiega clickjacking
32 xFrameOptions: { action: 'deny' },
33 // X-Content-Type-Options - zapobiega MIME sniffing
34 xContentTypeOptions: true,
35 // Referrer-Policy
36 referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
37 }));
38}Nazwy opcji pochodzą z Helmet 8; dawne hsts, frameguard i noSniff to przestarzałe aliasy. Z scriptSrc zniknęło 'unsafe-inline', które wyłączało ochronę CSP przed XSS. preload wpisuje domenę na listę HSTS w przeglądarkach i trudno to cofnąć, więc włączaj go świadomie. Od NestJS 12.1 te same domyślne nagłówki ustawia wbudowane app.useSecurityHeaders().
CORS dla produkcji
CORS (Cross-Origin Resource Sharing) kontroluje, które domeny mogą komunikować się z naszym API. To jak lista zaufanych ambasadorów Imperium:
1// src/main.ts - produkcyjna konfiguracja CORS
2import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
3
4const corsOptions: CorsOptions = {
5 origin: (origin, callback) => {
6 const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || [];
7
8 // Pozwól na requesty bez origin (mobile, Postman)
9 if (!origin) return callback(null, true);
10
11 if (allowedOrigins.includes(origin)) {
12 callback(null, true);
13 } else {
14 // false = brak nagłówków CORS; przeglądarka sama zablokuje odpowiedź
15 callback(null, false);
16 }
17 },
18 methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
19 allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
20 exposedHeaders: ['X-Total-Count', 'X-Page-Count'],
21 credentials: true,
22 maxAge: 86400, // Cache preflight na 24h (Chromium skraca do 2h)
23};
24
25app.enableCors(corsOptions);Obca domena dostaje po prostu brak nagłówków CORS; pierwsza wersja zwracała błąd, co kończyło się odpowiedzią 500 i śmieciami w logach. CORS chroni tylko użytkowników przeglądarek - Postman czy aplikacja mobilna go ignorują, więc nie zastąpi autoryzacji.
Rate Limiting z @nestjs/throttler
Rate limiting to strażnicy przy bramach, którzy liczą, ile osób wchodzi w danym czasie:
1// src/app.module.ts - konfiguracja ThrottlerModule
2import { Module } from '@nestjs/common';
3import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
4import { APP_GUARD } from '@nestjs/core';
5
6@Module({
7 imports: [
8 ThrottlerModule.forRoot([
9 {
10 name: 'short', // Krótkoterminowy limit
11 ttl: 1000, // 1 sekunda
12 limit: 3, // Max 3 requesty na sekundę
13 },
14 {
15 name: 'medium', // Średni limit
16 ttl: 10000, // 10 sekund
17 limit: 20, // Max 20 requestów na 10 sekund
18 },
19 {
20 name: 'long', // Długoterminowy limit
21 ttl: 60000, // 1 minuta
22 limit: 100, // Max 100 requestów na minutę
23 },
24 ]),
25 ],
26 providers: [
27 {
28 provide: APP_GUARD,
29 useClass: ThrottlerGuard,
30 },
31 ],
32})
33export class AppModule {}ttl podajemy w milisekundach, a wszystkie trzy limity obowiązują jednocześnie. Throttler chroni przed brute force i nadużyciami API, ale nie przed prawdziwym DDoS, który zatyka łącze, zanim ruch dotrze do Node.js. Za proxy ustaw trust proxy, a przy kilku instancjach użyj wspólnego magazynu w Redisie.
Dekoratory zmieniają limity dla pojedynczych endpointów:
1// Wyłączenie rate limitingu dla konkretnych endpointów
2import { SkipThrottle, Throttle } from '@nestjs/throttler';
3
4@Controller('legions')
5export class LegionsController {
6 @SkipThrottle({ short: true, medium: true, long: true }) // Bez limitu dla health check
7 @Get('health')
8 healthCheck() {
9 return { status: 'ok' };
10 }
11
12 @Throttle({ short: { ttl: 1000, limit: 1 } }) // Surowszy limit
13 @Post('login')
14 login() {
15 // Logowanie z ograniczeniem 1 req/s
16 }
17}Przy nazwanych limitach @SkipThrottle() bez argumentów nie wyłącza żadnego z nich - w teście health check dostał 429 po trzech żądaniach, dlatego podajemy nazwy. @Throttle zmienia tylko short; medium i long nadal działają.
Ochrona CSRF
CSRF (Cross-Site Request Forgery) to atak, w którym złośliwa strona wykonuje żądania w imieniu zalogowanego użytkownika. Dotyczy logowania ciasteczkami; token JWT w nagłówku Authorization przeglądarka sama nie dołącza. Starsze projekty chroniły się pakietem csurf:
1// src/main.ts - ochrona CSRF z csurf (pakiet przestarzały, tylko dla starych projektów)
2import csurf from 'csurf';
3import cookieParser from 'cookie-parser';
4
5async function bootstrap() {
6 const app = await NestFactory.create(AppModule);
7
8 // csurf z opcją cookie wymaga wcześniej cookie-parser
9 app.use(cookieParser());
10
11 // CSRF protection (dla aplikacji z sesją/cookies)
12 app.use(csurf({
13 cookie: {
14 httpOnly: true,
15 secure: process.env.NODE_ENV === 'production',
16 sameSite: 'strict',
17 },
18 }));
19
20 // Middleware ustawiający CSRF token w odpowiedzi
21 app.use((req, res, next) => {
22 res.cookie('XSRF-TOKEN', req.csrfToken(), {
23 httpOnly: false, // Frontend musi móc odczytać
24 secure: process.env.NODE_ENV === 'production',
25 sameSite: 'strict',
26 });
27 next();
28 });
29}csurf jest zarchiwizowany i oznaczony jako przestarzały. Wymaga też cookie-parser przed sobą - bez niego w teście każde żądanie kończyło się błędem 500. NestJS 12.1 ma ochronę wbudowaną:
1// src/main.ts - wbudowana ochrona CSRF (NestJS 12.1+)
2async function bootstrap() {
3 const app = await NestFactory.create(AppModule);
4
5 app.enableCsrfProtection({
6 // Frontend z innej domeny, który może wysyłać POST-y z ciasteczkami
7 trustedOrigins: ['https://admin.imperium.rome'],
8 });
9
10 await app.listen(3000);
11}enableCsrfProtection() sprawdza nagłówek Sec-Fetch-Site, który przeglądarki dołączają do żądań. W teście POST z obcej strony dostał 403, a GET, żądania bez nagłówków przeglądarki i zaufane domeny przeszły. Tokeny, np. z pakietu csrf-csrf, dokumentacja NestJS zostawia dla bardzo starych przeglądarek.
Kompletna konfiguracja bezpieczeństwa
Middleware z Helmet można też podpiąć w module:
1// src/security/security.module.ts
2import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
3import { ThrottlerModule } from '@nestjs/throttler';
4import helmet from 'helmet';
5
6@Module({
7 imports: [
8 ThrottlerModule.forRoot([
9 { name: 'short', ttl: 1000, limit: 3 },
10 { name: 'long', ttl: 60000, limit: 100 },
11 ]),
12 ],
13})
14export class SecurityModule implements NestModule {
15 configure(consumer: MiddlewareConsumer) {
16 consumer
17 .apply(
18 helmet(),
19 helmet.contentSecurityPolicy({
20 directives: {
21 defaultSrc: ["'self'"],
22 scriptSrc: ["'self'"],
23 },
24 }),
25 )
26 .forRoutes('*');
27 }
28}helmet() ustawia już domyślne CSP, a drugie wywołanie je nadpisuje - wystarczyłoby jedno z opcją contentSecurityPolicy.
Bezpieczeństwo to nie jednorazowe zadanie, a ciągły proces - jak obrona murów, która wymagała stałej czujności strażników. Każda warstwa (TLS, Helmet, CORS, rate limiting, CSRF) dodaje kolejny mur. Polecam Ci zaczynać od domyślnych ustawień Helmet i dokładać wyjątki dopiero wtedy, gdy coś przestaje działać. W następnej lekcji zbudujemy pipeline CI/CD, który sprawdzi te mury przy każdym wdrożeniu.
Pamiętaj: jeden mur nigdy nie wystarcza - Imperium broni się pierścieniami, od drogi po bramę.
Kod do tej lekcji: src/security-config.ts
1// SSL/TLS i HTTPS Configuration - Tarcza Obronna Imperium
2import { NestFactory } from '@nestjs/core';
3import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
4import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
5import { APP_GUARD } from '@nestjs/core';
6import helmet from 'helmet';
7import * as fs from 'fs';
8
9// 1. HTTPS Configuration
10async function bootstrap() {
11 const httpsOptions = {
12 key: fs.readFileSync(process.env.SSL_KEY_PATH),
13 cert: fs.readFileSync(process.env.SSL_CERT_PATH),
14 };
15
16 // TODO: Utworz aplikacje z httpsOptions
17 // const app = await NestFactory.create(AppModule, { httpsOptions });
18
19 // 2. Helmet - naglowki bezpieczenstwa
20 // app.use(helmet({
21 // contentSecurityPolicy: {
22 // directives: {
23 // defaultSrc: ["'self'"],
24 // scriptSrc: ["'self'"],
25 // },
26 // },
27 // hsts: { maxAge: 31536000, includeSubDomains: true },
28 // frameguard: { action: 'deny' },
29 // }));
30
31 // 3. CORS dla produkcji
32 // app.enableCors({
33 // origin: process.env.CORS_ORIGINS?.split(','),
34 // methods: ['GET', 'POST', 'PUT', 'DELETE'],
35 // credentials: true,
36 // });
37}
38
39// 4. Rate Limiting z @nestjs/throttler
40@Module({
41 imports: [
42 ThrottlerModule.forRoot([
43 { name: 'short', ttl: 1000, limit: 3 },
44 { name: 'long', ttl: 60000, limit: 100 },
45 ]),
46 ],
47 providers: [
48 { provide: APP_GUARD, useClass: ThrottlerGuard },
49 ],
50})
51export class SecurityModule {}
52
53// TODO: Dodaj Helmet middleware w SecurityModule
54// TODO: Skonfiguruj CORS z lista dozwolonych domen
55// TODO: Dodaj CSRF protection dla endpointow z cookies
56Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Helmet.js w aplikacji NestJS służy do:
2. @nestjs/throttler w NestJS służy do:
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Zaimplementuj SecurityModule z ThrottlerModule, Helmet middleware i konfiguracją CORS dla produkcji
- Układanie w pionie
Uporządkuj warstwy zabezpieczeń aplikacji NestJS od najniższej (transport) do najwyższej (aplikacja):