Перейти к содержимому

mTLS, SPIFFE/SPIRE, Secrets Management, KMS

Зачем знать на Middle 3: на этом уровне инженер не просто пользуется готовой инфраструктурой безопасности, а проектирует её: настраивает Zero-Trust внутри сервиса, ротирует сертификаты без даунтайма, интегрирует SPIRE с Kubernetes, выбирает между Vault, AWS KMS и Sealed Secrets. Compliance (PCI-DSS, GDPR, HIPAA) и сертификационные аудиты ждут от tech lead умения объяснить, почему data key никогда не покидает HSM и как устроена envelope encryption.

  1. Концепция mTLS, SPIFFE и Secrets
  2. Глубже: PKI, SVID, Vault, KMS, envelope encryption
  3. Gotchas / Best practices
  4. Real cases (Istio, Cloudflare, банковские системы)
  5. Вопросы (20)
  6. Practice
  7. Источники

Обычный TLS аутентифицирует сервер: клиент проверяет, что говорит с настоящим api.example.com. mTLS добавляет вторую сторону: сервер проверяет клиента по сертификату.

Client Server
| |
| --- ClientHello -------------->|
|<-- ServerHello, Cert, Req --- | (Certificate Request)
| --- ClientCert, Verify ------>| (клиент шлёт свой сертификат)
|<-- Finished ------------------|
| |
| <== Encrypted traffic ====> |

Зачем mTLS:

  • Zero-Trust: каждое соединение аутентифицировано, даже внутри VPN.
  • Замена сетевых ACL: сервис A может вызывать B, только если у него правильный cert.
  • Compliance: PCI-DSS требует strong authentication.

TLS 1.3 (RFC 8446) сократил handshake до 1-RTT и добавил 0-RTT (early data):

  • Клиент шлёт данные в первом же пакете на основе PSK (pre-shared key) от прошлой сессии.
  • Минус: replay attack. Атакующий может перехватить 0-RTT request и переотправить.
  • Защита: применять 0-RTT только для идемпотентных запросов (GET без побочных эффектов), или иметь nonce-based replay protection.

В Go: tls.Config{MinVersion: tls.VersionTLS13} обязательно для современных сервисов.

SPIFFE (Secure Production Identity Framework For Everyone) — стандарт для назначения identity workloads.

  • SVID (SPIFFE Verifiable Identity Document): сертификат, доказывающий identity.
    • X.509-SVID — стандартный сертификат.
    • JWT-SVID — токен для случаев без TLS (например, gRPC metadata).
  • Trust domain — URI, идентифицирующий организацию: spiffe://prod.example.com.
  • SPIFFE ID — URI workload: spiffe://prod.example.com/ns/payments/sa/billing.

SPIRE (SPIFFE Runtime Environment) — референсная имплементация:

  • Server — выдаёт SVID workload’ам.
  • Agent — на каждой ноде, доставляет SVID процессам через Unix Domain Socket.
  • Workload attestation — Agent проверяет, кто запросил SVID (по UID, K8s ServiceAccount, AWS instance metadata).

Категории secrets:

  • Static (DB password, API key) — редко меняется.
  • Dynamic (DB user с TTL) — генерится при запросе.
  • Encryption keys — никогда не покидают KMS/HSM.

Решения:

  • HashiCorp Vault — централизованный secret store.
  • AWS Secrets Manager / GCP Secret Manager — managed.
  • Sealed Secrets (Bitnami) — encrypt в git.
  • SOPS (Mozilla) — encrypt files (YAML/JSON) в git.
  • K8s Secrets — base64 (не шифрование!), нужно encryption at rest.

KMS (Key Management Service) хранит master keys в HSM. Прямое шифрование больших объёмов через KMS — медленно (один RPC на каждый блок). Envelope encryption:

Data
|
| encrypt with DEK (Data Encryption Key, AES-256)
v
Encrypted data + Encrypted DEK (KEK encrypts DEK)
|
| KEK (Key Encryption Key) сидит в KMS

DEK генерится локально, шифрует данные, сама шифруется через KMS (один RPC). Хранится рядом с данными.


package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
)
func mTLSServer() {
// 1. CA cert для проверки клиентов
caCert, err := os.ReadFile("ca.crt")
if err != nil {
panic(err)
}
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
// 2. Server cert + key
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
panic(err)
}
cfg := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert, // обязательный mTLS
ClientCAs: caPool,
MinVersion: tls.VersionTLS13,
// Кастомная логика поверх стандартной валидации
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
for _, chain := range verifiedChains {
if len(chain) > 0 {
// Проверяем SPIFFE URI в SAN
for _, uri := range chain[0].URIs {
if uri.Scheme == "spiffe" && uri.Host == "prod.example.com" {
return nil
}
}
}
}
return fmt.Errorf("client SPIFFE ID not allowed")
},
}
srv := &http.Server{
Addr: ":8443",
TLSConfig: cfg,
Handler: http.DefaultServeMux,
}
if err := srv.ListenAndServeTLS("", ""); err != nil {
panic(err)
}
}

