How requests flow through GraphWarden
GraphWarden is a reverse proxy that stands between a client application and Microsoft Graph. The client authenticates to the proxy, the proxy evaluates the ruleset bound to that client's App Identity, and allowed calls are forwarded to Graph with the real tenant credentials the proxy reads from your Key Vault.
Shipped in Phase 1Prerequisites
The request lifecycle
A request traverses five numbered stages. Each stage is a decision point and a failure point; the troubleshooting section below maps each to its observable error.
- Client token request — your app posts
client_id+client_secret(the proxy pair issued by GraphWarden) toPOST /proxy/tokenusing the OAuth2client_credentialsgrant. No Graph call happens at this stage. - Proxy authenticates the client — the proxy looks up the proxy
client_idin its identity store, compares a SHA-256 hash of the supplied secret against the stored hash, and — if valid — reads the paired Graph client secret from your Azure Key Vault. - Ruleset evaluation — when your app subsequently calls a Graph endpoint (e.g.
GET /v1.0/users), the proxy decodes its own JWTBearertoken, resolves the App Identity, loads the associated ruleset, and walks the rules top-down. The first rule whosematchandconditionspass decides the verdict:allow,filter,block, orlog_only. - Forward to Graph — for
allow,filter, andlog_only, the proxy issues the upstream request tohttps://graph.microsoft.comusing the real tenant token it obtained from MSAL. The request path and query string are preserved byte-for-byte by the YARP catch-all route. - Response transformation — on the way back,
filterrules applyallow_propertiesorstrip_propertiesto the response body.blocknever reaches Graph and returns403 Forbiddendirectly. Every outcome (allowed, filtered, blocked) produces an audit event.
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
│ Your app │───▶│ GraphWarden │───▶│ Microsoft Graph │
└──────────┘ │ Proxy │ │ graph.ms.com │
└──────┬───────┘ └──────────────────┘
│
▼
┌──────────────┐
│ Audit log │
│ (Elastic) │
└──────────────┘
Components
The runtime is a single self-hosted ASP.NET Core service. The pieces below are load-bearing and worth naming explicitly; everything else is auxiliary.
- Kestrel — the proxy runs as a Kestrel-hosted ASP.NET Core service. It is NOT hosted behind IIS. On Windows Server the binary registers as a Windows Service via
sc.exe create; in hosted mode it runs as a container process. - YARP reverse proxy — a catch-all route (
Match: { Path: "/{**catch-all}" }) forwards every authenticated request tohttps://graph.microsoft.comwith path and query preserved. No per-endpoint routing table is maintained. - MSAL client — the proxy uses Microsoft Authentication Library to acquire Graph tokens via the client-credentials flow, keyed on the real Azure AD
client_idresolved from the App Identity. Tokens are cached to their natural expiry. - YamlDotNet rule loader — rulesets are YAML files read at startup and on each sync cycle.
RuleLoader.csparses the schema, validates conditions and transforms, and materializes an evaluation tree in memory. - Serilog — structured JSON logs are emitted to stdout (container) or the Windows Event Log (service). An audit sink ships one event per Graph request to Elasticsearch when
Audit:ElasticsearchUrlis configured.
What the proxy does NOT do
GraphWarden's scope is narrow on purpose. Confusion about the proxy usually traces back to assuming it does one of the things below.
- It does not sign users in. GraphWarden speaks app-to-service OAuth2 only. User authentication (interactive sign-in, delegated scopes, MFA prompts) remains in Azure AD. If your app uses delegated flows today, you still acquire the user token from Azure AD; the proxy only intermediates app tokens.
- It does not cache Graph response bodies by default. Per-rule
cache_ttlcaches the rule verdict for a given caller and object, not the raw payload. Response bodies are forwarded fresh on every request unless a rule explicitly opts in. - It does not store user data. Audit events are metadata: timestamp, caller App Identity, HTTP method, URI, status code, latency, matched rule. Request and response bodies are NOT written to the audit store unless a response-filter transform inspects them in-flight to redact fields.
- It does not replace Azure RBAC or Conditional Access. GraphWarden gates Graph API calls from your apps; it does not evaluate user role assignments, device compliance, or sign-in risk. Those continue to live in Azure AD and Microsoft Entra.
Troubleshooting
401 Unauthorized from /proxy/token — the client_id or client_secret your app sent does not match any App Identity. Confirm the pair was issued by GraphWarden App Admin for this environment (proxy credentials differ per deployed proxy instance). Check the proxy log for Unknown proxy client_id or Secret hash mismatch.
403 Forbidden on a Graph request with a valid Bearer token — a ruleset evaluated the request and returned block, OR no rule matched and the default policy in your tenant is deny. Inspect the audit event for the correlation ID in the response header and find the MatchedRuleId field to see which rule fired.
502 Bad Gateway returned from a Graph endpoint that normally works — the proxy reached the upstream but Graph returned an error the proxy could not classify, OR MSAL failed to acquire a tenant token. Check Kestrel stdout for Upstream Graph call failed and confirm the Graph secret in Key Vault has not expired.
Requests hang for minutes, then time out — the proxy is failing to reach Key Vault. Managed Identity may be misconfigured, or the outbound egress rule from the proxy host blocks *.vault.azure.net. Confirm az login --identity works from the host if applicable.
See it in production
Scenarios that anchor this architecture to concrete customer patterns — multi-tenant SaaS, internal HR integrations, SIEM-fed audit — are collected on the use cases page. For a walkthrough against your own Graph-integrated estate, contact the GraphWarden team.
Last reviewed: