Опирается на правила:
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 раз в неделю — leaked secret может всплыть задним числом.
- Pre-commit hook через
pre-commit-framework — не ручная инструкция в README.- Утечка → rotate сначала, удаление из истории — опционально; GitHub уже проиндексировал.
- Секреты в
settings.py/ YAML — запрещены; только переменные окружения или secret-store..envв git — запрещён, даже с пометкой «example»; используй.env.exampleбез значений.Pydantic BaseSettings— единственный легитимный способ собрать конфигурацию из env в FastAPI-сервисе.python-dotenv— только для локального dev;.envв.gitignore.
Один закоммиченный API-ключ — это инцидент даже если удалить через минуту. GitHub индексирует репо, сканирующие боты работают 24/7 и используют найденные credentials в первые минуты. UCP формулирует двухслойную защиту: предотвращение через pre-commit и детекция через CI, плюс быстрая rotation при обнаружении.
Gitleaks в pre-commit
R-SEC-SECRET-1: первый слой — до push.
.gitleaks.toml в корне репо:
title = "order-service gitleaks config"
[allowlist]
description = "Allowed patterns"
paths = [
'''\.example$''',
'''^tests/.*\.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-config.yaml — подключение Gitleaks через pre-commit-framework:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks
args: ["--config", ".gitleaks.toml"]
После pre-commit install hook установлен. Любой разработчик после git clone + pre-commit install получает его автоматически — не через README.
Pre-commit hook автоматически
R-SEC-SECRET-2: hook коммитится в репо.
Makefile:
.PHONY: dev-setup
dev-setup:
uv pip install pre-commit
pre-commit install
Проект задокументирован в CONTRIBUTING.md одной строкой: make dev-setup. Без pre-commit-framework — README пишет «установи hook вручную», большинство разработчиков пропускает.
Альтернатива для uv-проектов:
# pyproject.toml
[tool.hatch.envs.default.scripts]
setup = ["pre-commit install"]
CI как страховка
R-SEC-SECRET-1: pre-commit может быть пропущен (git commit --no-verify), CI — нет.
GitHub Actions:
name: Gitleaks
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '0 3 * * 1' # weekly 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 мог быть закоммичен полгода назад, тогда паттерн не ловил, теперь ловит.
Конфигурация через Pydantic BaseSettings
R-SEC-SECRET-X1: единственный легитимный способ подключить секреты в FastAPI — через pydantic-settings.
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class OrderServiceSettings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
secrets_dir="/run/secrets",
)
db_password: SecretStr
sber_api_key: SecretStr
kafka_brokers: str
customer_service_url: str
SecretStr скрывает значение при repr() и логировании — *** вместо реального значения. Pydantic читает переменные из env, .env-файла (только локально) или /run/secrets (Docker secrets / Kubernetes volume), в таком приоритете.
Подключение к FastAPI:
from functools import lru_cache
from fastapi import Depends
@lru_cache
def get_settings() -> OrderServiceSettings:
return OrderServiceSettings()
def create_app() -> FastAPI:
app = FastAPI()
settings = get_settings()
app.state.settings = settings
return app
get_settings() вызывается один раз при старте; @lru_cache гарантирует singleton. В unit-тестах заменяется через app.dependency_overrides.
Если секрет утёк — rotate first
R-SEC-SECRET-3: порядок действий.
Сценарий: gitleaks-bot нашёл закоммиченный Sber acquiring API key.
Шаг 1 (в течение часа): rotate.
- Войти в личный кабинет Sber acquiring, деактивировать ключ.
- Создать новый ключ, обновить переменную окружения в CI / Kubernetes Secret.
- Уведомить команду безопасности.
Шаг 2 (опционально): удаление из истории.
git filter-repoилиbfg-repo-cleaner— переписать историю без secret.- Force push в
main. - Уведомить всех contributors сделать
git cloneзаново.
Почему именно в таком порядке: GitHub индексирует репо, поисковые боты тоже. Через минуту после push attacker знает секрет. git rebase через час бесполезен — secret уже у attacker. bfg-repo-cleaner помогает только для compliance audit, но не возвращает скомпрометированный ключ.
Запреты
Секреты в settings.py
R-SEC-SECRET-X1:
# КАТАСТРОФА
class ProductSettings:
db_password = "super-secret-prod-password"
sber_api_key = "live_sk_12345abcde"
customer_service_token = "Bearer eyJhbGciOiJSUzI1NiJ9..."
Любой с доступом к репо (включая read-only forks) видит prod-credentials.
Также запрещено в config.yaml / application.yaml:
# КАТАСТРОФА
database:
password: super-secret-prod-password
sber:
api_key: live_sk_12345abcde
Корректно — только плейсхолдеры:
database:
password: ${DB_PASSWORD}
sber:
api_key: ${SBER_API_KEY}
Откуда берутся значения в prod:
- Kubernetes Secret (минимальный baseline).
- Vault через Vault Agent Injector.
- Cloud Secret Manager (AWS SM, GCP Secret Manager).
- Docker secrets (
/run/secrets/<name>).
.env в git
R-SEC-SECRET-X2: даже с пометкой «example».
# КАТАСТРОФА — .env в git
DB_PASSWORD=devpassword123
SBER_API_KEY=test-key-please-replace
KAFKA_BROKERS=localhost:9092
Если разработчик не заметит и положит prod-значения — утечка. Плюс боты сканируют .env-файлы.
Правильно:
# .env.example (закоммитен, без значений)
DB_PASSWORD=
SBER_API_KEY=
KAFKA_BROKERS=
CUSTOMER_SERVICE_URL=
# .env (в .gitignore, локальный)
DB_PASSWORD=devpassword123
SBER_API_KEY=test-key
KAFKA_BROKERS=localhost:9092
CUSTOMER_SERVICE_URL=http://localhost:8001
.gitignore:
.env
.env.local
.env.*.local
*.pem
*.key
config/credentials.yml
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Секреты в settings.py или config.yaml | R-SEC-SECRET-X1 | Pydantic BaseSettings + env/secret-store |
.env в git (даже с примером) | R-SEC-SECRET-X2 | .env.example без значений |
git rebase после leak без rotation | R-SEC-SECRET-3 | rotate first, в течение часа |
| Pre-commit hook в README, не автоматически | R-SEC-SECRET-2 | pre-commit-framework + make dev-setup |
| Gitleaks только в CI, без pre-commit | R-SEC-SECRET-1 | оба слоя |
| Скан только diff, не history | R-SEC-SECRET-1 | weekly full history scan |
SecretStr не используется для чувствительных полей | R-SEC-SECRET-X1 | pydantic.SecretStr скрывает в логах |
Hardcoded secrets в переменных окружения Docker ENV | R-SEC-SECRET-X1 | runtime env injection |
Куда дальше
- Container/image-уязвимости — секреты не в Docker layers и не в
ENVинструкции. - Криптография в коде —
secrets.token_*,argon2-cffi, AES-GCM. - CVE в зависимостях —
pip-audit, Renovate, lock-файл. - Реакция на findings — SLA для утечки: hotfix ≤ 24ч.
- SAST по коду —
banditловитB105/B106(hardcoded passwords).