Опирается на правила: R-SEC-SECRET-1R-SEC-SECRET-3 и R-SEC-SECRET-X1R-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.yamlR-SEC-SECRET-X1Pydantic BaseSettings + env/secret-store
.env в git (даже с примером)R-SEC-SECRET-X2.env.example без значений
git rebase после leak без rotationR-SEC-SECRET-3rotate first, в течение часа
Pre-commit hook в README, не автоматическиR-SEC-SECRET-2pre-commit-framework + make dev-setup
Gitleaks только в CI, без pre-commitR-SEC-SECRET-1оба слоя
Скан только diff, не historyR-SEC-SECRET-1weekly full history scan
SecretStr не используется для чувствительных полейR-SEC-SECRET-X1pydantic.SecretStr скрывает в логах
Hardcoded secrets в переменных окружения Docker ENVR-SEC-SECRET-X1runtime 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).