Слушатель вебхуков Microsoft Graph

Платформа шлюза msgraph_webhook — это входящий слушатель событий. Через неё Hermes принимает уведомления об изменениях из Microsoft Graph: «встреча в Teams завершилась», «в этом чате появилось новое сообщение», «событие в календаре обновили». От платформы teams (чат-бот, которому пишут пользователи) она отличается принципиально: здесь M365 сам сообщает Hermes, что что-то произошло, а не человек что-то печатает.

Сейчас главный потребитель этих событий — конвейер сводок по встречам Teams. Graph присылает уведомление, как только для встречи готов транскрипт, конвейер забирает его, и Hermes публикует сводку обратно в Teams. Другие ресурсы Graph (/chats/.../messages, /users/.../events) ходят через тот же слушатель — потребители для них в конвейере приедут отдельными PR.

Предварительные требования

  • Учётные данные приложения Microsoft Graph — Регистрация приложения Microsoft Graph
  • Публичный HTTPS-URL, до которого Microsoft Graph сможет дотянуться (Graph не звонит на приватные эндпоинты). Для тестов сгодится dev-туннель; для прода нужен настоящий домен с действующим сертификатом.
  • Стойкий общий секрет для значения clientState. Сгенерируйте его командой openssl rand -hex 32 и положите в ~/.hermes/.env под именем MSGRAPH_WEBHOOK_CLIENT_STATE.

Быстрый старт

Минимальный ~/.hermes/config.yaml:

platforms:
  msgraph_webhook:
    enabled: true
    extra:
      host: 127.0.0.1
      port: 8646
      client_state: "replace-with-a-strong-secret"
      accepted_resources:
        - "communications/onlineMeetings"

Либо через переменные окружения в ~/.hermes/.env (они подмешиваются автоматически при запуске):

MSGRAPH_WEBHOOK_ENABLED=true
MSGRAPH_WEBHOOK_PORT=8646
MSGRAPH_WEBHOOK_CLIENT_STATE=<generate-with-openssl-rand-hex-32>
MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES=communications/onlineMeetings

Учтите: хост для привязки берётся из extra.host в config.yaml (см. пример выше); переменной MSGRAPH_WEBHOOK_HOST для переопределения нет.

Запустите шлюз: hermes gateway run. Слушатель отдаёт следующие эндпоинты:

  • POST /msgraph/webhook — уведомления об изменениях от Graph
  • GET /msgraph/webhook?validationToken=... — рукопожатие для валидации подписки Graph
  • GET /health — проба готовности со счётчиками принятых и дублей

Откройте слушатель наружу (через обратный прокси, dev-туннель, ingress). URL для уведомлений в подписках Graph — это ваш публичный HTTPS-origin плюс /msgraph/webhook:

https://ops.example.com/msgraph/webhook

Конфигурация

Все настройки лежат под platforms.msgraph_webhook.extra:

НастройкаПо умолчаниюОписание
host0.0.0.0Адрес привязки HTTP-слушателя. Для привязки не к loopback нужен allowed_source_cidrs; loopback (127.0.0.1 / ::1) — самый простой вариант для dev-туннеля или обратного прокси.
port8646Порт привязки.
webhook_path/msgraph/webhookURL-путь, на который Graph отправляет POST.
health_path/healthЭндпоинт готовности.
client_stateОбщий секрет, который Graph возвращает в каждом уведомлении. Сравнивается через hmac.compare_digest — генерируйте командой openssl rand -hex 32.
accepted_resources[] (принимать все)Список разрешённых путей и шаблонов ресурсов Graph. Завершающая * работает как совпадение по префиксу. Ведущий / допускается. Пример: ["communications/onlineMeetings", "chats/*/messages"].
max_seen_receipts5000Размер кэша дедупликации для идентификаторов уведомлений. При достижении лимита старые записи вытесняются.
allowed_source_cidrs[]Обязателен при привязке не к loopback. Оставляйте пустым, только когда слушатель привязан к loopback и спрятан за локальным туннелем или обратным прокси.

У большинства настроек есть эквивалентная переменная окружения (MSGRAPH_WEBHOOK_*), которая подмешивается в конфиг при запуске шлюза (исключение — host: только конфиг, см. примечание выше) — см. справочник по переменным окружения.

