DevOps

Построение zero-trust сети микросервисов на Kubernetes: политики, mTLS и identity без Service Mesh

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

Введение: почему zero-trust стал необходимостью в 2026 году

Модель безопасности zero-trust строится на принципе «не доверяй никому по умолчанию» — ни внешним запросам, ни внутренним сервисам в кластере. В 2026 году эта философия перестала быть архитектурной экзотикой и стала стандартом де-факто для production-окружений на Kubernetes. Причины просты: горизонтальное масштабирование микросервисов увеличивает поверхность атаки, а компрометация одного пода без должной изоляции открывает путь ко всей инфраструктуре.

Многие команды сразу тянутся к Istio или Linkerd, считая, что zero-trust невозможен без полноценного Service Mesh. Это заблуждение. Kubernetes предоставляет достаточно примитивов — NetworkPolicy, RBAC, ServiceAccount — а открытый стандарт SPIFFE и его реализация SPIRE закрывают задачу identity без дополнительного data plane. Взаимная TLS-аутентификация реализуется прямо в коде Go-сервиса. В этой статье разберём каждый из этих слоёв детально, с примерами YAML и Go-кода.

Проблемы традиционной периметровой безопасности в Kubernetes

Классический подход «защищённый периметр» предполагает, что всё внутри сети доверенно. В Kubernetes это означает, что любой под в кластере по умолчанию может обращаться к любому другому поду через Service DNS. Никаких ограничений нет, если не настроено иное.

Последствия очевидны:

  • Скомпрометированный под payment-service может напрямую обратиться к user-db без каких-либо проверок.
  • Отсутствие mutual TLS означает, что сервис не может быть уверен, с кем именно он общается — подлинный сервис или атакующий посередине.
  • Горизонтальное движение (lateral movement) после взлома одного компонента становится тривиальным.

Zero-trust решает эти проблемы через три ортогональных механизма: сетевую изоляцию, криптографическую идентификацию и аудит каждого взаимодействия.

NetworkPolicy: изоляция трафика на уровне namespace и pod

NetworkPolicy — это Kubernetes-ресурс, который описывает правила входящего (ingress) и исходящего (egress) трафика для набора подов. Важно понимать: NetworkPolicy работает только если в кластере установлен CNI-плагин с поддержкой политик (Calico, Cilium, Weave Net). Flannel в базовой конфигурации NetworkPolicy не применяет.

Стратегия «deny-all by default» — первый шаг к zero-trust. Создаём политику, которая запрещает весь входящий трафик для всех подов в namespace payments:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Ingress

Затем явно разрешаем только нужные пути. Например, order-service из namespace orders должен обращаться к payment-service в namespace payments:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-order-to-payment
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: payment-service
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: orders
          podSelector:
            matchLabels:
              app: order-service
      ports:
        - protocol: TCP
          port: 8443

Обратите внимание: мы используем порт 8443 — это уже намёк на TLS. NetworkPolicy не проверяет содержимое трафика, она работает на L3/L4. Для L7-контроля нужен либо CNI с поддержкой L7 (Cilium с Hubble), либо application-layer проверка через mTLS.

Рекомендации по организации NetworkPolicy в production:

  • Создавайте default-deny для ingress и egress в каждом namespace.
  • Явно разрешайте обращения к DNS (kube-dns, порт 53 UDP/TCP).
  • Используйте отдельные labels для разных ролей подов (role: api, role: worker).
  • Версионируйте NetworkPolicy в Git как часть IaC.

SPIFFE/SPIRE: выдача и ротация идентификаторов сервисов

SPIFFE (Secure Production Identity Framework for Everyone) — открытый стандарт, который определяет формат криптографических идентификаторов для рабочих нагрузок. Идентификатор имеет вид URI: spiffe://trust-domain/path. Например: spiffe://cluster.local/ns/payments/sa/payment-service.

SPIRE — референсная реализация SPIFFE, состоящая из двух компонентов:

  • SPIRE Server — центральный CA, который выдаёт SVID (SPIFFE Verifiable Identity Document) в форме x.509-сертификатов.
  • SPIRE Agent — DaemonSet на каждой ноде, аттестует поды через Kubernetes API и доставляет SVID через Unix-сокет.

Развёртывание SPIRE в кластере начинается с установки CRD и деплоймента Server. Минимальный ConfigMap для SPIRE Server:

apiVersion: v1
kind: ConfigMap
metadata:
  name: spire-server
  namespace: spire
