Пошук уроків, статей та іншого контенту
Систематизуєте налаштування конфігурації, HTTP-захисту, логування та секретів перед розгортанням застосунку.
Перед розгортанням NestJS-застосунку потрібно перевірити не лише бізнес-логіку, а й базові налаштування безпеки:
конфігурація не повинна містити секрети в коді;
вхідні дані мають проходити валідацію;
HTTP-заголовки повинні бути захищені;
CORS має дозволяти лише потрібні джерела;
запити мають бути обмежені за частотою та розміром;
логи не повинні містити паролі, токени й персональні дані;
помилки не повинні розкривати внутрішню структуру застосунку.
Найкраще перевіряти ці пункти автоматизованим чеклістом перед кожним production-релізом.
До секретів належать:
паролі бази даних;
JWT secret;
ключі сторонніх API;
credentials для хмарних сервісів;
приватні ключі та сертифікати;
ключі шифрування cookies або сесій.
Не слід зберігати їх у:
src/;
Dockerfile;
docker-compose.yml;
файлах конфігурації, які комітяться;
тестових fixtures;
логах і повідомленнях помилок.
Файл .env для локальної розробки має бути доданий до .gitignore. У production змінні краще передавати через систему секретів платформи розгортання або змінні середовища контейнера.
Застосунок має завершувати запуск, якщо обов’язкова змінна відсутня або має неправильний формат. Це безпечніше, ніж запустити сервіс із порожнім secret або невірним підключенням до бази даних.
Приклад конфігурації:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
cache: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'test', 'production')
.default('development'),
PORT: Joi.number()
.port()
.default(3000),
CORS_ORIGIN: Joi.string().required(),
JWT_SECRET: Joi.string()
.min(32)
.when('NODE_ENV', {
is: 'production',
then: Joi.required(),
}),
DATABASE_URL: Joi.string()
.uri()
.required(),
}),
validationOptions: {
abortEarly: false,
allowUnknown: false,
},
}),
],
})
export class AppModule {}Важливі властивості:
isGlobal: true робить ConfigService доступним без повторного імпорту модуля;
cache: true зменшує кількість звернень до process.env;
validationSchema перевіряє змінні до старту застосунку;
allowUnknown: false допомагає виявляти несподівані змінні конфігурації;
короткий JWT secret не повинен бути прийнятий у production.
У production не використовуйте значення на кшталт:
JWT_SECRET=secret
JWT_SECRET=123456
DATABASE_PASSWORD=passwordДоступ до конфігурації краще централізувати через ConfigService, а не читати process.env по всьому застосунку:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class TokenConfigService {
constructor(private readonly configService: ConfigService) {}
get secret(): string {
return this.configService.getOrThrow<string>('JWT_SECRET');
}
}getOrThrow корисний для критичних параметрів: якщо секрет відсутній, помилка виникне одразу, а не під час першої авторизації користувача.
Глобальний ValidationPipe повинен бути обов’язковою частиною production-конфігурації.
Рекомендовані параметри:
transform: true — перетворює вхідні значення до типів DTO;
whitelist: true — видаляє поля, яких немає в DTO;
forbidNonWhitelisted: true — повертає помилку замість тихого видалення невідомих полів;
forbidUnknownValues: true — відхиляє непередбачувані значення;
disableErrorMessages: true у production — не розкриває внутрішні деталі правил валідації.
// src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { json, urlencoded } from 'express';
import helmet from 'helmet';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const isProduction = process.env.NODE_ENV === 'production';
app.use(
helmet({
contentSecurityPolicy: isProduction ? undefined : false,
}),
);
app.use(json({ limit: '1mb' }));
app.use(urlencoded({ extended: true, limit: '100kb' }));
app.enableCors({
origin: process.env.CORS_ORIGIN?.split(',').map((origin) => origin.trim()),
credentials: true,
methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
disableErrorMessages: isProduction,
}),
);
app.setGlobalPrefix('api');
await app.listen(Number(process.env.PORT ?? 3000), '0.0.0.0');
}
bootstrap();Для DTO використовуйте явні правила:
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email!: string;
@IsString()
@MinLength(12)
password!: string;
}Не приймайте довільний об’єкт, якщо можна описати його DTO. DTO обмежує форму запиту та зменшує ризик масового присвоєння полів, коли клієнт передає, наприклад, role: "admin" разом із дозволеними полями.
Для Express-застосунку використовуйте helmet:
npm install helmethelmet встановлює або налаштовує захисні HTTP-заголовки, зокрема:
X-Content-Type-Options;
X-Frame-Options;
політики, пов’язані з Content Security Policy;
інші заголовки, які зменшують поширені ризики браузерного рівня.
Не вимикайте всі заголовки лише через те, що один із них потребує додаткового налаштування. Якщо застосунок має власний frontend або специфічні inline-скрипти, політику потрібно налаштовувати свідомо, а не безконтрольно послаблювати.
CORS не є механізмом автентифікації. Він лише визначає, які браузерні origin можуть виконувати крос-доменні запити.
Небезпечна production-конфігурація:
app.enableCors({
origin: '*',
credentials: true,
});Не поєднуйте origin: '*' із credentials: true. Для production перелік дозволених origin має бути явним:
app.enableCors({
origin: ['https://app.example.com'],
credentials: true,
});Якщо дозволених origin кілька, зберігайте їх у конфігурації, а не в коді. Не приймайте origin від клієнта без перевірки.
Production-трафік повинен передаватися через HTTPS. Якщо TLS завершується на reverse proxy або балансувальнику, переконайтеся, що:
proxy дійсно пересилає HTTPS-запити;
cookie мають прапорець Secure;
застосунок коректно обробляє X-Forwarded-Proto;
конфігурація довіри до proxy відповідає вашій інфраструктурі.
Не вмикайте безумовну довіру до довільних proxy без розуміння мережевої схеми. Неправильна конфігурація може дозволити клієнту підробити IP-адресу або схему запиту.
Великі body можуть спричинити надмірне споживання пам’яті. Встановлюйте ліміти відповідно до реальних потреб API:
app.use(json({ limit: '1mb' }));
app.use(urlencoded({ extended: true, limit: '100kb' }));Для endpoint завантаження файлів використовуйте окремі, чітко визначені обмеження. Не збільшуйте глобальний ліміт лише для одного endpoint.
Публічні endpoint, автентифікація, відновлення пароля та перевірка одноразових кодів повинні мати rate limit.
Для NestJS можна використати @nestjs/throttler:
npm install @nestjs/throttler// src/app.module.ts
import { Module } from '@nestjs/common';
import {
ThrottlerGuard,
ThrottlerModule,
} from '@nestjs/throttler';
import { APP_GUARD } from '@nestjs/core';
@Module({
imports: [
ThrottlerModule.forRoot([
{
ttl: 60_000,
limit: 100,
},
]),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}Ця конфігурація встановлює базове обмеження для запитів. Для чутливих endpoint потрібні суворіші ліміти. Наприклад, endpoint входу не повинен дозволяти стільки ж спроб, скільки звичайний endpoint читання даних.
Якщо застосунок працює за reverse proxy або в кількох репліках, перевірте, за якою IP-адресою виконується обмеження і чи потрібне спільне сховище лімітів. Локальний ліміт у пам’яті кожної репліки не дає єдиного глобального обмеження.
У production клієнт не повинен отримувати:
stack trace;
SQL-запити;
внутрішні шляхи файлової системи;
назви таблиць і служб;
значення змінних середовища;
службові заголовки інших сервісів;
повні тексти винятків від бази даних.
Для очікуваних помилок повертайте стабільний формат і зрозумілий HTTP-статус. Не передавайте клієнту error.message сторонньої бібліотеки без перевірки.
Водночас внутрішній лог має містити достатньо контексту для діагностики:
request ID;
endpoint;
HTTP-метод;
статус;
тривалість;
ідентифікатор користувача, якщо це допустимо;
тип помилки.
Відповідь клієнту та внутрішній лог можуть мати різний рівень деталізації.
Ніколи не логувати:
Authorization;
cookies;
паролі;
JWT;
refresh tokens;
API keys;
повні номери платіжних карток;
повні персональні дані без необхідності.
Особливо небезпечні автоматичні логи вхідних запитів, де middleware записує всі headers і body.
Небезпечний приклад:
console.log('Request:', req.headers, req.body);Безпечніше логувати лише потрібні поля:
import { Logger } from '@nestjs/common';
const logger = new Logger('OrdersService');
logger.log({
event: 'order_created',
orderId,
userId,
});Якщо потрібно ідентифікувати користувача, використовуйте внутрішній ідентифікатор, а не email або інші персональні дані.
Для production зазвичай потрібні:
error — помилки, що потребують уваги;
warn — підозрілі або нестандартні ситуації;
log або info — важливі бізнесові та операційні події;
debug — деталі для розробки, які не слід увімкнути без потреби.
Не використовуйте debug або verbose постійно в production, якщо це створює великий обсяг логів або ризик витоку даних.
Логи production мають потрапляти в централізовану систему, де можна:
шукати події;
фільтрувати за рівнем;
пов’язувати записи за request ID;
налаштовувати сповіщення;
контролювати термін зберігання;
обмежувати доступ.
Не покладайтеся лише на локальні файли контейнера. Вони можуть бути втрачені після перезапуску або недоступні під час інциденту.
Production-секрети мають бути доступні лише тим процесам і людям, яким вони потрібні. Перевірте:
права доступу до системи секретів;
права CI/CD;
доступ розробників;
доступ до логів;
можливість прочитати змінні через debug endpoint або actuator-подібні маршрути.
Не створюйте endpoint на кшталт /config, який повертає весь ConfigService.
Передбачте заміну:
JWT secret;
ключів сторонніх сервісів;
паролів бази даних;
ключів підпису;
credentials CI/CD.
Ротація має бути можливою без коміту нового секрету в репозиторій. Якщо секрет випадково потрапив у Git, недостатньо просто видалити рядок у наступному коміті — ключ потрібно негайно відкликати або замінити.
Різні середовища повинні мати різні значення:
development;
test;
staging;
production.
Також різні сервіси не повинні без необхідності використовувати один і той самий ключ. Компрометація одного сервісу не має автоматично компрометувати всі інші.
[ ] Секрети відсутні в Git і Dockerfile.
[ ] .env виключений із репозиторію.
[ ] Обов’язкові змінні валідовуються під час старту.
[ ] Для production використовується окремий набір секретів.
[ ] Немає небезпечних fallback-значень для паролів і ключів.
[ ] Доступ до конфігурації централізований.
[ ] Увімкнено HTTPS.
[ ] Налаштовано helmet.
[ ] CORS містить лише дозволені origin.
[ ] Не використовується origin: '*' разом із credentials.
[ ] Встановлено ліміти розміру body.
[ ] Для публічних і чутливих endpoint налаштовано rate limit.
[ ] Перевірено поведінку за reverse proxy.
[ ] Увімкнено глобальний ValidationPipe.
[ ] DTO містять явні правила валідації.
[ ] Невідомі поля відхиляються або видаляються за свідомим рішенням.
[ ] Production-відповіді не містять stack trace.
[ ] SQL- та внутрішні помилки не повертаються клієнту.
[ ] Паролі, токени й cookies не потрапляють у логи.
[ ] Не логуються повні headers і body без фільтрації.
[ ] Логи мають рівні та зрозумілі події.
[ ] Є request ID або інший спосіб пов’язати події одного запиту.
[ ] Логи централізовано зберігаються та мають контроль доступу.
[ ] Налаштовано термін зберігання та очищення логів.
process.env без перевіркиconst secret = process.env.JWT_SECRET;Якщо змінна відсутня, застосунок може непомітно працювати з undefined або некоректною fallback-логікою. Для обов’язкових значень використовуйте валідований конфігураційний шар і getOrThrow.
app.enableCors();Таке налаштування може бути прийнятним для локального прототипу, але в production політика доступу має бути явною.
Автоматичне логування headers і body часто призводить до витоку Authorization, cookies та паролів. Логувати потрібно не весь об’єкт запиту, а лише безпечні поля.
Ліміт у десятки або сотні мегабайт збільшує ризик виснаження пам’яті. Для великих payload використовуйте окремий endpoint і окрему політику.
Приховування поля у frontend не захищає API. Клієнт може відправити довільний HTTP-запит вручну. Перевірки DTO, авторизація та обмеження доступу мають виконуватися на сервері.
Якщо ключ із development потрапив у відкритий доступ, production не повинен бути скомпрометований тим самим ключем.
Endpoint для health check може повідомляти статус сервісу, але не повинен повертати секрети, повний конфіг, stack trace або детальні результати внутрішніх перевірок.
Production-безпека NestJS-застосунку складається з кількох узгоджених шарів:
Валідована конфігурація без секретів у коді.
Явна валідація та обмеження вхідних даних.
Захисні HTTP-заголовки, HTTPS і контрольований CORS.
Ліміти розміру запитів і частоти звернень.
Помилки без внутрішніх деталей для клієнта.
Логи без токенів, паролів і зайвих персональних даних.
Контроль доступу, ротація та розділення секретів.
Цей чекліст варто виконувати не лише перед першим деплоєм, а й під час кожної зміни конфігурації, інфраструктури або схеми логування.