HashiCorp Vault для управления секретами: как мы закрыли требования PCI DSS в финтехе

Ситуация: секреты в .env файлах и Git-репозитории

Финтех-компания «КриптоПей» обратилась к нам в itfresh.ru с запросом на подготовку к аудиту PCI DSS. При предварительной проверке мы обнаружили критическую проблему: секреты хранились в незашифрованных .env файлах на серверах, а часть паролей — прямо в Git-репозитории.

Масштаб проблемы:

  • 47 .env файлов на 12 серверах — пароли от баз данных, API-ключи платёжных провайдеров, TLS-сертификаты
  • 23 hardcoded пароля в Git-истории (в том числе master password от production MySQL)
  • Ротация паролей — не проводилась никогда за 3 года работы
  • Доступ — любой разработчик (18 человек) имел SSH-доступ ко всем серверам и видел все секреты
  • Аудит — невозможно определить, кто и когда читал конкретный секрет

По требованиям PCI DSS (пункты 3.4, 6.5, 8.2): секреты должны храниться зашифрованными, доступ должен быть ограничен по принципу least privilege, все обращения к секретам должны логироваться, а пароли — ротироваться не реже чем раз в 90 дней.

Установка Vault в HA-режиме с Consul

Для production-инсталляции нужен High Availability — если Vault недоступен, ни одно приложение не сможет получить секреты и перестанет работать. Мы развернули кластер из 3 нод Vault с Consul в качестве бэкенда хранения:

# Установка Vault (все 3 ноды)
curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
  | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y vault consul

Конфигурация Consul кластера (3 ноды):

# /etc/consul.d/consul.hcl (нода 1)
datacenter = "dc1"
data_dir   = "/opt/consul/data"
log_level  = "INFO"
server     = true
bootstrap_expect = 3

bind_addr  = "10.0.1.10"
client_addr = "0.0.0.0"

retry_join = ["10.0.1.10", "10.0.1.11", "10.0.1.12"]

encrypt = "CONSUL_GOSSIP_KEY_BASE64"

ui_config {
  enabled = false
}

performance {
  raft_multiplier = 1
}

tls {
  defaults {
    ca_file   = "/etc/consul.d/tls/ca.pem"
    cert_file = "/etc/consul.d/tls/consul.pem"
    key_file  = "/etc/consul.d/tls/consul-key.pem"
    verify_incoming = true
    verify_outgoing = true
  }
}

Конфигурация Vault:

# /etc/vault.d/vault.hcl
cluster_name = "cryptopay-vault"
log_level    = "info"

storage "consul" {
  address = "127.0.0.1:8500"
  path    = "vault/"
  scheme  = "https"
  tls_ca_file   = "/etc/consul.d/tls/ca.pem"
  tls_cert_file = "/etc/consul.d/tls/consul.pem"
  tls_key_file  = "/etc/consul.d/tls/consul-key.pem"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_cert_file = "/etc/vault.d/tls/vault.pem"
  tls_key_file  = "/etc/vault.d/tls/vault-key.pem"
  tls_min_version = "tls12"
}

api_addr     = "https://10.0.1.10:8200"
cluster_addr = "https://10.0.1.10:8201"

ui = true

telemetry {
  prometheus_retention_time = "24h"
  disable_hostname = true
}

Seal/Unseal и Auto-Unseal

После инициализации Vault зашифрован (sealed) и не может обслуживать запросы. Для расшифровки нужен unseal key. По умолчанию Vault использует Shamir's Secret Sharing — мастер-ключ разделяется на N частей, и для unseal требуется M из N:

# Инициализация Vault (один раз)
vault operator init -key-shares=5 -key-threshold=3

# Результат:
# Unseal Key 1: abc123...
# Unseal Key 2: def456...
# Unseal Key 3: ghi789...
# Unseal Key 4: jkl012...
# Unseal Key 5: mno345...
# Initial Root Token: hvs.xxxxxxxx

# КРИТИЧНО: каждый ключ — отдельному человеку!
# Key 1 → CTO
# Key 2 → Lead DevOps
# Key 3 → Security Officer
# Key 4 → Head of Development
# Key 5 → сейф (физический)

# Unseal (нужны 3 из 5 ключей)
vault operator unseal  # вводим ключ 1
vault operator unseal  # вводим ключ 2
vault operator unseal  # вводим ключ 3

