
Пароли в .env, ключи в Slack и «вшитые» токены в образах контейнеров — это не просто «технический долг». Это прямые риски: утечки данных, штрафы, простой продукта и провал аудита (ISO 27001, SOC 2, PCI DSS). А ещё это тормоз для команды: невозможно безопасно подключать подрядчиков, проводить ревью инцидентов и менять ключи без паники.
Цель статьи — показать, как за 2–3 недели поставить систему управления секретами, чтобы:
Все они должны быть учтены, зашифрованы, доступны по принципу «минимально необходимый доступ», с настройкой ротации и журналированием.
Что точно не подходит: .env в Git, переменные окружения в Dockerfile, общий Google Sheet, секреты в Terraform без шифрования.
Идеально, когда приложение не «знает» постоянных паролей. Оно аутентифицируется через инфраструктуру (роль в облаке, ServiceAccount в Kubernetes, машинный сертификат), а секреты выдаёт менеджер по этой идентичности и политикам.
Ниже пример, как получать секреты из AWS Secrets Manager, кэшировать их и безопасно обновлять без простоя.
# requirements: boto3, fastapi, uvicorn
import json
import os
import threading
import time
from typing import Dict, Tuple
import boto3
from botocore.config import Config
from fastapi import FastAPI
import psycopg2
SECRET_NAME = os.getenv("DB_SECRET_NAME", "prod/app/db")
REGION = os.getenv("AWS_REGION", "eu-central-1")
REFRESH_INTERVAL = int(os.getenv("SECRET_REFRESH_SEC", "60"))
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=REGION,
config=Config(retries={'max_attempts': 3, 'mode': 'standard'})
)
_cache_lock = threading.Lock()
_secret_cache: Dict[str, Tuple[dict, float]] = {}
def get_secret(name: str) -> dict:
with _cache_lock:
item = _secret_cache.get(name)
if item and time.time() - item[1] < REFRESH_INTERVAL:
return item[0]
resp = client.get_secret_value(SecretId=name)
payload = resp.get('SecretString') or resp.get('SecretBinary')
if isinstance(payload, bytes):
payload = payload.decode()
data = json.loads(payload)
with _cache_lock:
_secret_cache[name] = (data, time.time())
return data
def db_connect():
s = get_secret(SECRET_NAME)
conn = psycopg2.connect(
host=s['host'],
port=s.get('port', 5432),
user=s['username'],
password=s['password'],
dbname=s['dbname'],
connect_timeout=3,
sslmode='require'
)
return conn
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/users/count")
def users_count():
with db_connect() as conn:
with conn.cursor() as cur:
cur.execute("SELECT count(*) FROM users")
(cnt,) = cur.fetchone()
return {"count": cnt}
# Фоновое обновление секрета: если пароль обновили, приложение заметит в течение минуты
def secret_refresher():
while True:
try:
get_secret(SECRET_NAME)
except Exception as e:
# залогируйте в вашу систему логирования/метрик
pass
time.sleep(REFRESH_INTERVAL)
threading.Thread(target=secret_refresher, daemon=True).start()
Практика: при ротации пароля в Secrets Manager создайте второго пользователя в БД, обновите секрет и постепенно переподключите соединения — так вы избежите обрывов.
Динамические секреты уменьшают «размах поражения»: пароль живёт минуты и привязан к роли.
Включаем движок и настраиваем подключение к БД:
vault secrets enable database
vault write database/config/appdb \
plugin_name=postgresql-database-plugin \
allowed_roles="appdb-role" \
connection_url="postgresql://{{username}}:{{password}}@db.example.com:5432/postgres?sslmode=require" \
username="vault_admin" \
password="<пароль_админа_BД>"
vault write database/roles/appdb-role \
db_name=appdb \
creation_statements='CREATE ROLE "{{name}}" WITH LOGIN PASSWORD "{{password}}" VALID UNTIL ''{{expiration}}'' INHERIT; GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA public TO "{{name}}"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT,INSERT,UPDATE,DELETE ON TABLES TO "{{name}}";' \
default_ttl=15m \
max_ttl=1h
Приложение получает временные креды через роль и токен:
# requirements: hvac, psycopg2
import os
import hvac
import psycopg2
VAULT_ADDR = os.getenv("VAULT_ADDR", "https://vault.example.com")
VAULT_ROLE = os.getenv("VAULT_DB_ROLE", "appdb-role")
VAULT_TOKEN = os.getenv("VAULT_TOKEN") # лучше — аутентификация по JWT/Kubernetes
client = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN)
creds = client.secrets.database.generate_credentials(name=VAULT_ROLE)
user = creds['data']['username']
password = creds['data']['password']
conn = psycopg2.connect(host='db.example.com', dbname='app', user=user, password=password, sslmode='require')
Через 15 минут учётка истечёт автоматически. Даже если лог утёк, злоумышленник не успеет им воспользоваться.
Читаем секреты из облачного менеджера напрямую в кластер, не «кладя» их навсегда в etcd.
# service-account с правом читать секреты из внешнего хранилища (пример для AWS)
apiVersion: v1
kind: ServiceAccount
metadata:
name: web-sa
namespace: prod
---
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-sm
namespace: prod
spec:
provider:
aws:
service: SecretsManager
region: eu-central-1
auth:
jwt:
serviceAccountRef:
name: web-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: prod
spec:
refreshInterval: 1m
secretStoreRef:
name: aws-sm
kind: SecretStore
target:
name: app-env
creationPolicy: Owner
data:
- secretKey: DB_PASSWORD
remoteRef:
key: prod/app/db
property: password
- secretKey: DB_USERNAME
remoteRef:
key: prod/app/db
property: username
Деплой монтирует секрет как переменные окружения/файл. При обновлении в хранилище оператор перезапишет значение, а контроллер перезапустит pod при необходимости.
Хранить секреты в Git удобно, но только зашифрованными. sops шифрует конкретные поля файла, ключи лежат в KMS/age.
# установка age и sops
# macOS: brew install age sops
# Linux: скачайте релизы с GitHub
age-keygen -o agekey.txt
export SOPS_AGE_KEY_FILE=agekey.txt
cat > secrets.yaml <<'YAML'
db:
user: app
password: supersecret
YAML
sops --encrypt --in-place --age $(cat agekey.txt | grep public | awk '{print $4}') secrets.yaml
# Файл теперь зашифрован и может храниться в Git
# Расшифровать для локального запуска
sops --decrypt secrets.yaml > secrets.dec.yaml
Конфигурация sops.yaml позволяет автоматически шифровать нужные файлы и поля.
# .sops.yaml
creation_rules:
- path_regex: secrets\\.yaml$
age: ["age1qxyz...ваш_публичный_ключ..."]
Грубая оценка: если команда тратит ~10 часов в месяц на ручное управление секретами (по $50/час) — это $500. Менеджер секретов с автоматизацией окупается буквально в первый месяц.
Типичные ошибки:
Грамотное управление секретами — это не «бюрократия», а простой способ резко снизить риски и ускорить работу команды. Начните с инвентаризации и выбранного менеджера секретов, переведите критичные доступы, включите аудит и ротацию. Через 2–3 недели вы забудете о .env в репозитории, спокойно пройдёте аудит и перестанете бояться увольнений и компрометации ключей. А главное — это масштабируется вместе с продуктом, не мешая разработке и релизам.