Сертификаты в production должны быть короткоживущими (24-48 часов). Их нужно ротировать без рестарта сервиса.

Решение: GetCertificate callback в tls.Config.

package main
import (
"crypto/tls"
"sync/atomic"
"time"
"github.com/fsnotify/fsnotify"
)
type CertWatcher struct {
cert atomic.Pointer[tls.Certificate]
certPath string
keyPath string
}
func NewCertWatcher(certPath, keyPath string) (*CertWatcher, error) {
w := &CertWatcher{certPath: certPath, keyPath: keyPath}
if err := w.reload(); err != nil {
return nil, err
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
watcher.Add(certPath)
watcher.Add(keyPath)
go func() {
for ev := range watcher.Events {
// debounce: cert + key обновляются почти одновременно
if ev.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) != 0 {
time.Sleep(500 * time.Millisecond)
if err := w.reload(); err != nil {
// log error, продолжить со старым cert
continue
}
}
}
}()
return w, nil
}
func (w *CertWatcher) reload() error {
cert, err := tls.LoadX509KeyPair(w.certPath, w.keyPath)
if err != nil {
return err
}
w.cert.Store(&cert)
return nil
}
func (w *CertWatcher) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
return w.cert.Load(), nil
}

Использование:

cfg := &tls.Config{
GetCertificate: watcher.GetCertificate,
MinVersion: tls.VersionTLS13,
}

Tip: fsnotify может пропускать события на NFS / эфемерных volumes. Для production надёжнее проверять periodic poll каждые 30 секунд + сравнивать not_after.

package main
import (
"context"
"log"
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
"github.com/spiffe/go-spiffe/v2/workloadapi"
)
func main() {
ctx := context.Background()
// Подключение к SPIRE Agent через Unix Domain Socket
source, err := workloadapi.NewX509Source(
ctx,
workloadapi.WithClientOptions(
workloadapi.WithAddr("unix:///run/spire/sockets/agent.sock"),
),
)
if err != nil {
log.Fatalf("unable to create X509Source: %v", err)
}
defer source.Close()
// Authorize только конкретный SPIFFE ID
tlsConfig := tlsconfig.MTLSServerConfig(
source,
source,
tlsconfig.AuthorizeID(spiffeid.RequireFromString("spiffe://example.org/client")),
)
// ...используем tlsConfig в http.Server
}

SPIRE Agent сам обнаружит, кто этот процесс (по PID → UID/SA), и выдаст соответствующий SVID. Ротация происходит автоматически.

package main
import (
"context"
vault "github.com/hashicorp/vault/api"
auth "github.com/hashicorp/vault/api/auth/kubernetes"
)
func getDBCredsFromVault(ctx context.Context) (user, pass string, err error) {
cfg := vault.DefaultConfig()
cfg.Address = "https://vault.internal:8200"
cl, err := vault.NewClient(cfg)
if err != nil {
return "", "", err
}
// Auth через K8s ServiceAccount JWT
k8sAuth, err := auth.NewKubernetesAuth("payments-role")
if err != nil {
return "", "", err
}
_, err = cl.Auth().Login(ctx, k8sAuth)
if err != nil {
return "", "", err
}
// Запрос dynamic credentials с TTL=1h
secret, err := cl.Logical().ReadWithContext(ctx, "database/creds/payments-readonly")
if err != nil {
return "", "", err
}
user = secret.Data["username"].(string)
pass = secret.Data["password"].(string)
return user, pass, nil
}

Vault создаёт пользователя в Postgres только в момент запроса, отзывает через TTL. Если credentials утекли — урон ограничен временем жизни.

package main
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/kms/types"
)
type Envelope struct {
EncryptedDEK []byte
Nonce []byte
Ciphertext []byte
}
func encryptEnvelope(ctx context.Context, kmsCl *kms.Client, kekID string, plaintext []byte) (*Envelope, error) {
// 1. KMS генерит DEK (plaintext + encrypted)
out, err := kmsCl.GenerateDataKey(ctx, &kms.GenerateDataKeyInput{
KeyId: &kekID,
KeySpec: types.DataKeySpecAes256,
})
if err != nil {
return nil, err
}
defer zeroize(out.Plaintext) // обнуляем DEK после использования
// 2. Локально шифруем AES-GCM
block, err := aes.NewCipher(out.Plaintext)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return &Envelope{
EncryptedDEK: out.CiphertextBlob, // храним
Nonce: nonce,
Ciphertext: ciphertext,
}, nil
}
func zeroize(b []byte) {
for i := range b {
b[i] = 0
}
}