vault status
# Sealed: false
# HA Enabled: true
# HA Mode: active

Для production мы настроили Auto-Unseal через Transit — отдельный Vault-инстанс хранит unseal-ключ и автоматически расшифровывает основной при перезапуске:

# vault.hcl — auto-unseal через Transit
seal "transit" {
  address    = "https://vault-transit.internal:8200"
  token      = "hvs.transit-token"
  key_name   = "autounseal"
  mount_path = "transit/"
  tls_ca_cert = "/etc/vault.d/tls/ca.pem"
}

Теперь при перезапуске любой ноды Vault автоматически расшифровывается без участия людей. Transit-Vault защищён физическим Shamir's unseal — он перезагружается крайне редко.

KV Secrets Engine и Dynamic Database Credentials

Все статические секреты (API-ключи, пароли сторонних сервисов) мы перенесли в KV Secrets Engine v2:

# Включаем KV v2
vault secrets enable -path=secret kv-v2

# Сохраняем секреты с метаданными
vault kv put secret/production/payment-service \
  stripe_api_key="sk_live_xxxxx" \
  stripe_webhook_secret="whsec_xxxxx" \
  db_password="P@ssw0rd_pr0d_2026"

# Чтение с версионированием
vault kv get secret/production/payment-service
vault kv get -version=1 secret/production/payment-service

# Метаданные — кто и когда менял
vault kv metadata get secret/production/payment-service

Но главная ценность Vault — dynamic secrets. Вместо статических паролей к базам данных приложения получают временные credentials, которые автоматически удаляются:

# Настраиваем Database Secrets Engine для PostgreSQL
vault secrets enable database

vault write database/config/cryptopay-db \
  plugin_name=postgresql-database-plugin \
  connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/cryptopay?sslmode=require" \
  allowed_roles="payment-readonly,payment-readwrite" \
  username="vault_admin" \
  password="vault_admin_password"

# Роль для микросервиса payment — read/write, TTL 1 час
vault write database/roles/payment-readwrite \
  db_name=cryptopay-db \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
    GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA payments TO \"{{name}}\"; \
    GRANT USAGE ON ALL SEQUENCES IN SCHEMA payments TO \"{{name}}\";" \
  revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"

# Приложение запрашивает credentials
vault read database/creds/payment-readwrite
# username: v-approle-payment-readwrite-abc123
# password: A1B2c3D4-random-generated
# lease_duration: 1h
# lease_id: database/creds/payment-readwrite/lease-id-xxx

Через час credentials автоматически отзываются, а пользователь удаляется из PostgreSQL. Если сервис компрометирован — злоумышленник получает доступ максимум на 1 час. При статических паролях — навсегда.

PKI Engine: автоматическая выдача TLS-сертификатов

«КриптоПей» использовал самоподписанные сертификаты для внутренних сервисов, а для внешних — Let's Encrypt с ручным обновлением. Мы развернули внутренний PKI через Vault:

# Создаём Root CA (срок — 10 лет, хранится в Vault)
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki

vault write -field=certificate pki/root/generate/internal \
  common_name="CryptoPay Internal CA" \
  issuer_name="root-ca" \
  ttl=87600h > /tmp/root-ca.pem

# Создаём Intermediate CA (срок — 3 года)
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int

vault write -format=json pki_int/intermediate/generate/internal \
  common_name="CryptoPay Intermediate CA" \
  issuer_name="int-ca" \
  | jq -r '.data.csr' > /tmp/int-ca.csr

vault write -format=json pki/root/sign-intermediate \
  csr=@/tmp/int-ca.csr \
  format=pem_bundle \
  ttl=43800h \
  | jq -r '.data.certificate' > /tmp/int-ca.pem

vault write pki_int/intermediate/set-signed \
  certificate=@/tmp/int-ca.pem

# Роль для выдачи сертификатов сервисам
vault write pki_int/roles/internal-service \
  allowed_domains="internal,svc.cluster.local" \
  allow_subdomains=true \
  max_ttl=720h \
  key_type=ec \
  key_bits=256 \
  require_cn=false