data:
  server.conf: |
    server {
      bind_address = "0.0.0.0"
      bind_port = "8081"
      trust_domain = "cluster.local"
      data_dir = "/run/spire/server/data"
      log_level = "INFO"
      ca_ttl = "24h"
      default_x509_svid_ttl = "1h"
    }
    plugins {
      DataStore "sql" {
        plugin_data {
          database_type = "sqlite3"
          connection_string = "/run/spire/server/data/datastore.sqlite3"
        }
      }
      NodeAttestor "k8s_psat" {
        plugin_data {
          clusters = {
            "my-cluster" = {
              service_account_allow_list = ["spire:spire-agent"]
            }
          }
        }
      }
      KeyManager "disk" {
        plugin_data {
          keys_path = "/run/spire/server/data/keys.json"
        }
      }
    }

SPIRE Agent регистрируется на ноде через Projected Service Account Token (PSAT) — механизм аттестации, встроенный в Kubernetes. После успешной аттестации агент получает доступ к Workload API и раздаёт SVID подам через Unix-сокет /run/spire/sockets/agent.sock.

Регистрация рабочей нагрузки (workload registration entry) выполняется командой:

kubectl exec -n spire spire-server-0 -- \
  spire-server entry create \
  -spiffeID spiffe://cluster.local/ns/payments/sa/payment-service \
  -parentID spiffe://cluster.local/spire/agent/k8s_psat/my-cluster/NODE_UID \
  -selector k8s:ns:payments \
  -selector k8s:sa:payment-service

SVID автоматически ротируются за 5 минут до истечения TTL. Сервис получает новый сертификат без перезапуска — это критично для long-running Go-сервисов.

mTLS между Go-сервисами: реализация без Service Mesh

Получив SVID от SPIRE Agent через Workload API, Go-сервис использует его для установки взаимной TLS-аутентификации с другими сервисами. Библиотека github.com/spiffe/go-spiffe/v2 абстрагирует работу с Workload API и предоставляет готовые tls.Config.

Пример Go-кода для сервера с mTLS через SPIFFE:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/spiffe/go-spiffe/v2/spiffeid"
	"github.com/spiffe/go-spiffe/v2/spiffetls"
	"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
	"github.com/spiffe/go-spiffe/v2/workloadapi"
)

func main() {
	ctx := context.Background()

	// Подключаемся к SPIRE Agent через Workload API
	client, err := workloadapi.New(ctx,
		workloadapi.WithAddr("unix:///run/spire/sockets/agent.sock"),
	)
	if err != nil {
		log.Fatalf("unable to create workload API client: %v", err)
	}
	defer client.Close()

	// Источник X.509 SVID с автоматической ротацией
	source, err := workloadapi.NewX509Source(ctx,
		workloadapi.WithClient(client),
	)
	if err != nil {
		log.Fatalf("unable to create X509Source: %v", err)
	}
	defer source.Close()

	// Разрешаем только order-service из нашего trust domain
	allowedID := spiffeid.RequireFromString(
		"spiffe://cluster.local/ns/orders/sa/order-service",
	)

	tlsCfg := tlsconfig.MTLSServerConfig(
		source,
		source,
		tlsconfig.AuthorizeID(allowedID),
	)

	server := &http.Server{
		Addr:      ":8443",
		TLSConfig: tlsCfg,
		Handler:   http.HandlerFunc(paymentHandler),
	}

	log.Println("payment-service listening on :8443 with mTLS")
	if err := server.ListenAndServeTLS("", ""); err != nil {
		log.Fatalf("server error: %v", err)
	}
}

func paymentHandler(w http.ResponseWriter, r *http.Request) {
	// SPIFFE ID клиента доступен через TLS peer certificates
	if len(r.TLS.PeerCertificates) > 0 {
		log.Printf("authenticated caller SPIFFE ID: %s",
			r.TLS.PeerCertificates[0].URIs[0])
	}
	fmt.Fprintln(w, `{"status": "ok"}`)
}

Клиентский код для order-service, обращающегося к payment-service:

package main

import (
	"context"
	"io"
	"log"
	"net/http"

	"github.com/spiffe/go-spiffe/v2/spiffeid"
	"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
	"github.com/spiffe/go-spiffe/v2/workloadapi"
)