Дешифровка:

  1. kms.Decrypt(EncryptedDEK) → plaintext DEK.
  2. AES-GCM Open с DEK + Nonce → plaintext.

Для публичных endpoint:

import (
"crypto/tls"
"golang.org/x/crypto/acme/autocert"
)
func publicTLS() *tls.Config {
m := &autocert.Manager{
Cache: autocert.DirCache("/var/cache/autocert"),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("api.example.com"),
}
return m.TLSConfig() // ALPN-01, HTTP-01 challenge
}

Подводный камень: autocert.DirCache пишет в локальную ФС. В Kubernetes с несколькими репликами нужен общий cache (Redis, S3) — иначе каждый pod получит свой rate limit от Let’s Encrypt (50 certs/week).

cert-manager в Kubernetes:

  • Issuer / ClusterIssuer (Vault, ACME, self-signed CA).
  • Certificate resource описывает желаемый сертификат.
  • Контроллер периодически переоформляет (renewBefore: 30d).
  • Хранит cert в Secret, который монтируется в Pod.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: payments-tls
namespace: payments
spec:
secretName: payments-tls
duration: 24h
renewBefore: 8h
issuerRef:
name: internal-ca
kind: ClusterIssuer
dnsNames:
- payments.payments.svc.cluster.local
uris:
- spiffe://prod.example.com/ns/payments/sa/billing

⚠️ mTLS без CRL/OCSP — отзыв сертификата не работает. Альтернатива: короткий TTL (1-24h) делает CRL ненужным.

⚠️ Self-signed CA в Go требует x509.CertPool, а не системного store. Не забыть InsecureSkipVerify: false (никогда не ставить true в production!).

⚠️ 0-RTT replay: применяйте только для идемпотентных HTTP-методов. http.Server Go по умолчанию не включает 0-RTT.

⚠️ fsnotify пропускает события на overlay FS (Docker), NFS. Дополняйте polling’ом.

⚠️ Vault token TTL: дефолтный токен живёт 32 дня, но lease для secrets — иной. Не путать.

⚠️ SPIRE Agent ↔ Server: если Agent не может достучаться до Server, новые workloads не получат SVID, но существующие продолжат работать до истечения. Не сразу обнаружишь проблему.

⚠️ KMS rate limit: AWS KMS — 5500 req/s по умолчанию. Envelope encryption критически важен; не шифровать каждое поле напрямую.

⚠️ DEK в памяти: после использования обнуляй (runtime.KeepAlive + ручная заливка нулями). GC не гарантирует освобождение секретов.

⚠️ Base64 в K8s Secret — НЕ шифрование. Включай encryption at rest на etcd (EncryptionConfiguration).

⚠️ Sealed Secrets: ключ кластера ротируется. При восстановлении кластера старые SealedSecrets не дешифруются без backup ключа.

⚠️ TLS 1.2 vs 1.3 ciphers: в TLS 1.3 CipherSuites в tls.Config игнорируется (фиксированный список). Не пытайтесь disable конкретные suite.

  • Short-lived certs (24h) — отказаться от CRL.
  • Workload identity (SPIFFE) — не username/password между сервисами.
  • Defence in depth: mTLS + AuthZ policy + network policy.
  • Secret zero: первоначальный токен для bootstrap (Vault root) — храни в HSM или одноразово.
  • Rotation drills — раз в квартал учения по ротации root CA.
  • Audit log: все secret reads пишутся в SIEM.
  • Least privilege: Vault policy на чтение только конкретного path.
  • Encrypt headers/cookies: не только тело — иногда токен в URL засветится.

Istio назначает каждому Pod identity в формате spiffe://cluster.local/ns/<ns>/sa/<sa>. Envoy sidecar terminates TLS. Приложение шлёт plain HTTP, Envoy auto-upgrade до mTLS.

  • Все сертификаты выдаёт Istiod через CSR.
  • Ротация — каждые 24h.
  • AuthorizationPolicy ограничивает: from.source.principals: "cluster.local/ns/payments/sa/billing".

Урок: Service Mesh снимает большую часть сложности mTLS с приложения, но добавляет операционных вопросов (Istiod как SPoF, multi-cluster mesh).

