본문으로 건너뛰기

Microsoft Graph Webhook Listener

msgraph_webhook 게이트웨이 플랫폼은 인바운드 이벤트 리스너. Hermes가 Microsoft Graph로부터 변경 알림 받는 방식 — "Teams 세션 종료됨", "이 채팅에 새 메시지 도착", "이 캘린더 이벤트 업데이트됨". teams 플랫폼(사용자가 입력하는 채팅 봇)과 다름 — 이건 사람이 아니라 M365가 Hermes에게 뭔가 발생했다고 알리는 것.

현재 주요 컨슈머는 Teams 세션 요약 파이프라인: 세션가 transcript 생성하면 Graph 알림, 파이프라인이 가져옴, Hermes가 요약을 Teams로 다시 게시. 다른 Graph 리소스(/chats/.../messages, /users/.../events)도 같은 리스너 사용 — 파이프라인 컨슈머는 각자 PR로 도착.

사전 요구사항

  • Microsoft Graph 애플리케이션 자격 증명 — Register a Microsoft Graph Application
  • Microsoft Graph가 도달 가능한 공개 HTTPS URL (Graph는 비공개 엔드포인트 호출 안 함). 테스트는 dev tunnel로 충분; 프로덕션은 유효한 인증서 있는 실제 도메인 필요.
  • clientState 값으로 쓸 강력한 공유 시크릿. openssl rand -hex 32로 생성하고 ~/.hermes/.envMSGRAPH_WEBHOOK_CLIENT_STATE로 저장.

빠른 시작

최소 ~/.hermes/config.yaml:

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

또는 ~/.hermes/.env의 env vars로 (시작 시 자동 병합):

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

게이트웨이 시작: hermes gateway run. 리스너 노출 항목:

  • POST /msgraph/webhook — Graph 변경 알림
  • GET /msgraph/webhook?validationToken=... — Graph subscription 검증 핸드셰이크
  • GET /health — accepted/duplicate 카운터 포함 readiness probe

리스너 공개 노출 (reverse proxy, dev tunnel, ingress). Graph subscription용 알림 URL은 공개 HTTPS origin 뒤에 /msgraph/webhook:

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

Configuration

모든 설정은 platforms.msgraph_webhook.extra 하위:

설정기본값설명
host0.0.0.0HTTP 리스너 bind 주소.
port8646Bind 포트.
webhook_path/msgraph/webhookGraph가 POST하는 URL 경로.
health_path/healthReadiness 엔드포인트.
client_stateGraph가 모든 알림에 echo하는 공유 시크릿. hmac.compare_digest와 비교 — openssl rand -hex 32로 생성.
accepted_resources`` (모두 허용)Graph 리소스 경로/패턴 allowlist. 끝의 *은 prefix match. 앞의 /은 허용. 예: ["communications/onlineMeetings", "chats/*/messages"].
max_seen_receipts5000알림 ID dedupe 캐시 크기. 한도 도달 시 가장 오래된 항목 evict.
allowed_source_cidrs`` (모두 허용)선택적 source-IP allowlist. 아래 참조.

각 설정은 게이트웨이 시작 시 config에 병합되는 동등한 env var(MSGRAPH_WEBHOOK_*) 보유 — environment variables reference 참조.

보안 강화

clientState가 주요 인증 체크

모든 Graph 알림은 subscription 등록 시 사용한 clientState 문자열 포함. 리스너는 clientState 불일치하는 알림은 timing-safe 비교로 거부. Microsoft 공식 메커니즘 — 값을 강력한 공유 시크릿으로 취급.

client_state가 unset이면 리스너는 형식만 맞으면 모든 POST 수락. 프로덕션에서 절대 이 상태로 실행 금지.

Source-IP allowlisting (프로덕션 배포)

프로덕션에서는 Microsoft 공식 Graph webhook source IP 범위로 리스너 제한. Microsoft가 Office 365 IP Address and URL Web service에서 egress 범위 문서화. 설정 방법:

platforms:
msgraph_webhook:
enabled: true
extra:
client_state: "..."
allowed_source_cidrs:
- "52.96.0.0/14"
- "52.104.0.0/14"
#...add the current Microsoft 365 "Common" + "Teams" category egress ranges

또는 env var로:

MSGRAPH_WEBHOOK_ALLOWED_SOURCE_CIDRS="52.96.0.0/14,52.104.0.0/14"

빈 allowlist = 어디서든 수락 (기본값; dev-tunnel 워크플로 유지). 잘못된 CIDR 문자열은 경고 로그 남기고 무시. Microsoft IP 목록은 분기별 검토 — 변경됨.

HTTPS 종료

리스너는 평문 HTTP 사용. TLS는 reverse proxy(Caddy, Nginx, Cloudflare Tunnel, AWS ALB)에서 종료 후 로컬 네트워크로 리스너에 프록시. Graph는 비HTTPS 엔드포인트로 전달 거부 — Graph 자체에서 암호화되지 않은 트래픽이 도달할 경로 없음.

응답 위생

성공 시 리스너는 빈 body로 202 Accepted 반환 — 내부 카운터는 wire 응답에 노출 안 됨. 운영자는 /health로 카운트 관찰 가능.

상태 코드 표:

결과상태
알림 수락 또는 dedupe됨202
검증 핸드셰이크 (validationToken 포함 GET)200 (토큰 echo)
배치의 모든 항목이 clientState 실패403
잘못된 JSON / value 배열 누락 / 알 수 없는 리소스400
Source IP가 allowlist에 없음403
validationToken 없는 단순 GET400

트러블슈팅

문제확인 항목
Graph subscription 검증 실패공개 URL 도달 가능, /msgraph/webhook 경로 일치, validationToken 포함 GET이 10초 내에 토큰을 text/plain로 그대로 echo.
알림 POST는 오는데 ingest 안 됨client_state이 subscription 등록값과 일치. 값이 변경됐으면 openssl rand -hex 32 재실행하고 새 subscription 생성. accepted_resources에 Graph가 보내는 리소스 경로 포함 확인.
모든 알림 403clientState 불일치 (위조, 또는 subscription이 다른 값으로 등록됨). hermes teams-pipeline subscribe --client-state "$MSGRAPH_WEBHOOK_CLIENT_STATE"...로 subscription 재생성 (파이프라인 런타임 PR에 포함).
리스너 시작되는데 curl http://localhost:8646/health포트 바인딩 충돌. 필요 시 ss -tlnp | grep 8646 and change port: 확인.
Microsoft에서 오는 실제 Graph 요청이 403Source IP allowlist가 너무 좁음. allowed_source_cidrs 임시 제거하고 트래픽 흐름 확인 후 현재 Microsoft egress 범위 포함하도록 목록 확장.