func main() {
	ctx := context.Background()

	source, err := workloadapi.NewX509Source(ctx,
		workloadapi.WithClientOptions(
			workloadapi.WithAddr("unix:///run/spire/sockets/agent.sock"),
		),
	)
	if err != nil {
		log.Fatalf("X509Source error: %v", err)
	}
	defer source.Close()

	serverID := spiffeid.RequireFromString(
		"spiffe://cluster.local/ns/payments/sa/payment-service",
	)

	tlsCfg := tlsconfig.MTLSClientConfig(
		source,
		source,
		tlsconfig.AuthorizeID(serverID),
	)

	client := &http.Client{
		Transport: &http.Transport{TLSClientConfig: tlsCfg},
	}

	resp, err := client.Get("https://payment-service.payments.svc.cluster.local:8443/charge")
	if err != nil {
		log.Fatalf("request failed: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	log.Printf("response: %s", body)
}

Ключевые моменты реализации:

  • Функции tlsconfig.AuthorizeID проверяют SPIFFE ID собеседника — если ID не совпадает с ожидаемым, TLS handshake завершится ошибкой.
  • X509Source автоматически обновляет сертификаты в фоне через watch на Workload API.
  • Сокет SPIRE Agent монтируется в под через hostPath volume или CSI driver.

Volume mount для пода:

volumes:
  - name: spire-agent-socket
    hostPath:
      path: /run/spire/sockets
      type: Directory
containers:
  - name: payment-service
    image: payment-service:latest
    volumeMounts:
      - name: spire-agent-socket
        mountPath: /run/spire/sockets
        readOnly: true

Аудит и логирование событий безопасности

Zero-trust без аудита — это половина работы. Необходимо фиксировать каждое значимое событие: кто обратился к кому, когда, с каким результатом.

На уровне Kubernetes включите Audit Policy для API Server. Пример политики, которая логирует изменения NetworkPolicy и секретов:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: RequestResponse
    resources:
      - group: "networking.k8s.io"
        resources: ["networkpolicies"]
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets", "serviceaccounts"]
  - level: None
    users: ["system:nodes"]
    verbs: ["get", "list", "watch"]

На уровне приложения каждый Go-сервис должен логировать SPIFFE ID входящих соединений. Используйте структурированное логирование (slog или zap) и отправляйте логи в централизованное хранилище (Loki, Elasticsearch). Пример middleware для HTTP-сервера:

func auditMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		callerID := "unknown"
		if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
			for _, uri := range r.TLS.PeerCertificates[0].URIs {
				callerID = uri.String()
				break
			}
		}
		log.Printf(`{"event":"request","caller":"%s","path":"%s","method":"%s"}`,
			callerID, r.URL.Path, r.Method)
		next.ServeHTTP(w, r)
	})
}

Дополнительно рассмотрите интеграцию с Falco — runtime security tool, который умеет обнаруживать аномальные syscall-паттерны и сетевые соединения прямо в подах.

Практический пример: защищённая коммуникация трёх микросервисов

Рассмотрим сценарий: api-gateway (namespace gateway) → order-service (namespace orders) → payment-service (namespace payments). Каждый hop должен быть защищён mTLS с проверкой SPIFFE ID.

Архитектура идентификаторов:

  • spiffe://cluster.local/ns/gateway/sa/api-gateway
  • spiffe://cluster.local/ns/orders/sa/order-service
  • spiffe://cluster.local/ns/payments/sa/payment-service

NetworkPolicy для order-service разрешает входящий трафик только от api-gateway:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gateway-to-orders
  namespace: orders
spec:
  podSelector:
    matchLabels:
      app: order-service
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: gateway
          podSelector:
            matchLabels:
              app: api-gateway
      ports:
        - protocol: TCP
          port: 8443
  policyTypes:
    - Ingress

На уровне приложения order-service дополнительно проверяет, что SPIFFE ID клиента — именно api-gateway. Даже если NetworkPolicy обойдена (например, через compromised ноду), mTLS с проверкой identity не позволит неавторизованному сервису установить соединение.

Цепочка защиты выглядит так:

  1. NetworkPolicy блокирует нежелательный L3/L4 трафик ещё до того, как пакет достигнет пода.
  2. TLS handshake с проверкой сертификата обеспечивает шифрование и аутентификацию на L4.
  3. Проверка SPIFFE ID в Go-коде гарантирует авторизацию на уровне приложения.
  4. Аудит-лог фиксирует каждое успешное и неуспешное взаимодействие.

Такая многоуровневая защита реализует принцип defense-in-depth без единой точки отказа.

Выводы

Zero-trust безопасность в Kubernetes — это не продукт, который можно «установить и забыть». Это архитектурный подход, реализуемый через комбинацию механизмов:

  • NetworkPolicy обеспечивает сетевую изоляцию на L3/L4 без изменения кода сервисов.
  • SPIFFE/SPIRE дают каждому сервису криптографически верифицированный идентификатор с автоматической ротацией.
  • mTLS в Go с проверкой SPIFFE ID закрывает авторизацию на уровне приложения.
  • Аудит и логирование обеспечивают видимость и возможность forensics.

Описанный стек не требует Istio или Linkerd и хорошо вписывается в существующие Kubernetes-кластеры. Отсутствие Service Mesh означает меньше операционной сложности, меньше компонентов, которые могут упасть, и прямой контроль над каждым аспектом безопасности. Для команд, которые хотят полного понимания своей security-модели, это часто лучший выбор, чем непрозрачный sidecar-proxy.

Начните с малого: введите default-deny NetworkPolicy в одном namespace, задеплойте SPIRE, добавьте mTLS в один сервис на Go. Этот инкрементальный путь позволит команде накопить экспертизу, не рискуя стабильностью production.

Технологии

Теги

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

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