Архитектура

Защита микросервисов: JWT, mTLS и централизованная авторизация через REST API в Kubernetes

Ruslan Ismailov Опубликовано 18 мин чтения
З

Введение: почему безопасность микросервисов сложнее монолита

В 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)
  • Время жизни (exp claim)
  • Издателя (iss claim)
  • Аудиторию (aud claim)
  • Присутствие токена в 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-заголовков — никогда не логируйте Authorization header целиком
  • Передаче через 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-готовой системы:

  1. JWT подписывается алгоритмом RS256 или ES256, алгоритм валидируется явно в коде
  2. Access token имеет TTL не более 15 минут, refresh token — не более 7 дней
  3. Публичные ключи публикуются через JWKS и кэшируются с автообновлением
  4. Весь межсервисный трафик защищён mTLS с минимальной версией TLS 1.3
  5. Сертификаты управляются через cert-manager с автоматической ротацией
  6. Авторизация централизована через OPA, политики версионируются в Git
  7. Revocation list хранится в Redis с TTL = оставшемуся сроку жизни токена
  8. Все security-события логируются в структурированном формате JSON
  9. Network policy в Kubernetes ограничивают межсервисное взаимодействие по whitelist
  10. Заголовки Authorization исключены из application-логов

Реализация каждого из этих слоёв требует инженерных усилий, но именно их совокупность обеспечивает defence-in-depth — ситуацию, когда компрометация одного компонента не означает компрометации всей системы.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →