Модуль 2. Безопасность брокеров: TLS, SASL, ACL и секреты

Цель модуля: закрыть брокеров от «гостя по умолчанию»: настроить аутентификацию (SASL/PLAIN, SCRAM, TLS-сертификаты, NATS creds/NKeys), авторизацию (vhost/permissions, Kafka ACL, NATS account permissions), шифрование in transit/at rest, сетевую изоляцию и работу с секретами в Go-коде. Практика — TLS+SASL для всех трёх брокеров в docker-compose.

Перед началом: курсы брокеров трека (механика), Модуль 1 (OrderFlow). Инструменты: openssl, docker-compose, keytool (Kafka), nsc (NATS). Трудоёмкость: ~8–10 ч (теория 3 ч · практика 5 ч · квиз 1 ч).

📖 2.1. Аутентификация: кто ты

SASL — общий каркас

SASL (Simple Authentication and Security Layer) — фреймворк аутентификации поверх протокола. Механизмы, которые важны на практике:

МеханизмКак работаетСекрет на проводеВердикт
PLAINлогин+пароль в открытом видедатолько внутри TLS
SCRAM-SHA-256/512proof-of-knowledge (challenge-response)нетминимум для Kafka
TLS client cert (mTLS)сертификат клиента = личностьн/псервис-сервис стандарт

Железное правило: SASL/PLAIN без TLS — пароль в открытом виде на проводе. Даже «внутренняя» сеть — не аргумент (8.4 объяснит, почему).

RabbitMQ

bash
# включаем TLS-листенер (5671) и auth-механизмы
# /etc/rabbitmq/rabbitmq.conf:
listeners.tcp.default = 5672
listeners.ssl.default = 5671
ssl_options.cacertfile  = /etc/rabbitmq/tls/ca.pem
ssl_options.certfile    = /etc/rabbitmq/server.pem
ssl_options.keyfile     = /etc/rabbitmq/server.key
ssl_options.verify      = verify_peer
ssl_options.fail_if_no_peer_cert = true   # mTLS: без сертификата не пустим

auth_mechanisms.1 = PLAIN

Клиент amqp091-go: amqp.DialTLS("amqps://order:...@rabbit:5671/", tlsConfig) — и обязательно проверяйте, что tlsConfig.RootCAs содержит ваш CA, а ServerName совпадает с CN сертификата брокера (иначе проверка молча отключится).

Kafka: SASL_SSL + SCRAM

properties
# broker: listener с SASL_SSL
listeners=SASL_SSL://:9093
listener.security.protocol.map=SASL_SSL:SASL_SSL
sasl.enabled.mechanisms=SCRAM-SHA-512
sasl.inter.broker.protocol=SCRAM-SHA-512

# создаём пользователя SCRAM (секреты хранятся в ZK/KRaft)
kafka-configs.sh --bootstrap-server localhost:9093 \
  --alter --add-config 'SCRAM-SHA-512=[password=OrderSvcSecret]' \
  --entity-type users --entity-name order-svc
go
// franz-go с SCRAM
cl, err := kgo.NewClient(
	kgo.SeedBrokers("kafka:9093"),
	kgo.DialTLSConfig(&tls.Config{RootCAs: caPool, ServerName: "kafka"}),
	kgo.SASL(scram.Client{
		Auth: scram.Auth{User: "order-svc", Pass: pass},
		HashGen: scram.SHA512,
	}),
)

NATS: creds и NKeys

NATS не использует пароли в привычном виде — там NKeys (Ed25519 ключи) и декларативные .creds-файлы (JWT-токен аккаунта + seed).

text
# order.creds (выдаётся nsc)
-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5...
------END NATS USER JWT------
-----BEGIN USER NKEY SEED-----
SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIW4754ABCD1234
------END USER NKEY SEED------
go
nc, err := nats.Connect("tls://nats:4222",
	nats.UserCredentials("order.creds"),
	nats.RootCAs("ca.pem"),
)

Seed — эквивалент приватного ключа: утёк = скомпрометирован сервис. В k8s он живёт в Secret, монтируется файлом, а не зашивается в образ.

📖 2.2. Авторизация: что тебе можно

БрокерЕдиницаПример политики
RabbitMQvhost + permissions (configure/write/read на regex ресурсов)order-svc: write на orders.*, read только на свою reply-очередь
KafkaACL: principal → операция (Read/Write/Describe...) → ресурс (topic/group)order-svc: Write на orders.events, payment-svc: Read на orders.events + Group payment
NATSaccount permissions: publish/subscribe allow/deny по subject'амnotify-svc: sub orders.status.>, deny pub orders.>

