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.
Содержание
Заголовок раздела «Содержание»- Концепция mTLS, SPIFFE и Secrets
- Глубже: PKI, SVID, Vault, KMS, envelope encryption
- Gotchas / Best practices
- Real cases (Istio, Cloudflare, банковские системы)
- Вопросы (20)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»Mutual TLS (mTLS)
Заголовок раздела «Mutual TLS (mTLS)»Обычный 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 и 0-RTT
Заголовок раздела «TLS 1.3 и 0-RTT»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 / SPIRE
Заголовок раздела «SPIFFE / SPIRE»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 Management
Заголовок раздела «Secrets Management»Категории 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 и envelope encryption
Заголовок раздела «KMS и envelope encryption»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) сидит в KMSDEK генерится локально, шифрует данные, сама шифруется через KMS (один RPC). Хранится рядом с данными.
2. Глубже
Заголовок раздела «2. Глубже»2.1 mTLS в Go (production-grade)
Заголовок раздела «2.1 mTLS в Go (production-grade)»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) }}2.2 Ротация сертификатов без рестарта
Заголовок раздела «2.2 Ротация сертификатов без рестарта»Сертификаты в 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.
2.3 SPIRE: workload attestation в Go
Заголовок раздела «2.3 SPIRE: workload attestation в Go»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. Ротация происходит автоматически.
2.4 Vault: dynamic database credentials
Заголовок раздела «2.4 Vault: dynamic database credentials»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 утекли — урон ограничен временем жизни.
2.5 KMS Envelope Encryption (AWS)
Заголовок раздела «2.5 KMS Envelope Encryption (AWS)»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 }}Дешифровка:
kms.Decrypt(EncryptedDEK)→ plaintext DEK.- AES-GCM Open с DEK + Nonce → plaintext.
2.6 Сертификаты для production: ACME / Let’s Encrypt
Заголовок раздела «2.6 Сертификаты для production: ACME / Let’s Encrypt»Для публичных 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).
2.7 Внутренний PKI с cert-manager
Заголовок раздела «2.7 Внутренний PKI с cert-manager»cert-manager в Kubernetes:
Issuer/ClusterIssuer(Vault, ACME, self-signed CA).Certificateresource описывает желаемый сертификат.- Контроллер периодически переоформляет (renewBefore: 30d).
- Хранит cert в
Secret, который монтируется в Pod.
apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: payments-tls namespace: paymentsspec: 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/billing3. Gotchas / Best practices
Заголовок раздела «3. Gotchas / Best practices»⚠️ 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.
Best practices
Заголовок раздела «Best practices»- 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 засветится.
4. Real cases
Заголовок раздела «4. Real cases»4.1 Istio + SPIFFE: автоматический mTLS
Заголовок раздела «4.1 Istio + SPIFFE: автоматический mTLS»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).
4.2 Cloudflare Origin CA
Заголовок раздела «4.2 Cloudflare Origin CA»Cloudflare выдаёт сертификаты для origin (бэкенд), действительные 15 лет, доверяемые только Cloudflare. Это нестандартный CA — публичные TLS клиенты их не примут. Используется для защиты бэкенда от прямого доступа.
4.3 Банк с PCI-DSS: tokenization + envelope encryption
Заголовок раздела «4.3 Банк с PCI-DSS: tokenization + envelope encryption»PAN (номер карты) никогда не хранится в plain — заменяется на token (UUID), а сам PAN шифруется в vault и доступен только через короткоживущий запрос. Поля шифруются через AWS KMS envelope. Логи маскируются (411111******1111).
4.4 Vault и автоматическая ротация DB
Заголовок раздела «4.4 Vault и автоматическая ротация DB»Сервис при старте читает database/creds/role-x. Vault создаёт уникального юзера в Postgres с TTL=1h. Сервис кеширует, при подходе к expiry запрашивает заново. Если pod падает — Vault sweeps unused.
Результат: даже если postgres dump утечёт, password будет уже невалидным.
4.5 Repjacking: pkg.go.dev
Заголовок раздела «4.5 Repjacking: pkg.go.dev»В 2022-2023 атаки на Go: автор удалял аккаунт, кто-то перерегистрировал имя и подменял модуль. Go module proxy с immutable хешами в go.sum защищает от такого, но требует обновления go.sum сразу при добавлении модуля и проверки PR на изменения go.sum.
5. Вопросы (20)
Заголовок раздела «5. Вопросы (20)»- Чем mTLS отличается от обычного TLS?
- Что такое 0-RTT в TLS 1.3 и в чём risk replay attack?
- Что такое SPIFFE ID и из чего состоит trust domain?
- Как SPIRE Agent аттестует workload в Kubernetes?
- В чём разница X509-SVID vs JWT-SVID?
- Как ротировать сертификат в Go без рестарта?
- Что делает
GetCertificatecallback вtls.Config? - Опиши envelope encryption. Зачем нужен DEK отдельно от KEK?
- Какие auth methods поддерживает Vault?
- Что такое dynamic secrets в Vault и где их использовать?
- Чем Sealed Secrets отличается от SOPS?
- Почему base64 в Kubernetes Secret — не шифрование?
- Что такое encryption at rest для etcd?
- Какие ограничения у Let’s Encrypt rate limit?
- Как обеспечить ротацию root CA без даунтайма?
- Что произойдёт, если CRL/OCSP сервер недоступен?
- Как защитить DEK в памяти Go-процесса?
- Чем Istio mTLS отличается от ручного mTLS в Go-приложении?
- Что такое
VerifyPeerCertificateи когда его использовать? - Как Vault PKI engine выдаёт сертификаты и чем отличается от cert-manager?
6. Practice
Заголовок раздела «6. Practice»- Развернуть локально SPIRE Server + Agent в minikube, выдать SVID для двух подов, протестировать mTLS.
- Написать Go-сервис, который читает cert из файла, реализует hot-reload через fsnotify + polling.
- Реализовать envelope encryption поверх AWS KMS (или LocalStack) для пользовательских PII.
- Настроить cert-manager + ClusterIssuer (self-signed) и выдать сертификат с SPIFFE URI в SAN.
- Vault: настроить
database/postgresqlsecret engine, написать клиент, который продлевает lease. - Реализовать
VerifyPeerCertificate, который смотрит только на SPIFFE ID в URI SAN. - Sigstore: подписать Docker image через
cosign, проверить подпись. - Sealed Secrets: зашифровать
Secretчерез kubeseal, закоммитить, развернуть.
7. Источники
Заголовок раздела «7. Источники»- RFC 8446 — TLS 1.3
- RFC 5280 — Internet X.509 PKI Certificate
- SPIFFE specification: https://github.com/spiffe/spiffe
- SPIRE docs: https://spiffe.io/docs/latest/spire-about/
- HashiCorp Vault: https://developer.hashicorp.com/vault/docs
- AWS KMS Best Practices: https://docs.aws.amazon.com/kms/latest/developerguide/best-practices.html
- Cloud Native Security WhitePaper (CNCF)
- Google Beyond Corp paper (Zero Trust)
- NIST SP 800-57 — Recommendation for Key Management
- Go
crypto/tlsdocs: https://pkg.go.dev/crypto/tls - cert-manager: https://cert-manager.io/docs/
- go-spiffe v2: https://github.com/spiffe/go-spiffe
- Sigstore docs: https://docs.sigstore.dev/
- Bitnami Sealed Secrets: https://github.com/bitnami-labs/sealed-secrets
- Mozilla SOPS: https://github.com/getsops/sops
- Istio Security Best Practices
- PCI-DSS v4.0 Quick Reference Guide
- OWASP Cheat Sheet: Transport Layer Security
- “Securing DevOps” — Julien Vehent (Manning, 2018)
- “Zero Trust Networks” — Evan Gilman, Doug Barth (O’Reilly)