
Пароль к базе, ключ платежного провайдера, токен к CRM — это «скелеты в шкафу» любого сервиса. Их утечка означает не только простой и репутационные потери, но и прямые деньги: штрафы по 152‑ФЗ/GDPR, возвраты клиентам, заблокированные мерчанты, фрод. В отличие от отказа сервиса, утечка секретов часто долго незаметна, а ущерб растягивается на месяцы.
Правильно выстроенное хранение и ротация секретов дают бизнесу четыре выгоды:
Секрет — это любой материал, раскрытие которого дает доступ или раскрывает чувствительные данные:
Где их обычно находят аудиторы:
Выбор зависит от требований к латентности, частоте ротации и окружения.
Ключевая идея — период, когда одновременно валидны «старый» и «новый» секрет (grace period), и приложение умеет обновляться без рестарта или с поэтапным рестартом.
Стратегии:
План ротации API‑ключа без даунтайма:
План ротации пароля к БД:
Пример манифеста с CSI (общее, идея):
apiVersion: v1
kind: Pod
metadata:
name: app-with-secrets
spec:
serviceAccountName: app-sa
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: my-provider-class
containers:
- name: app
image: myrepo/app:1.0.0
volumeMounts:
- name: secrets-store
mountPath: "/etc/secrets"
readOnly: true
import json
import os
import signal
import threading
import time
import boto3
from botocore.config import Config
SECRET_ID = os.getenv("SECRET_ID", "prod/db-creds")
REGION = os.getenv("AWS_REGION", "eu-central-1")
REFRESH_INTERVAL = int(os.getenv("REFRESH_INTERVAL", "300"))
_session = boto3.session.Session()
_client = _session.client("secretsmanager", region_name=REGION, config=Config(retries={"max_attempts": 3}))
_lock = threading.RLock()
_cache = {"username": None, "password": None, "updated_at": 0}
def fetch_secret():
resp = _client.get_secret_value(SecretId=SECRET_ID)
secret = resp.get("SecretString") or resp.get("SecretBinary").decode()
data = json.loads(secret)
with _lock:
_cache.update({
"username": data["username"],
"password": data["password"],
"updated_at": int(time.time())
})
def get_db_creds():
with _lock:
return _cache["username"], _cache["password"]
def refresher():
while True:
try:
fetch_secret()
except Exception as e:
# Логируйте аккуратно, без секретов
print(f"[secrets] refresh failed: {e}")
time.sleep(REFRESH_INTERVAL)
def on_sighup(signum, frame):
print("[secrets] SIGHUP received, forcing refresh")
fetch_secret()
if __name__ == "__main__":
fetch_secret()
signal.signal(signal.SIGHUP, on_sighup)
t = threading.Thread(target=refresher, daemon=True)
t.start()
# Здесь — инициализация БД‑пула, используя get_db_creds()
# Пул должен уметь переустанавливать коннекты при ошибке аутентификации
while True:
time.sleep(60)
Идея: секрет подхватывается на старте и периодически обновляется; при SIGHUP можно форсировать перечитывание (например, когда CSI обновил файлы).
package main
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/lib/pq"
vault "github.com/hashicorp/vault/api"
)
type DBLease struct {
Username string
Password string
LeaseID string
TTL time.Duration
}
func getVaultCreds(ctx context.Context, client *vault.Client, path string) (*DBLease, error) {
secret, err := client.Logical().ReadWithContext(ctx, path)
if err != nil {
return nil, err
}
if secret == nil || secret.Data == nil {
return nil, fmt.Errorf("empty secret")
}
username := secret.Data["username"].(string)
password := secret.Data["password"].(string)
ttl := time.Duration(secret.LeaseDuration) * time.Second
return &DBLease{Username: username, Password: password, LeaseID: secret.LeaseID, TTL: ttl}, nil
}
func openDB(u, p, host, db string) (*sql.DB, error) {
dsn := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=require", u, p, host, db)
return sql.Open("postgres", dsn)
}
func main() {
ctx := context.Background()
vaultAddr := os.Getenv("VAULT_ADDR")
token := os.Getenv("VAULT_TOKEN")
path := os.Getenv("VAULT_DB_PATH") // например: database/creds/prod-reader
host := os.Getenv("PG_HOST")
dbname := os.Getenv("PG_DB")
cfg := vault.DefaultConfig()
cfg.Address = vaultAddr
client, err := vault.NewClient(cfg)
if err != nil { log.Fatal(err) }
client.SetToken(token)
lease, err := getVaultCreds(ctx, client, path)
if err != nil { log.Fatal(err) }
db, err := openDB(lease.Username, lease.Password, host, dbname)
if err != nil { log.Fatal(err) }
defer db.Close()
ticker := time.NewTicker(lease.TTL / 2)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Обновляем креды до истечения TTL
newLease, err := getVaultCreds(ctx, client, path)
if err != nil {
log.Printf("vault refresh failed: %v", err)
continue
}
// Переоткрываем пул (или обновляем коннекты по месту — зависит от драйвера)
newDB, err := openDB(newLease.Username, newLease.Password, host, dbname)
if err != nil {
log.Printf("db reconnect failed: %v", err)
continue
}
old := db
db = newDB
_ = old.Close()
lease = newLease
log.Printf("db creds rotated, ttl=%s", lease.TTL)
default:
// Используем db по работе
time.Sleep(2 * time.Second)
}
}
}
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.4
hooks:
- id: gitleaks
args: ["protect", "--staged"]
Установите pre-commit и подключите этот хук — он прервет коммит, если найдет токены/пароли.
Что мониторить:
План реагирования при утечке:
Если начать с инвентаризации и перевода хотя бы самых опасных секретов (платежи, БД прод) в централизованный менеджер с ротацией, риск больших потерь резко падает. Дальше — подключайте CI, Kubernetes и остальную инфраструктуру.