Пошук уроків, статей та іншого контенту
Налаштуєте безпечну роботу з ключами, токенами й паролями та уникатимете їх потрапляння до репозиторію.
Секрет — це значення, розкриття якого може надати доступ до даних або інфраструктури:
JWT-ключі підпису;
токени сторонніх API;
паролі до баз даних;
ключі шифрування;
облікові дані для хмарних сервісів;
приватні ключі.
Секрети не повинні:
зберігатися безпосередньо у вихідному коді;
потрапляти до Git-репозиторію;
передаватися у фронтенд;
виводитися в логи;
зберігатися у відкритому вигляді в документації.
Звичайні налаштування, наприклад номер порту або назва середовища, також можна зберігати у змінних середовища. Але їх витік зазвичай не має таких наслідків, як витік пароля чи приватного ключа.
Найпростіший спосіб передати секрет застосунку — змінна середовища:
JWT_SECRET="локальний-секрет"
DATABASE_URL="postgresql://app:password@localhost:5432/app"У NestJS змінні середовища доступні через ConfigService пакета @nestjs/config.
Встановіть пакет:
npm install @nestjs/config joiПідключіть конфігурацію в кореневому модулі:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
],
})
export class AppModule {}Параметр isGlobal: true робить ConfigService доступним в інших модулях без повторного імпорту ConfigModule.
Тепер секрет можна отримати в сервісі:
// src/auth/auth.service.ts
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
private readonly jwtSecret: string;
constructor(private readonly configService: ConfigService) {
const jwtSecret = this.configService.get<string>('JWT_SECRET');
if (!jwtSecret) {
throw new InternalServerErrorException(
'JWT_SECRET не налаштовано',
);
}
this.jwtSecret = jwtSecret;
}
getJwtSecret(): string {
return this.jwtSecret;
}
}У реальному застосунку секрет потрібен для підписування або перевірки токенів. Саме значення не потрібно передавати у контролери, повертати клієнту чи виводити в лог.
.env для локальної розробкиФайл .env зручно використовувати на локальній машині:
NODE_ENV=development
PORT=3000
JWT_SECRET=згенерований-локальний-секрет
DATABASE_URL=postgresql://app:password@localhost:5432/appНе використовуйте однаковий секрет для всіх середовищ. Локальний секрет, тестовий секрет і production-секрет мають бути різними.
Додайте .env до .gitignore:
# Локальні секрети
.env
.env.*
!.env.exampleФайл .env.example можна зберігати в репозиторії. Він містить лише назви змінних і безпечні приклади:
NODE_ENV=development
PORT=3000
JWT_SECRET=replace-with-a-local-secret
DATABASE_URL=postgresql://user:password@localhost:5432/databaseРозробник створює власний .env на основі цього файлу:
cp .env.example .envЗначення в .env.example не повинні бути справжніми ключами, токенами або паролями.
Якщо секрет відсутній, застосунок краще зупинити під час запуску. Це безпечніше, ніж дозволити йому працювати з неправильним або порожнім ключем.
Для перевірки використайте Joi:
// 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,
envFilePath: '.env',
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'test', 'production')
.default('development'),
PORT: Joi.number()
.port()
.default(3000),
JWT_SECRET: Joi.string()
.min(32)
.required(),
DATABASE_URL: Joi.string()
.uri({
scheme: ['postgresql', 'postgres'],
})
.required(),
}),
}),
],
})
export class AppModule {}Тепер застосунок не запуститься, якщо:
JWT_SECRET не задано;
його довжина менша за 32 символи;
DATABASE_URL відсутній;
значення PORT не є коректним портом.
Перевірка довжини не замінює криптографічно безпечну генерацію секрету. Для створення випадкового секрету можна використати Node.js:
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"Отриманий рядок можна використати як локальний JWT_SECRET.
Розподіліть доступ до конфігурації через окремий сервіс або модуль. Не звертайтеся до process.env у кожному файлі застосунку.
Наприклад:
// src/config/app-config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AppConfigService {
constructor(private readonly configService: ConfigService) {}
get port(): number {
return this.configService.get<number>('PORT', 3000);
}
get jwtSecret(): string {
const secret = this.configService.get<string>('JWT_SECRET');
if (!secret) {
throw new Error('JWT_SECRET не налаштовано');
}
return secret;
}
get databaseUrl(): string {
const url = this.configService.get<string>('DATABASE_URL');
if (!url) {
throw new Error('DATABASE_URL не налаштовано');
}
return url;
}
}Такий підхід:
централізує назви змінних;
зменшує кількість прямих звернень до середовища;
робить конфігурацію зручнішою для тестування;
дозволяє приховати деталі отримання значень.
Для великих застосунків можна використовувати окремі конфігураційні фабрики, але базове правило залишається тим самим: секрет перевіряється на межі застосунку і не розповсюджується без потреби.
У production не обов’язково створювати файл .env на сервері. Платформа запуску може передати змінні середовища безпосередньо процесу застосунку.
Наприклад:
NODE_ENV=production \
PORT=3000 \
JWT_SECRET="$JWT_SECRET_FROM_DEPLOYMENT_PLATFORM" \
DATABASE_URL="$DATABASE_URL_FROM_DEPLOYMENT_PLATFORM" \
node dist/main.jsКонкретний спосіб передавання секретів залежить від середовища запуску:
змінні середовища сервера;
секрети CI/CD;
секрети контейнерної платформи;
спеціалізовані сховища секретів.
У production потрібно:
створювати окремі секрети для кожного середовища;
обмежувати доступ до секретів лише потрібними сервісами;
не копіювати production-секрети на локальні комп’ютери;
змінювати секрети за процедурою ротації;
перевіряти, що секрети не потрапляють у логи або артефакти збірки.
Пароль користувача і секрет застосунку — різні типи даних.
Секрет застосунку, наприклад JWT_SECRET, потрібен самому серверу. Пароль користувача сервер не повинен зберігати у відкритому вигляді. У базі даних зберігають лише результат повільного криптографічного хешування з використанням відповідного алгоритму та унікальної солі.
Не можна:
записувати пароль у логи;
повертати пароль в API-відповіді;
зберігати пароль у .env;
зберігати пароль користувача у відкритому вигляді в базі даних;
порівнювати паролі через звичайне текстове порівняння після їх зберігання.
Змінна DATABASE_URL може містити пароль сервісного користувача бази даних. Це теж секрет, тому її потрібно захищати так само, як JWT-ключ.
Перед комітом перевіряйте:
чи не містить diff значень JWT_SECRET, API_KEY, TOKEN, PASSWORD;
чи не додано файл .env;
чи не потрапили секрети до тестових фікстур;
чи не виводяться змінні середовища в діагностичному коді;
чи не включені секрети до Dockerfile, образів або артефактів CI/CD.
Небезпечний приклад:
console.log(process.env);Він може вивести всі секрети процесу.
Безпечніший варіант — виводити лише несекретні значення:
console.log({
environment: process.env.NODE_ENV,
port: process.env.PORT,
});Якщо секрет уже потрапив до репозиторію, простого видалення файлу недостатньо. Значення могло залишитися в історії Git або в кешах. Такий секрет потрібно негайно замінити, а доступи, пов’язані з ним, — перевірити.
const jwtSecret = 'my-secret-key';Код може потрапити до Git, журналів змін, pull request або зібраного артефакту. Використовуйте змінну середовища та перевірку конфігурації.
Якщо програма запускається з порожнім JWT_SECRET, помилку буде складніше знайти. Обов’язкові значення потрібно перевіряти під час ініціалізації ConfigModule.
.env.gitignore захищає лише від майбутнього додавання файлу. Переконайтеся, що .env не був закомічений раніше.
Компрометація локального або тестового середовища не повинна надавати доступ до production.
Навіть тимчасовий console.log(process.env) може потрапити до централізованого сховища логів і залишитися там надовго.
Значення, доступні фронтенду, не є секретами. Будь-який користувач може переглянути їх у браузері або мережевих запитах.
Перевірте налаштування локально:
cp .env.example .env
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
npm run start:devПотім тимчасово перейменуйте JWT_SECRET у .env і знову запустіть застосунок. Він має завершитися з помилкою валідації конфігурації.
Перед комітом перевірте стан Git:
git status
git diff --cachedУ результаті файл .env не повинен бути серед підготовлених до коміту файлів.
Секрети не зберігають у вихідному коді та репозиторії.
Для локальної розробки використовують .env, який додано до .gitignore.
У репозиторії зберігають лише .env.example без реальних значень.
ConfigModule і ConfigService надають доступ до змінних середовища в NestJS.
Обов’язкові секрети потрібно перевіряти під час запуску через схему валідації.
Production-секрети передають через захищене середовище розгортання.
Секрети не можна виводити в логи або передавати клієнту.
Паролі користувачів зберігають лише як безпечні хеші, а не у відкритому вигляді.
Якщо секрет опубліковано, його потрібно негайно відкликати та замінити.