Cloudflare выдаёт сертификаты для origin (бэкенд), действительные 15 лет, доверяемые только Cloudflare. Это нестандартный CA — публичные TLS клиенты их не примут. Используется для защиты бэкенда от прямого доступа.

PAN (номер карты) никогда не хранится в plain — заменяется на token (UUID), а сам PAN шифруется в vault и доступен только через короткоживущий запрос. Поля шифруются через AWS KMS envelope. Логи маскируются (411111******1111).

Сервис при старте читает database/creds/role-x. Vault создаёт уникального юзера в Postgres с TTL=1h. Сервис кеширует, при подходе к expiry запрашивает заново. Если pod падает — Vault sweeps unused.

Результат: даже если postgres dump утечёт, password будет уже невалидным.

В 2022-2023 атаки на Go: автор удалял аккаунт, кто-то перерегистрировал имя и подменял модуль. Go module proxy с immutable хешами в go.sum защищает от такого, но требует обновления go.sum сразу при добавлении модуля и проверки PR на изменения go.sum.


  1. Чем mTLS отличается от обычного TLS?
  2. Что такое 0-RTT в TLS 1.3 и в чём risk replay attack?
  3. Что такое SPIFFE ID и из чего состоит trust domain?
  4. Как SPIRE Agent аттестует workload в Kubernetes?
  5. В чём разница X509-SVID vs JWT-SVID?
  6. Как ротировать сертификат в Go без рестарта?
  7. Что делает GetCertificate callback в tls.Config?
  8. Опиши envelope encryption. Зачем нужен DEK отдельно от KEK?
  9. Какие auth methods поддерживает Vault?
  10. Что такое dynamic secrets в Vault и где их использовать?
  11. Чем Sealed Secrets отличается от SOPS?
  12. Почему base64 в Kubernetes Secret — не шифрование?
  13. Что такое encryption at rest для etcd?
  14. Какие ограничения у Let’s Encrypt rate limit?
  15. Как обеспечить ротацию root CA без даунтайма?
  16. Что произойдёт, если CRL/OCSP сервер недоступен?
  17. Как защитить DEK в памяти Go-процесса?
  18. Чем Istio mTLS отличается от ручного mTLS в Go-приложении?
  19. Что такое VerifyPeerCertificate и когда его использовать?
  20. Как Vault PKI engine выдаёт сертификаты и чем отличается от cert-manager?

  1. Развернуть локально SPIRE Server + Agent в minikube, выдать SVID для двух подов, протестировать mTLS.
  2. Написать Go-сервис, который читает cert из файла, реализует hot-reload через fsnotify + polling.
  3. Реализовать envelope encryption поверх AWS KMS (или LocalStack) для пользовательских PII.
  4. Настроить cert-manager + ClusterIssuer (self-signed) и выдать сертификат с SPIFFE URI в SAN.
  5. Vault: настроить database/postgresql secret engine, написать клиент, который продлевает lease.
  6. Реализовать VerifyPeerCertificate, который смотрит только на SPIFFE ID в URI SAN.
  7. Sigstore: подписать Docker image через cosign, проверить подпись.
  8. Sealed Secrets: зашифровать Secret через kubeseal, закоммитить, развернуть.

  1. RFC 8446 — TLS 1.3
  2. RFC 5280 — Internet X.509 PKI Certificate
  3. SPIFFE specification: https://github.com/spiffe/spiffe
  4. SPIRE docs: https://spiffe.io/docs/latest/spire-about/
  5. HashiCorp Vault: https://developer.hashicorp.com/vault/docs
  6. AWS KMS Best Practices: https://docs.aws.amazon.com/kms/latest/developerguide/best-practices.html
  7. Cloud Native Security WhitePaper (CNCF)
  8. Google Beyond Corp paper (Zero Trust)
  9. NIST SP 800-57 — Recommendation for Key Management
  10. Go crypto/tls docs: https://pkg.go.dev/crypto/tls
  11. cert-manager: https://cert-manager.io/docs/
  12. go-spiffe v2: https://github.com/spiffe/go-spiffe
  13. Sigstore docs: https://docs.sigstore.dev/
  14. Bitnami Sealed Secrets: https://github.com/bitnami-labs/sealed-secrets
  15. Mozilla SOPS: https://github.com/getsops/sops
  16. Istio Security Best Practices
  17. PCI-DSS v4.0 Quick Reference Guide
  18. OWASP Cheat Sheet: Transport Layer Security
  19. “Securing DevOps” — Julien Vehent (Manning, 2018)
  20. “Zero Trust Networks” — Evan Gilman, Doug Barth (O’Reilly)