Принцип наименьших привилегий работает и здесь: сервис должен уметь публиковать в свои топики и читать из своих — и ничего больше. Это превращает случайный «топик всем» из Модуля 1 курса «Фундамент» в невозможное.

bash
# Kafka ACL: payment-svc читает orders.events
kafka-acls.sh --bootstrap-server localhost:9093 --add \
  --allow-principal User:payment-svc --operation Read --operation Describe \
  --topic orders.events
kafka-acls.sh --bootstrap-server localhost:9093 --add \
  --allow-principal User:payment-svc --operation Read \
  --group payment
text
# NATS: аккаунт payment-svc (в конфиге оператора)
accounts: {
  payment_svc: {
    nkey: UBAPAY...
    permissions: {
      subscribe: { allow: ["orders.events.>", "$JS.API.CONSUMER.>"] }
      publish:   { allow: [], deny: ["orders.>"] }
    }
  }
}

Обратите внимание на $JS.API.CONSUMER.> — JetStream-консьюмеру нужны и управляющие subject'ы API, не только data-subject'ы. Забытый API-permission — самая частая причина «почему консьюмер не фетчит» после включения авторизации.

RabbitMQ vhost'ы — дополнительно изоляция сред: dev, staging, prod в одном кластере не видят ресурсов друг друга.

📖 2.3. Шифрование: in transit и at rest

In transit: TLS на всех листенерах клиентских соединений и между узлами кластера (inter-broker: Kafka listener.security.protocol.map, RabbitMQ ssl_options на clustering-порту, NATS tls в cluster-блоке). Забытый inter-broker TLS = plaintext-репликация внутри ДЦ.

At rest: шифрование дисков/томов брокеров (LUKS, cloud KMS-backed тома) и/или встроенные механизмы (Kafka: шифрование уровня приложения или зоопарк/KRaft-данные на шифрованных томах; NATS JetStream — файлы стримов на зашифрованном томе). Компромисс: CPU на крипто, ключи надо чем-то защищать (KMS/Vault), восстановление сложнее. Решение — по модели угроз: если угроза — «уносят диск», том-шифрование достаточно; если «читают ваш брокер с правами admin» — нужна app-level криптография.

📖 2.4. Сетевая безопасность: брокер — не публичный сервис

Shodan полон открытых RabbitMQ/Kafka с guest/guest — это всегда инцидент.

Чек-лист изоляции:

  1. Брокеры слушают приватную сеть (docker-сеть, VPC subnet); порты 5672/9092/4222 не проброшены на 0.0.0.0 наружу. Management UI (15672) — через VPN/bastion, не в интернет.
  2. advertise правильные адреса: Kafka advertised.listeners должен отдавать клиентам внутренние DNS-имена — иначе клиенты получат публичный IP и пойдут мимо VPN.
  3. Network policies / security groups: сервисные поды могут ходить только на брокерные порты; брокер — только к своим узлам.
  4. mTLS между сервисами и брокером там, где сеть общая (мультитенантный кластер, shared VPC).
  5. Аудит: включите логирование auth-событий и алерт на неуспешные логины — брут-форс виден раньше, чем он успешен.

📖 2.5. Секреты в Go-коде: env, файлы, Vault

Иерархия решений (от простого к зрелому):

  1. Env/файлы из оркестратора (docker-compose secrets, k8s Secret): минимум, который обязан быть всегда.
go
type BrokerConfig struct {
	URL      string
	User     string
	Password string
	CAFile   string
}

func LoadFromEnv() (BrokerConfig, error) {
	pass := os.Getenv("ORDER_SVC_BROKER_PASSWORD")
	if pass == "" {
		return BrokerConfig{}, errors.New("ORDER_SVC_BROKER_PASSWORD is required")
	}
	return BrokerConfig{
		URL:      envOr("ORDER_SVC_BROKER_URL", "tls://localhost:5671"),
		User:     envOr("ORDER_SVC_BROKER_USER", "order-svc"),
		Password: pass,
		CAFile:   envOr("ORDER_SVC_BROKER_CA", "/etc/tls/ca.pem"),
	}, nil
}
  1. Vault / cloud secrets manager: динамические креды с TTL, ротация без деплоя, аудит доступа. Go-SDK:
go
import vault "github.com/hashicorp/vault/api"

client, err := vault.NewClient(vault.DefaultConfig())
_ = client.SetToken(vaultToken) // токен агента/роли, не «корневой»
secret, err := client.KVv2("secret").Get(ctx, "orderflow/broker")
pass := secret.Data["password"].(string)
  1. Инъекция файлом (Vault Agent / CSI driver): секрет не проходит через память приложения вовсе — читаем смонтированный файл.

