Однажды разработчик пушит код на GitHub, а через 20 минут ему звонит взволнованный клиент: «Кто-то слил базу». Причина — строчка вида password = "super_secret_123" в коммите, который стал публичным. Такое случается чаще, чем кажется. Боты сканируют новые коммиты на GitHub в режиме реального времени и ищут именно это.

Разберём, как грамотно управлять секретами, чтобы не оказаться в такой ситуации.

Почему пароли в коде — это не просто «плохая практика»

Проблема не только в публичных репозиториях. Даже в приватном корпоративном git-хранилище секрет в коде — это:

  • Доступ для всех, у кого есть права на репозиторий (разработчики, QA, DevOps, иногда подрядчики)
  • Невозможность ротации без изменения кода и нового деплоя
  • История в git — даже если удалить пароль из файла, он навсегда останется в коммитах
  • Одинаковые секреты в dev и production, потому что «так удобнее»

Ротировать скомпрометированный пароль к базе в 3 ночи, когда об этом узнали — удовольствие сомнительное. Лучше выстроить нормальный процесс заранее.

Переменные окружения — минимальный порог

Самый простой шаг — вынести секреты в переменные окружения (environment variables). Вместо:

DB_PASSWORD = "my_password"

Пишем:

import os
DB_PASSWORD = os.environ.get("DB_PASSWORD")

Или в Node.js:

const dbPassword = process.env.DB_PASSWORD;

Секрет живёт вне кода — на сервере, в CI/CD, в Docker-контейнере. В репозиторий он не попадает.

Но и тут есть нюансы. Не стоит:

  • Хардкодить дефолтные значения: os.environ.get("DB_PASSWORD", "admin123") — это та же дыра
  • Логировать переменные окружения при старте приложения
  • Передавать секреты через аргументы командной строки (они видны в ps aux)

.env-файлы: удобно, но осторожно

Для локальной разработки удобен .env-файл:

DB_HOST=localhost
DB_PORT=5432
DB_PASSWORD=local_dev_password
SMTP_API_KEY=key_here

Приложение читает его через библиотеки вроде python-dotenv или dotenv для Node.js. Удобно — не надо каждый раз экспортировать переменные вручную.

Главное правило: .env никогда не попадает в git. Для этого — в .gitignore:

.env
.env.local
.env.production

Вместо него в репозитории хранится .env.example с заглушками:

DB_HOST=
DB_PORT=5432
DB_PASSWORD=
SMTP_API_KEY=

Это шаблон для новых разработчиков — понятно, какие переменные нужны, но без реальных значений.

Для production .env-файлы — не лучший вариант. Их надо как-то доставить на сервер, где-то хранить, синхронизировать. Лучше использовать специализированные инструменты.

Vault от HashiCorp — промышленный стандарт

HashiCorp Vault — это централизованное хранилище секретов. Принцип работы:

  1. Секреты хранятся в Vault, зашифрованные
  2. Приложение при старте запрашивает нужные секреты по токену или через сервисный аккаунт
  3. Vault отдаёт секреты в runtime, а не при деплое
  4. Все обращения логируются
  5. Секреты можно ротировать без перезапуска приложения (dynamic secrets)

Пример: Vault умеет сам создавать временные credentials к PostgreSQL с ограниченным сроком жизни. Приложение получает логин и пароль, которые действуют, скажем, 1 час. После этого они автоматически удаляются. Даже если credentials утекут — они уже недействительны.

Интеграция с приложением выглядит примерно так:

import hvac

client = hvac.Client(url='https://vault.internal:8200', token=os.environ['VAULT_TOKEN'])
secret = client.secrets.kv.read_secret_version(path='myapp/db')
db_password = secret['data']['data']['password']

Vault — серьёзный инструмент с кривой обучения. Для небольших проектов он избыточен, но для enterprise или нагруженных систем с командой из нескольких разработчиков — оправдан.

Облачные решения: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault

Если инфраструктура уже в облаке — проще использовать нативные инструменты.

AWS Secrets Manager хранит секреты и умеет их ротировать. Например, автоматически меняет пароль к RDS-базе и обновляет секрет. Приложение получает секрет через SDK:

import boto3
import json

client = boto3.client('secretsmanager', region_name='eu-central-1')
response = client.get_secret_value(SecretId='prod/myapp/db')
secret = json.loads(response['SecretString'])
db_password = secret['password']

Стоимость: около $0.40 в месяц за один секрет плюс $0.05 за 10 000 API-запросов. Для большинства проектов — копейки.

GCP Secret Manager работает аналогично. Удобен тем, что права доступа управляются через IAM — сервисный аккаунт получает только нужные секреты.

Azure Key Vault — то же самое в экосистеме Microsoft, плюс умеет хранить сертификаты и ключи шифрования.

