Модуль 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/512 | proof-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------
gonc, err := nats.Connect("tls://nats:4222", nats.UserCredentials("order.creds"), nats.RootCAs("ca.pem"), )
Seed — эквивалент приватного ключа: утёк = скомпрометирован сервис. В k8s он живёт в Secret, монтируется файлом, а не зашивается в образ.
📖 2.2. Авторизация: что тебе можно
| Брокер | Единица | Пример политики |
|---|---|---|
| RabbitMQ | vhost + permissions (configure/write/read на regex ресурсов) | order-svc: write на orders.*, read только на свою reply-очередь |
| Kafka | ACL: principal → операция (Read/Write/Describe...) → ресурс (topic/group) | order-svc: Write на orders.events, payment-svc: Read на orders.events + Group payment |
| NATS | account 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 — это всегда инцидент.
Чек-лист изоляции:
- Брокеры слушают приватную сеть (docker-сеть, VPC subnet); порты 5672/9092/4222 не проброшены на 0.0.0.0 наружу. Management UI (15672) — через VPN/bastion, не в интернет.
- advertise правильные адреса: Kafka
advertised.listenersдолжен отдавать клиентам внутренние DNS-имена — иначе клиенты получат публичный IP и пойдут мимо VPN. - Network policies / security groups: сервисные поды могут ходить только на брокерные порты; брокер — только к своим узлам.
- mTLS между сервисами и брокером там, где сеть общая (мультитенантный кластер, shared VPC).
- Аудит: включите логирование auth-событий и алерт на неуспешные логины — брут-форс виден раньше, чем он успешен.
📖 2.5. Секреты в Go-коде: env, файлы, Vault
Иерархия решений (от простого к зрелому):
- Env/файлы из оркестратора (
docker-compose secrets, k8s Secret): минимум, который обязан быть всегда.
gotype 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 }
- Vault / cloud secrets manager: динамические креды с TTL, ротация без деплоя, аудит доступа. Go-SDK:
goimport 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)
- Инъекция файлом (Vault Agent / CSI driver): секрет не проходит через память приложения вовсе — читаем смонтированный файл.
Гигиена, за которую дают расти:
- Секреты никогда не в git (даже «в приватном») и не в логах:
логируйте принципала (
user=order-svc), не креденшелы. - Ошибка подключения не печатает URL с паролем — вычищайте креды
из строк подключения перед логированием (
amqps://order-svc:***@...). - Ротация: пароль/creds меняются без релиза — значит, они в конфиге, а не в бинаре.
- Разные принципалы на сервис (8.2) — ротация и отзыв точечные.
💻 Практика: TLS+SASL для трёх брокеров
- CA и сертификаты:
bashopenssl 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")
- RabbitMQ: включите 5671 (8.1), создайте vhost
orderflowи пользователяorder-svcс правами write наorders.*— и убедитесь, что read-запрос на чужую очередь отклонён. - Kafka: SASL_SSL listener + SCRAM для
order-svc/payment-svc, ACL из 8.2; проверьте, чтоpayment-svcне может писать в топик. - NATS: поднимите оператор/аккаунты через
nsc, раздайте creds, ограничьте publish/subscribe; проверьте запрет публикации. - OrderFlow: переведите все три драйвера Watermill на TLS+аутентификацию; весь прогон интеграционных тестов Модуля 1 должен пройти без правок бизнес-кода — это и есть признак правильной изоляции конфигурации.
- Отрицательные тесты (главные!): без сертификата — connection refused; с чужим CA — ошибка цепочки; с правым пользователем и чужим топиком — authorization denied. Зафиксируйте в README, как выглядит каждая ошибка.
🧩 Quiz "Проверь себя"
✅ Итоги модуля
После прохождения ты должен уметь:
- Выбрать механизм аутентификации (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 и логов, с ротацией.
- Писать отрицательные тесты безопасности и читать их ошибки.
Шпаргалка модуля
| Слой | RabbitMQ | Kafka | NATS |
|---|---|---|---|
| Аутентификация | PLAIN/mTLS на 5671 | SASL_SSL + SCRAM | creds/NKeys |
| Авторизация | vhost + permissions (regex) | ACL (principal→op→resource) | account permissions (subjects) |
| In transit | TLS 5671 + cluster TLS | SASL_SSL + inter-broker SSL | tls:// + cluster tls |
| Управление | rabbitmqctl/definitions | kafka-acls/configs | nsc + operator config |
| Типовая дыра | guest/guest, UI наружу | plaintext 9092 | seed в git |
🔗 Мостик к Модулю 3
Защищённый брокер всё равно может захлебнуться: 100k msg/s не пролезут через плохо настроенный prefetch, а p99 в 2 секунды убьёт UX. Дальше — масштабирование и производительность: от тюнинга продюсера и консьюмера до методики поиска узких мест и единого бенчмарк-стенда для трёх брокеров. Научимся отвечать на вопрос «почему тормозит» — цифрами.