mTLS (mutual TLS): both parties authenticate with x509 certs. SPIFFE/SPIRE workload identity, service mesh sidecar pattern (Istio/Linkerd), short-lived cert auto-rotation, zero-trust service-to-service auth. 3 scenarios: mTLS handshake with mutual cert presentation, SPIFFE-issued SVID with auto-rotation, mesh sidecar mTLS with cert revocation flow.
Обычный TLS отвечает на один вопрос: «я говорю с тем сервером, чьё имя в URL?». Браузер проверяет сертификат сервера, сервер ничего не знает про клиента кроме IP. Для публичного веба этого хватает — пользователь идентифицируется логином/паролем/cookie поверх TLS.
В intra-cluster service-to-service общении эта модель ломается. Сетевая позиция перестала быть гарантией доверия (zero-trust): pod в той же VPC, в том же namespace, на том же node — необязательно «свой». Нужен механизм, который отвечает на симметричный вопрос: «кто на том конце сокета — и кто на этом».
mTLS добавляет к стандартному handshake шаг CertificateRequest: сервер требует от клиента свой x509 cert. Обе стороны верифицируют друг друга через общий trust bundle (root CA pubkey). Идентичность вшита в L4, до того как любой HTTP byte уйдёт по проводу.
Зачем это в реальности:
Думайте про mTLS не как про «TLS, но в обе стороны», а как про identityful transport. Каждое соединение знает кто его инициатор и кто принимает, до первого HTTP-байта. Это не auth header, который можно подделать или leak — это криптографическое доказательство владения private key, привязанным к субъекту в cert.
Три слоя, которые нужно держать в голове одновременно:
spiffe://prod/sa/payments-service вместо классического CN=server.example.com — это URI, который уникально называет workload вне зависимости от его IP, hostname, replica index.http://api.prod, sidecar iptables-перехватывает outbound трафик и оборачивает в mTLS прозрачно.Полезная аналогия: mTLS — это VIP-вход в клуб с двусторонним контролем. Гость показывает приглашение охране, охрана показывает гостю беджик с печатью клуба. Если кто-то из двоих не предъявил валидную бумагу — общения не происходит, дальше дверей разговор не идёт.
Скрипт mtls показывает реалистичный k8s service mesh с SPIFFE/SPIRE control plane.
Client Service (Pod) — два процесса в одном pod:
app-a — бизнес-код, говорит plain HTTP по localhostsidecar-a — Envoy, перехватывает outbound, делает mTLSServer Service (Pod) — симметрично: sidecar-b принимает mTLS, форвардит plain HTTP в app-b через loopback.
Compromised Pod — sidecar-c со скомпрометированным cert (используется в сценарии отзыва).
SPIFFE/SPIRE Control Plane:
spire-server — центральный issuer, держит registration entries и подписывает SVIDspire-agent-a/b — node-local агенты, делают node attestation и проксируют CSRtrust-bundle — public keys root CA, через которые sidecars верифицируют чужие certcrl (revocation set) — для federation между trust domainsEdges — только физические соединения. Loopback app↔sidecar, mTLS sidecar↔sidecar, gRPC от агентов к серверу, Workload API socket от sidecar к локальному агенту. Все ответы (handshake replies, cert deliveries) идут reverse-анимацией по тем же проводам — отдельных «обратных» edges нет (это типичный антипаттерн в схемах mesh, обходящий sidecar).
1. mTLS handshake показывает полный TLS 1.3 handshake с упором на новый шаг — CertificateRequest. App-a делает обычный GET, sidecar перехватывает, открывает соединение к sidecar-b. Дальше: Client Hello → Server Hello → server cert → CertificateRequest → client cert → mutual verify против trust bundle → derived session keys → encrypted application data. На выходе sidecar-b форвардит plain HTTP в app-b и добавляет header X-Forwarded-Client-Cert — единственный способ передать identity в app код. В конце сценарий явно проговаривает два gotcha: header trust требует enforcement (network policy на прямой port app), и mTLS — это authN, не authZ, нужна отдельная политика поверх.
2. SPIFFE/SPIRE auto-rotation — жизненный цикл cert на 24h. Boot: sidecar открывает SPIFFE Workload API через Unix socket (не network — приватный сокет). Node attestation: агент доказывает SPIRE Server, что он на known node (k8s_psat / aws_iid / join_token). CSR: keypair генерируется локально, private key никогда не покидает node. SVID выдан с TTL=24h и SAN=SPIFFE-ID. На T+12h (50% lifetime) триггерится rotation, hot swap cert без рестарта app, без drop соединений. Сценарий заканчивается failure mode: SPIRE Server недоступен > TTL/2 → через 12h массовый outage всего mesh. Mitigation — HA SPIRE (3+ реплики, leader election), tier-0 monitoring.
3. Mesh sidecar mTLS + revocation — что происходит при инциденте. Компрометированный pod (sidecar-c) ходит в sidecar-b с украденным валидным cert. SecOps детектит (DLP, SIEM alert). Админ удаляет registration entry через spire-server entry delete spiffe://prod/sa/compromised-pod. Через TTL/2 sidecar-c пытается rotate → [DENY] no registration entry → cert не выдан → через 12h истечение → следующий handshake fails. Ключевой insight: short-lived certs делают revocation бесплатным. Не выдал новый = автоматически out. Long-lived certs требуют CRL/OCSP с soft-fail клиентами, кэширования, privacy issues — истечение надёжнее.
ADR-001 на ноде sidecar-a: mTLS vs OAuth2 client credentials vs static API key для service-to-service auth внутри кластера.
Контекст: три мейнстрим-варианта. Static API key — каждый сервис носит shared secret в env. OAuth2 CC — сервис берёт JWT у auth server, кладёт в Authorization. mTLS — обе стороны предъявляют cert, identity = SPIFFE-ID в SAN.
Решение: mTLS как primary, через service mesh sidecar + SPIFFE/SPIRE. Reasoning:
X-User-Id header — здесь невозможен в принципе.Tradeoffs (платим за это):
openssl x509 -in cert.pem -text).НЕ использовать mTLS для:
Гибрид: mTLS на mesh edge для service identity + JWT поверх для user identity (Istio JWT filter / Envoy ext_authz). API key оставлять только для third-party callbacks, где нет mesh.
Self-signed certs everywhere без trust bundle management. Каждый сервис делает свой self-signed, остальные слепо trustят --insecure-skip-tls-verify. Это mTLS theatre: handshake проходит, но identity ничего не значит — любой attacker сгенерит свой cert и пройдёт.
Long-lived certs (1+ год) без auto-rotation. Cert на год = окно утечки на год. Забудешь rotate — упадёт в субботу в 3 ночи. Single-handedly виноват в большинстве «mtls outage» инцидентов индустрии. Стандарт сейчас: 24h для intra-cluster, 90d для публичных (Let's Encrypt).
Hardcoded cert paths в коде. /etc/certs/server.pem зашит в Go binary — невозможно rotate без redeploy. Используйте SPIFFE Workload API или mounted secrets с reload.
Доверять X-Forwarded-Client-Cert без enforcement, что только sidecar его выставляет. Если app port доступен напрямую (forgot iptables redirect, debug port на 9090 без auth) — attacker подделает header. Нужны network policy + AuthorizationPolicy в mesh.
mTLS вместо AuthZ. mTLS говорит «кто ты», не «что тебе можно». Service-A с валидным cert не должен ходить в /admin/* endpoint Service-B автоматически. Поверх mTLS нужен RBAC / Rego policy / Istio AuthorizationPolicy.
Client cert в localStorage/cookies для browser. XSS = leak. Если нужен client cert в browser — использовать OS cert store (macOS Keychain, Windows Cert Store) через <keygen> (deprecated) или WebAuthn для модерн-сценариев.
Игнорировать revocation. Украли private key, продолжает работать до expiry. С 24h cert это OK, с 1-year cert — катастрофа. CRL + OCSP stapling обязательны для long-lived.
Ручная rotation в проде. 100 сервисов × ручной cert update = коллапс. Auto-rotation или ничего.
SNI mismatch для wildcard. *.prod.svc.cluster.local cert на конкретный hostname payments.prod.svc.cluster.local — TLS handshake fails с bad_certificate, диагностируется часами без openssl s_client.
Mixed cert formats. Сервис ожидает PEM, монтируется DER. Crypto library молча падает. Нормализуйте через cert-manager / SPIRE workload API.
mTLS не серебряная пуля. Откажитесь от него, когда: