Опирается на правила:
R-SEC-SECRET-1…R-SEC-SECRET-3иR-SEC-SECRET-X1…R-SEC-SECRET-X2из Security Style Guide → раздел 3. Секреты в коде и истории.
Важно знать
- Gitleaks в pre-commit + CI — два слоя: pre-commit ловит до push, CI страховочно.
- Full history scan раз в неделю — секрет может всплыть задним числом (старый коммит, новые правила).
- Pre-commit hook через husky —
"prepare": "husky"вpackage.json, устанавливается однимnpm ci, не ручной инструкцией в README.- Утечка → rotate сначала — в течение часа; удаление из истории опционально и не отменяет компрометацию.
- GitHub уже проиндексировал — AWS-боты эксплуатируют ключ за минуты,
git rebaseне помогает.- Секреты в
src/— запрещены; читаются черезConfigService+@nestjs/configcvalidationSchema..envв git — запрещён даже «для примера»; используй.env.exampleбез значений.git commit --no-verify— обходит pre-commit; CI — единственный уровень, который нельзя обойти.
Один закоммиченный пароль или API-ключ — инцидент даже если удалить через минуту. GitHub и специализированные сканеры индексируют репозитории в реальном времени; AWS-ключи эксплуатируются в первые секунды после появления в публичном репо. UCP формулирует двухслойную защиту: предотвращение через pre-commit + детекция через CI + немедленная rotation при обнаружении.
Gitleaks в pre-commit
R-SEC-SECRET-1 — первый слой, до push.
.gitleaks.toml в корне репо:
title = "order-service gitleaks config"
[allowlist]
description = "Разрешённые паттерны"
paths = [
'''\.example$''',
'''^test/.*\.fixture\.json$''',
'''README\.md''',
]
[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(?:api[_-]?key|apikey|api[_-]?token)['"\s:=]+['"]([0-9a-zA-Z\-_]{20,})'''
keywords = ["api_key", "apikey", "api-key"]
[[rules]]
id = "aws-access-key"
description = "AWS Access Key ID"
regex = '''AKIA[0-9A-Z]{16}'''
[[rules]]
id = "private-key"
description = "Private SSH/TLS key"
regex = '''-----BEGIN (?:RSA |OPENSSH |DSA |EC |PGP )?PRIVATE KEY( BLOCK)?-----'''
[[rules]]
id = "jwt"
description = "JWT token"
regex = '''eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'''
Pre-commit hook через husky:
npm install --save-dev husky
npx husky init
.husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
docker run --rm -v "$(pwd):/repo" zricethezav/gitleaks:latest detect \
--source=/repo \
--config=/repo/.gitleaks.toml \
--staged
--staged — сканирует только staged changes (то, что войдёт в коммит). При finding — exit 1, коммит блокируется:
Finding: AKIAIOSFODNN7EXAMPLE
Secret: AKIAIOSFODNN7EXAMPLE
RuleID: aws-access-key
File: src/config/payment.config.ts
Line: 12
Pre-commit hook автоматически
R-SEC-SECRET-2 — hook коммитится в репо, устанавливается одним npm ci.
package.json:
{
"scripts": {
"prepare": "husky"
},
"devDependencies": {
"husky": "^9.1.0"
}
}
После git clone + npm ci — hook установлен у всех разработчиков автоматически. Без husky README пишет «установи hook вручную», большинство разработчиков пропускают этот шаг.
CI как страховка
R-SEC-SECRET-1 — git commit --no-verify обходит pre-commit; CI неминуем.
GitHub Actions:
name: Gitleaks
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '0 3 * * 1' # weekly full history scan
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
На PR — сканирует diff. На push в main — diff. Раз в неделю — full history: secret мог быть закоммичен полгода назад, когда правила были мягче, теперь pattern его ловит.
Секреты через ConfigService
R-SEC-SECRET-X1 — секреты в src/ запрещены. В NestJS единственный легитимный путь — @nestjs/config с валидацией схемы.
app.module.ts:
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DB_PASSWORD: Joi.string().required(),
PAYMENT_CLIENT_SECRET: Joi.string().required(),
SBER_API_KEY: Joi.string().required(),
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
}),
}),
],
})
export class AppModule {}
order.service.ts — инжект через DI, значение никогда не хардкодится:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class OrderService {
constructor(private readonly config: ConfigService) {}
async submitPayment(orderId: string): Promise<void> {
const apiKey = this.config.getOrThrow<string>('SBER_API_KEY');
// используется только здесь, не логируется
}
}
getOrThrow — бросает исключение при старте, если переменная отсутствует, а не падает в рантайме при первом запросе. Приложение не запустится с неполной конфигурацией.
Локально — .env:
# .env (в .gitignore)
DB_PASSWORD=devpassword123
PAYMENT_CLIENT_SECRET=test-secret
SBER_API_KEY=test-sber-key
NODE_ENV=development
В production — секреты через runtime: Kubernetes Secret, Vault Agent Injector, AWS Secrets Manager или GCP Secret Manager; в контейнер попадают как env vars, не хранятся в образе.
Если секрет утёк — rotate first
R-SEC-SECRET-3 — порядок действий.
Сценарий: gitleaks нашёл закоммиченный SBER_API_KEY в customer.config.ts.
Шаг 1 (в течение часа): rotate.
- Войти в панель Sber Business, деактивировать этот ключ.
- Сгенерировать новый, обновить env vars в CI и кластере.
- Уведомить команду безопасности.
Шаг 2 (опционально): удаление из истории.
git filter-repoилиbfg-repo-cleaner— переписать историю без секрета.- Force push в main, все разработчики делают
git cloneзаново.
Почему именно в таком порядке: GitHub и специализированные боты индексируют репозиторий в реальном времени. Время от push до эксплуатации AWS-ключа — секунды. git rebase через час бесполезен: секрет уже у злоумышленника. bfg-repo-cleaner нужен только для compliance — доказать аудитору, что в репо больше нет секрета, — но не возвращает скомпрометированный ключ.
.env в git
R-SEC-SECRET-X2 — запрещён даже с пометкой «example».
# НЕЛЬЗЯ — .env в git
DB_PASSWORD=devpassword123
SBER_API_KEY=test-key-please-replace
Разработчик не заметит и положит prod-значение вместо заглушки — утечка. Боты скрейпят .env-файлы так же активно, как и исходный код.
Правильно:
# .env.example (закоммитен, без значений)
DB_PASSWORD=
PAYMENT_CLIENT_SECRET=
SBER_API_KEY=
NODE_ENV=
# .env (в .gitignore, локальный)
DB_PASSWORD=devpassword123
PAYMENT_CLIENT_SECRET=test-secret
SBER_API_KEY=test-sber-key
NODE_ENV=development
.gitignore:
.env
.env.local
.env.*.local
*.pem
*.key
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Секрет в src/ или hardcoded в ConfigModule | R-SEC-SECRET-X1 | ConfigService.getOrThrow + env var |
.env в git (даже с заглушками) | R-SEC-SECRET-X2 | .env.example без значений |
Секрет в переменной ENV в Dockerfile | R-SEC-SECRET-X1 | env через runtime (k8s Secret / Vault) |
git rebase / bfg без rotation | R-SEC-SECRET-3 | rotate first, история — опционально |
| Pre-commit hook в README, не автоматически | R-SEC-SECRET-2 | husky + "prepare": "husky" |
| Gitleaks только в CI, без pre-commit | R-SEC-SECRET-1 | оба слоя |
| Скан только diff, без weekly full history | R-SEC-SECRET-1 | schedule: cron в CI |
git commit --no-verify как норма | R-SEC-SECRET-2 | CI как неминуемый второй слой |
Куда дальше
- Container/image-уязвимости — секреты не должны попасть в слои Docker-образа.
- Криптография в коде — hardcoded ключи/IV как частный случай secrets-антипаттерна.
- CVE в зависимостях — другой класс уязвимостей: библиотеки, не секреты.
- Реакция на findings — SLA для утечки секрета: hotfix ≤ 1 час.
- SAST по коду —
eslint-plugin-securityловитdetect-non-literal-requireи hardcoded credentials.