Гигиена, за которую дают расти:

  • Секреты никогда не в git (даже «в приватном») и не в логах: логируйте принципала (user=order-svc), не креденшелы.
  • Ошибка подключения не печатает URL с паролем — вычищайте креды из строк подключения перед логированием (amqps://order-svc:***@...).
  • Ротация: пароль/creds меняются без релиза — значит, они в конфиге, а не в бинаре.
  • Разные принципалы на сервис (8.2) — ротация и отзыв точечные.

💻 Практика: TLS+SASL для трёх брокеров

  1. CA и сертификаты:
bash
openssl genrsa -out ca.key 4096
openssl req -x509 -new -key ca.key -days 365 -out ca.pem -subj "/CN=course-ca"

# серверный сертификат брокера с SAN (без SAN современный Go-клиент не примет)
openssl req -new -key server.key -out server.csr -subj "/CN=rabbit"
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
  -out server.pem -days 365 \
  -extfile <(printf "subjectAltName=DNS:rabbit,DNS:localhost")
  1. RabbitMQ: включите 5671 (8.1), создайте vhost orderflow и пользователя order-svc с правами write на orders.* — и убедитесь, что read-запрос на чужую очередь отклонён.
  2. Kafka: SASL_SSL listener + SCRAM для order-svc/payment-svc, ACL из 8.2; проверьте, что payment-svc не может писать в топик.
  3. NATS: поднимите оператор/аккаунты через nsc, раздайте creds, ограничьте publish/subscribe; проверьте запрет публикации.
  4. OrderFlow: переведите все три драйвера Watermill на TLS+аутентификацию; весь прогон интеграционных тестов Модуля 1 должен пройти без правок бизнес-кода — это и есть признак правильной изоляции конфигурации.
  5. Отрицательные тесты (главные!): без сертификата — connection refused; с чужим CA — ошибка цепочки; с правым пользователем и чужим топиком — authorization denied. Зафиксируйте в README, как выглядит каждая ошибка.

🧩 Quiz "Проверь себя"

1. NATS-креденшел (.creds) содержит seed NKey. Что это значит для обращения с файлом?
2. Зачем brokers слушать только приватную сеть, если есть SASL+TLS?
3. Kafka ACL: что нужно principal payment-svc для чтения orders.events в группе payment?
4. Почему SASL/PLAIN нельзя использовать без TLS?
5. mTLS (verify_peer + fail_if_no_peer_cert) в RabbitMQ означает...
6. Где правильно хранить пароль брокера в Go-сервисе?
7. JetStream-консьюмер после включения NATS-авторизации перестал фетчить сообщения, хотя data-subject разрешён. Типичная причина?
8. Чем SCRAM-SHA-512 лучше PLAIN при том же TLS-канале?
9. advertised.listeners в Kafka: почему важно отдавать клиентам внутренние DNS-имена?
10. Какие слои защиты обязательны для брокера в продакшене? — выберите все подходящие

✅ Итоги модуля

После прохождения ты должен уметь:

  • Выбрать механизм аутентификации (PLAIN-in-TLS / SCRAM / mTLS / NKeys) и объяснить, что каждый защищает.
  • Выдать сервису минимальные права: vhost-permissions, Kafka ACL, NATS account permissions — включая JetStream API-subject'ы.
  • Включить TLS на клиентских и inter-broker соединениях, понимать роль SAN и цепочки CA.
  • Изолировать брокера сетью: приватные листенеры, правильный advertised.listeners, network policies.
  • Обращаться с секретами как инженер: env/Vault/файлы, без git и логов, с ротацией.
  • Писать отрицательные тесты безопасности и читать их ошибки.

Шпаргалка модуля

СлойRabbitMQKafkaNATS
АутентификацияPLAIN/mTLS на 5671SASL_SSL + SCRAMcreds/NKeys
Авторизацияvhost + permissions (regex)ACL (principal→op→resource)account permissions (subjects)
In transitTLS 5671 + cluster TLSSASL_SSL + inter-broker SSLtls:// + cluster tls
Управлениеrabbitmqctl/definitionskafka-acls/configsnsc + operator config
Типовая дыраguest/guest, UI наружуplaintext 9092seed в git

🔗 Мостик к Модулю 3

Защищённый брокер всё равно может захлебнуться: 100k msg/s не пролезут через плохо настроенный prefetch, а p99 в 2 секунды убьёт UX. Дальше — масштабирование и производительность: от тюнинга продюсера и консьюмера до методики поиска узких мест и единого бенчмарк-стенда для трёх брокеров. Научимся отвечать на вопрос «почему тормозит» — цифрами.