# Выдаём сертификат для payment-service
vault write pki_int/issue/internal-service \
  common_name="payment-service.internal" \
  alt_names="payment-service.svc.cluster.local" \
  ttl=168h

Результат: каждый сервис получает TLS-сертификат на 7 дней, который автоматически обновляется через Vault Agent. Компрометация одного сертификата не затрагивает остальные, а срок жизни достаточно короткий, чтобы украденный сертификат быстро стал бесполезным.

AppRole и Kubernetes Auth: аутентификация приложений

Как приложения аутентифицируются в Vault? Для VM-based сервисов мы используем AppRole, для Kubernetes — Kubernetes Auth:

# === AppRole для VM-based сервисов ===
vault auth enable approle

# Создаём роль для payment-service
vault write auth/approle/role/payment-service \
  token_policies="payment-secrets" \
  token_ttl=1h \
  token_max_ttl=4h \
  secret_id_ttl=720h \
  secret_id_num_uses=0

# Получаем role_id (публичный, зашивается в конфиг)
vault read auth/approle/role/payment-service/role-id
# role_id: 7c3c43e7-xxxx-xxxx-xxxx

# Генерируем secret_id (секретный, доставляется через CI/CD)
vault write -f auth/approle/role/payment-service/custom-secret-id \
  secret_id="DELIVERED_VIA_CICD"

# Приложение авторизуется
vault write auth/approle/login \
  role_id="7c3c43e7-xxxx" \
  secret_id="DELIVERED_VIA_CICD"
# Получает token, с которым читает секреты
# === Kubernetes Auth для K8s-сервисов ===
vault auth enable kubernetes

vault write auth/kubernetes/config \
  kubernetes_host="https://kubernetes.default.svc:443" \
  kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

vault write auth/kubernetes/role/payment-service \
  bound_service_account_names="payment-service" \
  bound_service_account_namespaces="production" \
  policies="payment-secrets" \
  ttl=1h

Vault Agent Injector для Kubernetes — sidecar-контейнер, который автоматически получает секреты и монтирует их как файлы в pod:

# deployment.yaml с аннотациями для Vault Agent Injector
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "payment-service"
        vault.hashicorp.com/agent-inject-secret-db: "database/creds/payment-readwrite"
        vault.hashicorp.com/agent-inject-template-db: |
          {{- with secret "database/creds/payment-readwrite" -}}
          export DB_USER="{{ .Data.username }}"
          export DB_PASS="{{ .Data.password }}"
          {{- end }}
        vault.hashicorp.com/agent-inject-secret-config: "secret/data/production/payment-service"
        vault.hashicorp.com/agent-inject-template-config: |
          {{- with secret "secret/data/production/payment-service" -}}
          export STRIPE_KEY="{{ .Data.data.stripe_api_key }}"
          {{- end }}
    spec:
      serviceAccountName: payment-service
      containers:
        - name: payment-service
          image: cryptopay/payment-service:v2.1
          command: ["/bin/sh", "-c", "source /vault/secrets/db && source /vault/secrets/config && ./app"]

Policies и Audit Logging

Политики Vault написаны на HCL и определяют, какие пути доступны каждой роли:

# payment-secrets.hcl
# payment-service может:
# - читать свои секреты
# - запрашивать database credentials
# - выпускать TLS-сертификаты

path "secret/data/production/payment-service" {
  capabilities = ["read"]
}

path "secret/data/production/shared/*" {
  capabilities = ["read"]
}

path "database/creds/payment-readwrite" {
  capabilities = ["read"]
}

path "pki_int/issue/internal-service" {
  capabilities = ["update"]
}

# Запрещаем всё остальное (deny по умолчанию)
path "secret/data/production/user-service" {
  capabilities = ["deny"]
}

path "sys/*" {
  capabilities = ["deny"]
}
# Применяем политику
vault policy write payment-secrets payment-secrets.hcl

# Проверяем что может делать токен
vault token capabilities hvs.token_xxx secret/data/production/payment-service
# read

vault token capabilities hvs.token_xxx secret/data/production/user-service
# deny

Audit logging — обязательное требование PCI DSS. Vault логирует каждое обращение:

# Включаем audit log в файл
vault audit enable file file_path=/var/log/vault/audit.log

# Включаем audit log в syslog (для SIEM)
vault audit enable syslog tag="vault" facility="AUTH"