Усиление безопасности

clientState — основная проверка подлинности

В каждое уведомление Graph кладёт строку clientState, с которой регистрировалась ваша подписка. Слушатель отклоняет любое уведомление с несовпадающим clientState, сравнивая значения в режиме, устойчивом к атакам по времени. Так это описано в документации Microsoft — относитесь к значению как к стойкому общему секрету.

Если client_state не задан, слушатель попросту не стартует.

Список разрешённых IP-адресов источника (для прод-развёртываний)

В проде ограничьте слушатель опубликованными Microsoft диапазонами IP-источников для вебхуков Graph. Microsoft публикует диапазоны исходящего трафика в веб-сервисе IP-адресов и URL Office 365. Задайте их так:

platforms:
  msgraph_webhook:
    enabled: true
    extra:
      host: 0.0.0.0
      client_state: "..."
      allowed_source_cidrs:
        - "52.96.0.0/14"
        - "52.104.0.0/14"
        # ...добавьте актуальные диапазоны исходящего трафика категорий Microsoft 365 «Common» + «Teams»

Или через переменную окружения:

MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS="52.96.0.0/14,52.104.0.0/14"

Привязка к хосту вне loopback — 0.0.0.0, :: или IP в локальной сети — без allowed_source_cidrs отклоняется уже на старте. Если у вас dev-туннель или обратный прокси на той же машине, привяжите Hermes к 127.0.0.1 или ::1 и оставьте список разрешений там пустым. Неверные строки CIDR пишутся в лог предупреждением и игнорируются. Сверяйтесь со списком IP от Microsoft раз в квартал — он меняется.

Терминация HTTPS

Слушатель говорит по обычному HTTP. Завершайте TLS на обратном прокси (Caddy, Nginx, Cloudflare Tunnel, AWS ALB) и проксируйте до слушателя по локальной сети. Graph отказывается доставлять на эндпоинты без HTTPS, так что незашифрованному трафику из самого Graph до вас попросту нет дороги.

Гигиена ответов

При успехе слушатель возвращает 202 Accepted с пустым телом — внутренние счётчики в ответ по проводу не утекают. Подсмотреть значения счётчиков оператор может через /health, который защищён теми же правилами по IP-источнику, что и путь вебхука.

Таблица кодов статуса:

ИсходСтатус
Уведомление(я) приняты или отброшены как дубль202
Рукопожатие валидации (GET с validationToken)200 (возвращает токен)
По всем элементам пакета не сошёлся clientState403
Битый JSON / нет массива value / неизвестный ресурс400
IP источника не в списке разрешений403
Голый GET без validationToken400

Диагностика

ПроблемаЧто проверить
Валидация подписки Graph не проходитПубличный URL достижим, путь /msgraph/webhook совпадает, GET с validationToken возвращает токен дословно как text/plain в течение 10 секунд.
POST с уведомлениями приходят, но ничего не загружаетсяclient_state совпадает с тем, под которым регистрировалась подписка. Если значение разъехалось, перегенерируйте его через openssl rand -hex 32 и создайте новую подписку. Проверьте, что accepted_resources включает путь ресурса, который шлёт Graph.
Каждое уведомление получает 403Несовпадение clientState (подделка или подписка зарегистрирована с другим значением). Пересоздайте подписку командой hermes teams-pipeline subscribe --client-state "$MSGRAPH_WEBHOOK_CLIENT_STATE" ... (приедет вместе с PR на рантайм конвейера).
Слушатель не запускается на 0.0.0.0Задайте allowed_source_cidrs с актуальными диапазонами исходящего трафика вебхуков Microsoft либо привяжите Hermes к 127.0.0.1 / ::1 за туннелем или обратным прокси.
Слушатель запустился, но curl http://localhost:8646/health зависаетКонфликт привязки порта. Проверьте ss -tlnp | grep 8646 и при необходимости смените port:.
Настоящие запросы Graph от Microsoft ловят 403Список разрешённых IP-источников слишком узкий. Расширьте его, добавив актуальные диапазоны исходящего трафика Microsoft. Если вы ещё отлаживаете путь через туннель, привяжите Hermes к loopback и доверьте публичный доступ туннелю.

Связанные документы

ESC