Проблема секретів у Kubernetes
Кожному застосунку потрібні секрети — паролі баз даних, API-ключі, TLS-сертифікати, ключі шифрування. У Kubernetes підхід за замовчуванням — зберігати їх у Kubernetes Secrets, які закодовані в base64 (не зашифровані) і доступні кожному з RBAC-доступом до неймспейсу.
Це не безпека. Це театр безпеки.
Поширені анти-патерни, які ми бачимо в продакшн-середовищах:
- Секрети, hardcoded в образах контейнерів — Експоновані в кожному реєстрі та логі збірки
- Секрети у змінних середовища — Видимі у виводі
kubectl describe pod - Секрети, закомічені в Git — Навіть "приватні" репо зламують
- Спільні статичні облікові дані — Без ротації, без аудиту, без терміну дії
HashiCorp Vault вирішує ці проблеми, надаючи централізоване управління секретами з динамічною генерацією облікових даних, автоматичною ротацією, детальним audit logging та точним контролем доступу.
Крок 1: Розгорніть Vault на Kubernetes за допомогою Helm
Рекомендований підхід — запускати Vault у режимі високої доступності з інтегрованим Raft-сховищем:
# Add the HashiCorp Helm repository
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
# Create a dedicated namespace
kubectl create namespace vault
# Install Vault with HA configuration
helm install vault hashicorp/vault \
--namespace vault \
--set server.ha.enabled=true \
--set server.ha.replicas=3 \
--set server.ha.raft.enabled=true \
--set server.dataStorage.size=10Gi \
--set server.dataStorage.storageClass=gp3 \
--set server.auditStorage.enabled=true \
--set server.auditStorage.size=10Gi \
--set injector.enabled=true \
--set ui.enabled=true
Після розгортання ініціалізуйте та розпечатайте перший Vault pod:
# Initialize Vault with 5 key shares, 3 required to unseal
kubectl exec -n vault vault-0 -- vault operator init \
-key-shares=5 \
-key-threshold=3 \
-format=json > vault-init.json
# Unseal the first pod (repeat with 3 different keys)
kubectl exec -n vault vault-0 -- vault operator unseal <key-1>
kubectl exec -n vault vault-0 -- vault operator unseal <key-2>
kubectl exec -n vault vault-0 -- vault operator unseal <key-3>
# Join the other pods to the Raft cluster
kubectl exec -n vault vault-1 -- vault operator raft join \
http://vault-0.vault-internal:8200
kubectl exec -n vault vault-2 -- vault operator raft join \
http://vault-0.vault-internal:8200
# Unseal the remaining pods
# (In production, use auto-unseal with AWS KMS, GCP KMS, or Azure Key Vault)
Для продакшн-розгортань завжди налаштовуйте auto-unseal, щоб уникнути ручного втручання після перезапуску podʼів:
# vault-config.hcl
seal "awskms" {
region = "us-east-1"
kms_key_id = "alias/vault-unseal-key"
}
Крок 2: Налаштуйте автентифікацію Kubernetes
Vault повинен перевіряти, що podʼи, що запитують секрети, є тими, ким себе видають. Метод автентифікації Kubernetes використовує токени service account для автентифікації:
# Login to Vault
export VAULT_ADDR="http://127.0.0.1:8200"
vault login <root-token>
# Enable Kubernetes auth method
vault auth enable kubernetes
# Configure it to communicate with the Kubernetes API
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"
Створіть політику, що визначає, до яких секретів сервіс може мати доступ:
# payment-service-policy.hcl
path "secret/data/payment-service/*" {
capabilities = ["read"]
}
path "database/creds/payment-service-role" {
capabilities = ["read"]
}
path "pki/issue/payment-service" {
capabilities = ["create", "update"]
}
Застосуйте політику і прив'яжіть її до Kubernetes service account:
# Write the policy
vault policy write payment-service payment-service-policy.hcl
# Create a role that maps Kubernetes SA to Vault policy
vault write auth/kubernetes/role/payment-service \
bound_service_account_names=payment-service \
bound_service_account_namespaces=production \
policies=payment-service \
ttl=1h
Крок 3: Впровадьте динамічні облікові дані бази даних
Статичні паролі бази даних — це зобов'язання: вони ніколи не закінчуються, спільні для середовищ і неможливо ротувати без простою. Database secrets engine у Vault генерує унікальні, короткоживучі облікові дані на вимогу.
# Enable the database secrets engine
vault secrets enable database
# Configure the PostgreSQL connection
vault write database/config/payments-db \
plugin_name=postgresql-database-plugin \
allowed_roles="payment-service-role" \
connection_url="postgresql://{{username}}:{{password}}@payments-db.production.svc:5432/payments?sslmode=require" \
username="vault_admin" \
password="initial-setup-password"
# Rotate the root password so only Vault knows it
vault write -force database/rotate-root/payments-db
# Create a role that generates credentials with a 1-hour TTL
vault write database/roles/payment-service-role \
db_name=payments-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="REVOKE ALL ON ALL TABLES IN SCHEMA public FROM \"{{name}}\"; DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
Тепер кожного разу, коли pod стартує, він отримує унікальні облікові дані бази даних, що автоматично закінчуються. Якщо облікові дані скомпрометовано, blast radius обмежений одним pod і однією годиною.
Крок 4: Інжектуйте секрети в podʼи за допомогою Vault Agent
Vault Agent Injector використовує Kubernetes mutating webhooks, щоб автоматично інжектувати секрети в podʼи через анотації. Жодних змін коду застосунку не потрібно.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "payment-service"
# Static secrets
vault.hashicorp.com/agent-inject-secret-config: "secret/data/payment-service/config"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/payment-service/config" -}}
export STRIPE_API_KEY="{{ .Data.data.stripe_api_key }}"
export WEBHOOK_SECRET="{{ .Data.data.webhook_secret }}"
{{- end }}
# Dynamic database credentials
vault.hashicorp.com/agent-inject-secret-db: "database/creds/payment-service-role"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "database/creds/payment-service-role" -}}
export DB_USERNAME="{{ .Data.username }}"
export DB_PASSWORD="{{ .Data.password }}"
{{- end }}
# Auto-renew credentials before they expire
vault.hashicorp.com/agent-cache-enable: "true"
vault.hashicorp.com/agent-cache-listener-port: "8200"
spec:
serviceAccountName: payment-service
containers:
- name: payment-service
image: myorg/payment-service:v1.2.3
command: ["/bin/sh", "-c"]
args:
- source /vault/secrets/config &&
source /vault/secrets/db &&
/app/payment-service
ports:
- containerPort: 8080
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Vault Agent sidecar обробляє автентифікацію, отримання секретів та автоматичне продовження — ваш застосунок просто читає файли або змінні середовища.
Крок 5: Альтернативний підхід з External Secrets Operator
Для команд, що віддають перевагу Kubernetes-нативному workflow, External Secrets Operator (ESO) автоматично синхронізує секрети Vault у Kubernetes Secrets:
# secret-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "http://vault.vault.svc:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: external-secrets
namespace: external-secrets
---
# external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payment-service-secrets
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: payment-service-secrets
creationPolicy: Owner
data:
- secretKey: stripe-api-key
remoteRef:
key: secret/data/payment-service/config
property: stripe_api_key
- secretKey: webhook-secret
remoteRef:
key: secret/data/payment-service/config
property: webhook_secret
Цей підхід добре працює, коли ваші застосунки вже читають з Kubernetes Secrets і ви хочете додати Vault як джерело правди без зміни коду застосунку.
Крок 6: Увімкніть audit logging
У регульованих середовищах ви повинні демонструвати, хто отримував доступ до яких секретів і коли. Увімкніть audit log Vault:
# Enable file-based audit logging
vault audit enable file file_path=/vault/audit/audit.log
# For production, ship logs to your SIEM
# Example: enable syslog backend
vault audit enable syslog tag="vault" facility="AUTH"
Кожна операція Vault — читання, запис, спроби автентифікації, зміни політик — записується з повним запитом і відповіддю (чутливі значення HMAC'ed). Пересилайте ці логи у ваш SIEM (Splunk, Elastic або Datadog) для сповіщень на підозрілі патерни доступу.
Чеклист зміцнення для продакшну
Перед виходом у продакшн перевірте ці пункти:
- Auto-unseal налаштовано з cloud KMS (ніколи не зберігайте unseal-ключі на диску)
- TLS увімкнено для всієї комунікації Vault (бажано mTLS)
- Audit logging увімкнено і перенаправляється до SIEM
- Root token відкликано після початкового налаштування (використовуйте identity-based auth)
- Namespaces налаштовано для ізоляції команд (Vault Enterprise)
- Disaster recovery replication налаштовано між регіонами
- Backup і restore протестовано зі snapshots Raft
- Sentinel policies забезпечують організаційні правила (Vault Enterprise)
- Ліміти ресурсів встановлено на podʼи Vault, щоб запобігти проблемам noisy-neighbor
- Network policies обмежують, які podʼи можуть досягати Vault
# Take a Raft snapshot for backup
vault operator raft snapshot save /tmp/vault-backup.snap
# Verify the snapshot
vault operator raft snapshot inspect /tmp/vault-backup.snap
# Automate daily backups with a CronJob
# vault-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-backup
namespace: vault
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: vault-backup
containers:
- name: backup
image: hashicorp/vault:1.15
command:
- /bin/sh
- -c
- |
vault operator raft snapshot save /tmp/vault-$(date +%Y%m%d).snap
aws s3 cp /tmp/vault-$(date +%Y%m%d).snap \
s3://my-vault-backups/$(date +%Y%m%d).snap
env:
- name: VAULT_ADDR
value: "http://vault-active.vault.svc:8200"
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: vault-backup-token
key: token
restartPolicy: OnFailure
Висновок
Управління секретами не є опціональним — це фундаментальна вимога безпеки, яку лише складніше додати пізніше, коли ваша інфраструктура зростає. HashiCorp Vault, належно розгорнутий на Kubernetes, надає динамічні секрети, автоматичну ротацію, точний контроль доступу та вичерпний audit logging, з якими статичні секрети просто не можуть зрівнятися.
У DevOpsVibe ми розгортали і керували Vault у десятках Kubernetes-кластерів для організацій від стартапів до enterprise. Чи потрібна вам допомога з початковим розгортанням, міграцією зі статичних секретів чи інтеграцією з вашими існуючими CI/CD-конвеєрами — наша команда може довести вас до управління секретами продакшн-рівня за тижні, а не місяці. Захистіть свою інфраструктуру сьогодні.