# Пример записи аудита (JSON, одна строка — развёрнуто для читаемости)
{
  "type": "response",
  "time": "2026-04-05T10:23:15.442Z",
  "auth": {
    "client_token": "hmac-sha256:xxxxx",
    "accessor": "hmac-sha256:xxxxx",
    "display_name": "approle-payment-service",
    "policies": ["default", "payment-secrets"],
    "token_type": "service"
  },
  "request": {
    "id": "req-uuid-xxx",
    "operation": "read",
    "path": "secret/data/production/payment-service",
    "remote_address": "10.0.2.15"
  },
  "response": {
    "data": {
      "data": {
        "stripe_api_key": "hmac-sha256:yyyyy",
        "db_password": "hmac-sha256:zzzzz"
      }
    }
  }
}

Обратите внимание: значения секретов в аудит-логе заменены HMAC-хешами. Это позволяет искать обращения к конкретному секрету (HMAC детерминированный), но не раскрывает сами значения.

Результаты и сравнение с AWS Secrets Manager

После 6 недель миграции все секреты «КриптоПей» переехали в Vault:

МетрикаДо (файлы .env)После (Vault)
Секретов в Git230
.env файлов на серверах470
Ротация паролей БДНикогдаКаждый час (dynamic)
Аудит доступа к секретамОтсутствует100% запросов
Время получения секретаcat .env (мгновенно)3-5 мс (API)
Людей с доступом ко всем секретам18 (все разработчики)2 (Security Officer + CTO)

Сравнение Vault с AWS Secrets Manager для тех, кто выбирает:

КритерийHashiCorp VaultAWS Secrets Manager
СтоимостьБесплатно (OSS) / $1.58/hr (Enterprise)$0.40/секрет/мес + $0.05/10K API
Dynamic secretsДа (DB, AWS, PKI, SSH, ...)Только через Lambda rotation
PKIВстроенный CAНет (ACM отдельно)
Multi-cloudДаТолько AWS
СложностьВысокая (HA, unseal, upgrade)Низкая (managed)
Контроль данныхПолный (on-premise)AWS управляет шифрованием

Для «КриптоПей» Vault подошёл идеально: полный контроль данных (требование PCI DSS), dynamic credentials для баз данных, встроенный PKI для mTLS между сервисами. Если вашему проекту нужен audit-ready secrets management — обращайтесь к нам в itfresh.ru.

Часто задаваемые вопросы

Приложения, которые уже получили секреты (через Vault Agent или environment variables), продолжат работать до истечения TTL токена или lease. Новые секреты получить не смогут. Поэтому HA-режим обязателен: 3 ноды Vault + 3 ноды Consul дают SLA 99.99%. Также рекомендуем кэширование через Vault Agent с persist-кэшем на диск.
Поэтапно: 1) загружаете все секреты в Vault (vault kv put), 2) настраиваете приложение читать сначала из Vault, а при ошибке — из .env (fallback), 3) тестируете 1-2 недели, 4) удаляете .env файлы. Vault Agent упрощает процесс — он записывает секреты в файл, и приложение не нужно менять.
Могут, если TTL слишком короткий. При TTL 1 час и пуле из 20 соединений — раз в час все 20 переподключатся. Это штатная операция, но нужно обработать в коде: при ошибке аутентификации — запросить новые credentials и переподключиться. Большинство ORM (SQLAlchemy, GORM) поддерживают reconnect из коробки.
Да, Vault OSS полностью бесплатен и содержит все основные функции: KV, dynamic secrets, PKI, AppRole, Kubernetes auth, audit. Enterprise добавляет: namespaces (мультитенантность), Sentinel policies, HSM support, DR replication. Для компании до 50 разработчиков OSS-версии обычно достаточно.
Мгновенно. Команда vault lease revoke -prefix database/creds/ отзывает все dynamic credentials за секунды — Vault подключается к PostgreSQL и удаляет пользователей. Для KV-секретов: меняете значение через vault kv put, и при следующем запросе приложение получит новое. Vault Agent обновляет секреты каждые 5 минут по умолчанию.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#HashiCorp Vault#secrets management#PCI DSS#dynamic credentials#PKI#AppRole#Kubernetes auth#Consul
Комментарии 0

Оставить комментарий

загрузка...