Общий принцип облачных хранилищ: приложение авторизуется не через пароль, а через IAM-роль или Workload Identity. Таким образом, нет никакого «ключа от ключей» — доступ определяется тем, под каким аккаунтом запущен сервис.

Kubernetes Secrets — и почему их недостаточно

В Kubernetes есть встроенный механизм — объект Secret:

apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  password: c3VwZXJfc2VjcmV0  # base64

Под монтирует секрет как переменную окружения или файл:

env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password

Проблема: Kubernetes Secrets хранятся в etcd в base64, а не зашифрованными. Base64 — это не шифрование, это кодирование. Любой, у кого есть доступ к etcd или права kubectl get secret, видит значение.

Чтобы исправить это:

  1. Encryption at rest — включить шифрование секретов в etcd через EncryptionConfiguration
  2. External Secrets Operator — оператор, который тянет секреты из Vault, AWS Secrets Manager или других хранилищ и создаёт Kubernetes Secrets на лету
  3. Sealed Secrets от Bitnami — секреты шифруются публичным ключом кластера и безопасно хранятся в git

External Secrets Operator — пожалуй, лучший подход для production Kubernetes-кластеров. Он связывает внешнее хранилище (Vault, AWS SM) с нативными Kubernetes Secrets, и разработчику не нужно думать о механизме получения секретов.

Секреты в CI/CD

CI/CD-пайплайны тоже нуждаются в секретах — для деплоя, отправки уведомлений, доступа к registry.

Все современные системы поддерживают хранение секретов:

  • GitHub Actions: Settings → Secrets and Variables → Actions. Секреты доступны как ${{ secrets.MY_SECRET }}
  • GitLab CI: Settings → CI/CD → Variables. Можно ограничить по окружению (только для production)
  • Jenkins: Credentials Manager

Важные правила:

  • Не передавай секреты через артефакты или кэш — только через защищённые переменные
  • Маскируй секреты в логах (большинство систем делает это автоматически, но проверь)
  • Раздели секреты для разных окружений — staging и production не должны иметь одинаковые ключи
  • Используй OIDC-аутентификацию вместо долгоживущих токенов, где возможно. GitHub Actions умеет получать временные AWS credentials через OIDC без хранения ключей

Аудит: что уже утекло

Если ты читаешь это и думаешь «а вдруг у нас уже что-то в репозитории» — это хороший вопрос.

Инструменты для поиска секретов в коде:

Gitleaks — сканирует git-историю и находит паттерны, похожие на секреты (API-ключи, пароли, токены):

gitleaks detect --source . --verbose

TruffleHog — аналогичный инструмент, умеет проверять не только локальные репозитории, но и GitHub/GitLab организации:

trufflehog git file://. --only-verified

detect-secrets от Yelp — можно встроить в pre-commit хуки, чтобы блокировать коммиты с потенциальными секретами:

pip install detect-secrets
detect-secrets scan > .secrets.baseline

Если нашёл секрет в истории — плохая новость: просто удалить файл не поможет. Нужно использовать git filter-repo или BFG Repo Cleaner для перезаписи истории. И, конечно, немедленно ротировать скомпрометированный секрет — считай его утекшим.

Чек-лист для production

Короткий список того, что стоит проверить прямо сейчас:

  • В .gitignore есть .env, .env.local, *.pem, *.key, config/secrets.yml и подобные файлы
  • В репозитории нет строк с password =, api_key =, secret = с реальными значениями
  • Для каждого окружения (dev, staging, production) — отдельные секреты
  • Секреты ротируются хотя бы раз в год, а лучше — автоматически
  • В CI/CD нет секретов, захардкоженных в .gitlab-ci.yml или Jenkinsfile
  • Есть мониторинг аномального использования ключей (например, AWS CloudTrail или аналоги)
  • Настроен pre-commit хук или CI-шаг с gitleaks/detect-secrets

Практика vs теория

Теория говорит «используй Vault». Практика — маленький стартап с одним бэкендером часто живёт на .env-файлах, которые передаются по Telegram. Это плохо, но это реальность.

Градация по сложности:

  1. Минимум: переменные окружения + .gitignore + .env.example в репозитории
  2. Лучше: секреты в GitHub/GitLab CI Variables, разделённые по окружениям
  3. Хорошо: облачное хранилище (AWS Secrets Manager, GCP Secret Manager) с доступом через IAM-роли
  4. Enterprise: HashiCorp Vault с dynamic secrets, аудит-логами и автоматической ротацией

Даже переход с первого уровня на второй уже закрывает большинство типичных проблем.

Когда мы в REEXY делаем проекты с production-деплоем, базовые правила по секретам — часть стандартного процесса разработки. Это не опция, это гигиена. Настроить нормальное управление секретами при старте проекта занимает пару часов, а потом экономит нервы и деньги.

Главное правило одно: если секрет попал в git — считай, что он публичный, независимо от видимости репозитория. Строй процессы исходя из этого.