Защита микросервисов: JWT, mTLS и централизованная авторизация через REST API в Kubernetes
Введение: почему безопасность микросервисов сложнее монолита
В 2026 году микросервисная архитектура стала стандартом для высоконагруженных систем, но вместе с гибкостью она принесла принципиально иную модель угроз. В монолите периметр защиты был чётким: один процесс, одна база данных, один набор прав. В микросервисах поверхность атаки распределена: десятки сервисов обмениваются данными по сети, каждый из них — потенциальная точка входа.
Актуальные угрозы для микросервисных систем в Kubernetes включают:
- Lateral movement — компрометация одного сервиса открывает доступ к внутренней сети кластера
- Token leakage — перехват JWT или session-токенов через незашифрованный трафик между подами
- Privilege escalation — сервис получает больше прав, чем ему необходимо, через некорректно настроенную авторизацию
- SSRF-атаки — злоумышленник эксплуатирует доверие между сервисами для обращения к внутренним ресурсам
- Supply chain атаки — компрометация зависимостей в одном сервисе затрагивает всю экосистему
Ответом на эти угрозы служит многоуровневая защита: аутентификация пользователей через JWT, взаимная аутентификация сервисов через mTLS и централизованная авторизация через Policy Decision Point. Рассмотрим каждый слой детально.
JWT в микросервисах: stateless аутентификация и валидация на gateway
Архитектура JWT-аутентификации
JSON Web Token позволяет реализовать stateless аутентификацию: сервер не хранит сессии, вся информация закодирована в самом токене и верифицируется криптографически. В контексте микросервисов это означает, что каждый сервис может независимо проверить подлинность запроса, не обращаясь к центральному хранилищу.
Типичная схема взаимодействия выглядит следующим образом:
Клиент → [POST /auth/login] → Auth Service
↓
Выдаёт JWT (access + refresh)
↓
Клиент → [GET /api/orders] → API Gateway
↓
Валидирует JWT (подпись, exp, iss)
↓
Проксирует запрос → Order Service
↓
Order Service доверяет gateway (mTLS)
Валидация JWT на уровне API Gateway
Валидацию JWT следует выполнять на уровне API Gateway, а не в каждом микросервисе. Это исключает дублирование логики и централизует контроль. Gateway проверяет:
- Подпись токена (алгоритм RS256 или ES256 — никогда не
none) - Время жизни (
expclaim) - Издателя (
issclaim) - Аудиторию (
audclaim) - Присутствие токена в revocation list (Redis)
Пример middleware на Go для валидации JWT с использованием библиотеки golang-jwt/jwt:
package middleware
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/redis/go-redis/v9"
)
type Claims struct {
UserID string `json:"sub"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
type JWTMiddleware struct {
publicKey interface{}
redisClient *redis.Client
}
func NewJWTMiddleware(publicKey interface{}, rc *redis.Client) *JWTMiddleware {
return &JWTMiddleware{publicKey: publicKey, redisClient: rc}
}
func (m *JWTMiddleware) Validate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
http.Error(w, "missing or invalid Authorization header", http.StatusUnauthorized)
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
// Принудительно проверяем алгоритм
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return m.publicKey, nil
})
if err != nil || !token.Valid {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// Проверка revocation list в Redis
ctx := context.Background()
revoked, err := m.redisClient.Exists(ctx, "revoked:"+tokenString[:16]).Result()
if err != nil || revoked > 0 {
http.Error(w, "token revoked", http.StatusUnauthorized)
return
}
// Передаём claims дальше через контекст
ctx = context.WithValue(r.Context(), "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Ротация ключей
Для ротации ключей подписи используется механизм JWKS (JSON Web Key Set). Auth Service публикует публичные ключи по эндпоинту /.well-known/jwks.json, а Gateway кэширует их с TTL и периодически обновляет. При ротации старый ключ остаётся доступным до истечения всех выданных токенов — это исключает отказ в обслуживании при плановой смене ключей.
mTLS между сервисами: шифрование и взаимная аутентификация без service mesh
Зачем mTLS в микросервисах
TLS в одностороннем режиме аутентифицирует только сервер. Mutual TLS (mTLS) требует, чтобы обе стороны предъявили сертификат: клиент доказывает серверу свою идентичность, и наоборот. В контексте Kubernetes это означает, что даже если злоумышленник получит доступ к сети кластера, он не сможет выдать себя за легитимный сервис без валидного клиентского сертификата.
Без service mesh (Istio, Linkerd) mTLS реализуется на уровне приложения. Каждый Go-сервис настраивается на использование клиентского сертификата при исходящих запросах и требует клиентский сертификат от входящих.
Реализация mTLS-клиента на Go
package transport
import (
"crypto/tls"
"crypto/x509"
"net/http"
"os"
"time"
)
// NewMTLSClient создаёт HTTP-клиент с взаимной TLS-аутентификацией
func NewMTLSClient(certFile, keyFile, caFile string) (*http.Client, error) {
// Загружаем клиентский сертификат и ключ
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("load client cert: %w", err)
}
// Загружаем CA для верификации сервера
caCert, err := os.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("read CA cert: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to append CA cert")
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13, // Минимум TLS 1.3
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
// Настройки пула соединений для production
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}, nil
}
Настройка mTLS-сервера на Go
package server
import (
"crypto/tls"
"crypto/x509"
"net/http"
"os"
)
func NewMTLSServer(certFile, keyFile, caFile string, handler http.Handler) (*http.Server, error) {
caCert, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert, // Обязательная проверка клиента
ClientCAs: caCertPool,
MinVersion: tls.VersionTLS13,
}
return &http.Server{
Addr: ":8443",
Handler: handler,
TLSConfig: tlsConfig,
}, nil
}
Схема взаимодействия с mTLS:
Order Service (клиент) Inventory Service (сервер)
│ │
│── ClientHello (TLS 1.3) ──────────────→ │
│← ServerHello + ServerCert ───────────── │
│── ClientCert ────────────────────────→ │
│ (Верификация по CA) ←──────────────── │
│← Verified (200 OK) ──────────────────── │
│ │
Каждый сервис знает, с кем разговаривает
Централизованный сервис авторизации: Policy Decision Point и Open Policy Agent
Паттерн Policy Decision Point
В распределённой системе авторизационная логика не должна быть размазана по микросервисам. Паттерн Policy Decision Point (PDP) выносит принятие решений об авторизации в отдельный компонент. Сервисы отправляют запрос: «может ли пользователь X выполнить действие Y над ресурсом Z?» — и получают ответ allow или deny.
Open Policy Agent (OPA) — де-факто стандарт для реализации PDP в Kubernetes-экосистеме. Политики описываются на языке Rego, что делает их версионируемыми, тестируемыми и независимыми от кода приложения.
Пример политики Rego
package authz.orders
import future.keywords.if
import future.keywords.in
# Разрешаем чтение заказов владельцу или менеджеру
default allow := false
allow if {
input.action == "read"
input.resource.owner_id == input.user.id
}
allow if {
input.action == "read"
"manager" in input.user.roles
}
# Только admin может удалять заказы
allow if {
input.action == "delete"
"admin" in input.user.roles
}
Интеграция с Go-сервисом через REST API
package authz
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
type OPAClient struct {
baseURL string
httpClient *http.Client
}
type AuthzRequest struct {
Input AuthzInput `json:"input"`
}
type AuthzInput struct {
User UserContext `json:"user"`
Action string `json:"action"`
Resource ResourceContext `json:"resource"`
}
type AuthzResponse struct {
Result bool `json:"result"`
}
func (c *OPAClient) IsAllowed(ctx context.Context, input AuthzInput) (bool, error) {
payload, err := json.Marshal(AuthzRequest{Input: input})
if err != nil {
return false, err
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
c.baseURL+"/v1/data/authz/orders/allow",
bytes.NewReader(payload),
)
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return false, fmt.Errorf("OPA request failed: %w", err)
}
defer resp.Body.Close()
var result AuthzResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, err
}
return result.Result, nil
}
Практическая реализация: Go-сервис и Laravel как потребитель
Go-сервис с JWT middleware и mTLS
Полная цепочка обработки запроса в Go-сервисе выглядит следующим образом:
package main
import (
"log"
"net/http"
"github.com/yourorg/service/internal/authz"
"github.com/yourorg/service/internal/middleware"
"github.com/yourorg/service/internal/server"
"github.com/yourorg/service/internal/transport"
)
func main() {
// Инициализация mTLS HTTP-клиента для исходящих запросов
mtlsClient, err := transport.NewMTLSClient(
"/etc/certs/client.crt",
"/etc/certs/client.key",
"/etc/certs/ca.crt",
)
if err != nil {
log.Fatalf("failed to create mTLS client: %v", err)
}
// OPA клиент использует mTLS для обращения к AuthZ сервису
opaClient := authz.NewOPAClient("https://opa.internal:8443", mtlsClient)
// Маршрутизация с JWT middleware
mux := http.NewServeMux()
jwtMW := middleware.NewJWTMiddleware(loadPublicKey(), initRedis())
mux.Handle("/api/orders", jwtMW.Validate(
authzMiddleware(opaClient, "read",
http.HandlerFunc(handleGetOrders),
),
))
// Запуск mTLS-сервера
srv, err := server.NewMTLSServer(
"/etc/certs/server.crt",
"/etc/certs/server.key",
"/etc/certs/ca.crt",
mux,
)
if err != nil {
log.Fatalf("failed to create server: %v", err)
}
log.Fatal(srv.ListenAndServeTLS("", ""))
}
Laravel-сервис как потребитель
Laravel-сервис взаимодействует с Go-сервисами через REST API, используя клиентские сертификаты для mTLS. Конфигурация HTTP-клиента в Laravel через Guzzle:
// config/services.php
'order_service' => [
'base_url' => env('ORDER_SERVICE_URL', 'https://order-service.internal:8443'),
'cert' => env('MTLS_CERT_PATH', '/etc/certs/client.crt'),
'key' => env('MTLS_KEY_PATH', '/etc/certs/client.key'),
'ca' => env('MTLS_CA_PATH', '/etc/certs/ca.crt'),
],
// app/Services/OrderServiceClient.php
class OrderServiceClient
{
private Client $client;
public function __construct()
{
$config = config('services.order_service');
$this->client = new Client([
'base_uri' => $config['base_url'],
'cert' => [$config['cert'], ''], // путь к сертификату
'ssl_key' => $config['key'],
'verify' => $config['ca'], // CA для верификации сервера
'timeout' => 5.0,
]);
}
public function getOrders(string $jwtToken): array
{
$response = $this->client->get('/api/orders', [
'headers' => [
'Authorization' => 'Bearer ' . $jwtToken,
'X-Request-ID' => (string) Str::uuid(),
],
]);
return json_decode($response->getBody()->getContents(), true);
}
}
Laravel передаёт JWT пользователя при вызове внутренних сервисов. Это позволяет downstream-сервисам знать контекст пользователя без повторной аутентификации.
Управление сертификатами в Kubernetes: cert-manager и автоматическая ротация
Архитектура PKI в кластере
Для управления TLS-сертификатами в Kubernetes используется cert-manager. Он автоматически выпускает и обновляет сертификаты, хранит их как Kubernetes Secrets и монтирует в поды.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: order-service-cert
namespace: production
spec:
secretName: order-service-tls
duration: 24h # Короткий TTL для mTLS-сертификатов
renewBefore: 8h # Обновление за 8 часов до истечения
subject:
organizations:
- your-org
commonName: order-service.production.svc.cluster.local
dnsNames:
- order-service
- order-service.production
- order-service.production.svc
- order-service.production.svc.cluster.local
issuerRef:
name: internal-ca-issuer
kind: ClusterIssuer
usages:
- digital signature
- key encipherment
- client auth # Для mTLS: сертификат используется как клиентский
- server auth
Монтирование сертификатов в Pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
spec:
containers:
- name: order-service
image: your-registry/order-service:latest
volumeMounts:
- name: tls-certs
mountPath: /etc/certs
readOnly: true
env:
- name: TLS_CERT_PATH
value: /etc/certs/tls.crt
- name: TLS_KEY_PATH
value: /etc/certs/tls.key
- name: TLS_CA_PATH
value: /etc/certs/ca.crt
volumes:
- name: tls-certs
secret:
secretName: order-service-tls
При ротации сертификата cert-manager обновляет Secret, Kubernetes перемонтирует файлы в работающий контейнер. Go-сервис должен поддерживать горячую перезагрузку сертификатов через tls.Config.GetCertificate вместо статической загрузки при старте.
Хранение и передача токенов: Redis как revocation list
Проблема отзыва JWT
JWT по природе stateless: валидный токен нельзя «отозвать» без дополнительного механизма. Если пользователь выходит из системы, сменил пароль или аккаунт скомпрометирован, нужен способ немедленно аннулировать выданные токены до истечения их срока действия.
Revocation list на Redis
Решение — хранить идентификаторы отозванных токенов в Redis с TTL, равным оставшемуся времени жизни токена. Gateway проверяет каждый входящий токен по этому списку.
package token
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
type RevocationStore struct {
client *redis.Client
}
const revokedKeyPrefix = "token:revoked:"
// RevokeToken добавляет JTI токена в revocation list
func (s *RevocationStore) RevokeToken(ctx context.Context, jti string, expiresAt time.Time) error {
ttl := time.Until(expiresAt)
if ttl <= 0 {
// Токен уже истёк — нечего отзывать
return nil
}
return s.client.Set(ctx, revokedKeyPrefix+jti, "1", ttl).Err()
}
// IsRevoked проверяет, отозван ли токен
func (s *RevocationStore) IsRevoked(ctx context.Context, jti string) (bool, error) {
result, err := s.client.Exists(ctx, revokedKeyPrefix+jti).Result()
if err != nil {
return false, err
}
return result > 0, nil
}
Для масштабируемости Redis Cluster с репликацией обеспечивает доступность revocation list даже при отказе нод. Используйте WAIT команду для подтверждения репликации при отзыве критически важных токенов.
Логирование и аудит security-событий
Что необходимо логировать
Аудит безопасности в микросервисах должен охватывать следующие события:
- Каждая попытка аутентификации (успешная и неуспешная) с указанием User-Agent, IP, timestamp
- Отказы авторизации от OPA с контекстом: кто, что, над чем пытался сделать
- Ошибки mTLS-рукопожатия: неверный сертификат, истёкший сертификат, неизвестный CA
- Отзыв токенов: кем инициирован, какой JTI, причина
- Смена ключей подписи JWT
- Изменения в политиках OPA (через CI/CD)
Структура security-события
{
"timestamp": "2026-03-15T14:32:01.123Z",
"level": "WARN",
"event_type": "authz.denied",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"service": "order-service",
"user_id": "usr_01HX4M",
"action": "delete",
"resource_type": "order",
"resource_id": "ord_09KL2P",
"policy": "authz.orders",
"decision": "deny",
"client_cert_cn": "inventory-service",
"source_ip": "10.0.1.45"
}
Структурированные логи в JSON-формате отправляются в централизованную систему (ELK, Loki, CloudWatch). Настройте алерты на аномалии: резкий рост authz.denied, повторяющиеся ошибки mTLS с одного IP, попытки использования отозванных токенов.
Типичные уязвимости и как их избежать
Token leakage
JWT передаётся в заголовке Authorization и может быть скомпрометирован при:
- Логировании полных HTTP-заголовков — никогда не логируйте
Authorizationheader целиком - Передаче через URL-параметр — токен попадает в access-логи серверов и историю браузера
- Незашифрованном трафике между сервисами — именно для этого нужен mTLS
Решение: логируйте только первые 8 символов токена (для корреляции), используйте исключительно HTTPS/mTLS, устанавливайте короткий TTL (15 минут для access token).
SSRF через межсервисное доверие
Если сервис A доверяет всем запросам от сервиса B на основании mTLS, злоумышленник, скомпрометировавший B, может эксплуатировать это доверие для SSRF-атак на внутренние ресурсы. Контрмеры:
- mTLS подтверждает идентичность сервиса, но не намерение запроса — авторизация через OPA обязательна для каждого действия
- Network policy в Kubernetes: сервисы могут обращаться только к явно разрешённым адресам
- Валидируйте все входящие URL в параметрах запросов по whitelist
Privilege escalation через claims
Никогда не доверяйте claims из JWT без верификации в контексте текущей операции. Пример ошибки: сервис принимает роль admin из токена без проверки через OPA, что позволяет подделать payload при слабом алгоритме подписи. Решения:
- Используйте только асимметричные алгоритмы (RS256, ES256) — никогда HS256 в многосервисной среде, где секрет должен знать каждый сервис
- Валидируйте алгоритм явно в коде, не доверяйте заголовку
algиз токена - OPA получает claims из токена, но финальное решение принимается на основе актуальных данных из БД
Заключение: чеклист безопасности микросервисов
Комплексная защита микросервисной архитектуры — это не набор независимых инструментов, а взаимосвязанная система. JWT обеспечивает идентичность пользователя, mTLS гарантирует идентичность сервисов, OPA централизует авторизационную логику, cert-manager автоматизирует управление сертификатами, а Redis обеспечивает механизм отзыва токенов.
Чеклист для production-готовой системы:
- JWT подписывается алгоритмом RS256 или ES256, алгоритм валидируется явно в коде
- Access token имеет TTL не более 15 минут, refresh token — не более 7 дней
- Публичные ключи публикуются через JWKS и кэшируются с автообновлением
- Весь межсервисный трафик защищён mTLS с минимальной версией TLS 1.3
- Сертификаты управляются через cert-manager с автоматической ротацией
- Авторизация централизована через OPA, политики версионируются в Git
- Revocation list хранится в Redis с TTL = оставшемуся сроку жизни токена
- Все security-события логируются в структурированном формате JSON
- Network policy в Kubernetes ограничивают межсервисное взаимодействие по whitelist
- Заголовки
Authorizationисключены из application-логов
Реализация каждого из этих слоёв требует инженерных усилий, но именно их совокупность обеспечивает defence-in-depth — ситуацию, когда компрометация одного компонента не означает компрометации всей системы.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →