Make dynamic egress decisions with a webhook
Attach a webhook to an egress rule so an external endpoint decides at request time how to handle outbound traffic. Use it for tenant-specific authorization, runtime token minting, or dynamic upstream routing that a static rule cannot express.
An egress rule normally carries a static action. Replace it with a hookRef and, when the rule matches, the egress proxy calls your endpoint and applies the decision it returns. A rule has either action or hookRef, never both.
Read Control egress first — a webhook is an advanced field on the same policy file. The webhook is called whenever a request matches the rule, including under Partial traffic inspection; you do not need Full inspection to use it.
Add a webhook rule
A hookRef rule declares the endpoint to call, what to send it, and how to authenticate. This example calls a policy service for every chat completion and applies whatever decision it returns.
defaultAction: Deny
trafficInspection: Full
rules:
- name: dynamic-copilot-policy
match:
host: "*.githubcopilot.com"
path: "/chat/completions"
hookRef:
endpoint: "https://policy.example.net/egressWebHook"
failBehavior: Deny
requestHeaders:
- x-tenant-id
- x-session-id
authHeaders:
- operation: Set
name: Authorization
format: "Bearer {value}"
valueRef:
managedIdentityRef:
resource: "api://policy-gateway"
type: SystemAssigned
| Field | Supported values |
|---|---|
hookRef.endpoint | Required HTTPS URL the proxy calls when the rule matches. |
hookRef.failBehavior | The decision applied when the call fails, times out, or returns an invalid response. Deny (default) or Allow. |
hookRef.requestHeaders | Request header names forwarded to the webhook. Only these headers are sent, and their names are lower-cased in the payload. |
hookRef.authHeaders | Headers the proxy adds to the webhook call to authenticate to your endpoint. Each entry has operation (Set, Insert, Remove), name, an optional format (with {value} as the placeholder), and a value or valueRef. |
authHeaders[].valueRef | managedIdentityRef (resource, type), secretRef, or a static value — the same value sources used by header transforms. |
hookRef.timeoutMs | Optional timeout in milliseconds for the webhook call. |
hookRef.routingMode | How the proxy reaches the webhook endpoint. Default uses the sandbox's routing, Vnet binds the call to the sandbox's VNet IP so the endpoint can be a private service, and Platform forces the node's platform egress. |
Rules are still evaluated in order, first match wins. The webhook is called only when its rule is the first to match.
What the webhook receives
When the rule matches, the proxy sends a POST to hookRef.endpoint with this body:
{
"apiVersion": "adc.egress.hook/v1",
"sandboxId": "...",
"armSandboxGroupResourceId": "/subscriptions/.../resourceGroups/.../providers/Microsoft.App/sandboxGroups/...",
"request": {
"protocol": "http",
"scheme": "https",
"host": "api.example.com",
"path": "/v1/chat/completions",
"method": "POST",
"headers": {
"x-tenant-id": "tenant-123",
"x-session-id": "session-abc"
}
}
}
| Field | Meaning |
|---|---|
apiVersion | Contract version. Currently adc.egress.hook/v1. |
sandboxId | The sandbox that made the request. |
armSandboxGroupResourceId | The ARM resource ID of the sandbox group, when the sandbox is associated with one. Omitted when unavailable. It identifies the group to your receiver but does not affect ADC evaluation, routing, or whether the hook is called. |
request.protocol | http or tds. It tags which fields are populated. |
request.scheme | http or https. Populated for http. |
request.host | Destination host of the outbound request. Populated for http. |
request.path | Request path. Populated for http. |
request.method | HTTP method. Populated for http. |
request.headers | Only the headers named in hookRef.requestHeaders, with lower-cased names. Populated for http. |
The same endpoint can serve SQL (TDS) traffic. For a tds request the proxy populates server, database, and login instead of the HTTP fields.
The authHeaders you configured are added to this request so your endpoint can authenticate the caller. They are not part of the JSON body.
What the webhook returns
Return a decision the proxy applies to the outbound request:
{
"decision": {
"type": "Rewrite",
"headers": [
{
"operation": "Set",
"name": "Authorization",
"value": "Bearer <token>"
}
],
"scheme": "https",
"host": "api.openai.com",
"path": "/v1/responses"
},
"reason": "tenant override",
"cacheTtlSeconds": 5,
"cacheKey": {
"fields": ["Method"],
"headers": ["x-tenant-id"]
}
}
| Field | Meaning |
|---|---|
decision.type | Allow, Deny, Transform, or Rewrite. |
decision.headers | Header mutations for Transform or Rewrite. Each has operation (Set, Insert, Remove), name, and value. Only literal values are accepted — secret and managed-identity references are not resolved from a webhook response. |
decision.scheme | Rewrite target scheme. Valid for Rewrite. |
decision.host | Rewrite target host. Valid for Rewrite. |
decision.path | Rewrite target path. Valid for Rewrite. Query strings from the original request are preserved. |
decision.routingMode | Optionally overrides how the proxy reaches the rewritten upstream. Default, Vnet, or Platform. Valid for Rewrite. |
decision.trustedCa | Optional PEM bundle that adds trust for the upstream TLS handshake. See Trust a private upstream CA. |
reason | Free-text reason recorded with the decision. |
cacheTtlSeconds | Seconds to cache this decision. Omit or 0 to call the webhook on every request. |
cacheKey | Which request values distinguish cached decisions. See Cache decisions. |
Decision types
| Type | Behavior |
|---|---|
Allow | Forward the request unchanged. |
Deny | Reject with 403. |
Transform | Apply the header mutations, then forward. |
Rewrite | Redirect the request to scheme/host/path and optionally apply header mutations. |
Cache decisions
When cacheTtlSeconds is positive, the proxy caches the decision so it does not call the webhook on every request. cacheKey controls which request values produce a distinct cache entry.
The cache is always scoped to the sandbox, hook endpoint, ARM sandbox-group resource ID, normalized hostname, and matched rule. You cannot broaden past that scope, so two rules that share a host and endpoint never share cached decisions. cacheKey narrows further within the scope:
| Field | Meaning |
|---|---|
cacheKey.fields | Any of Scheme, Method, Path. Each named field must match for a cache hit. |
cacheKey.headers | Header names — only those declared in the rule's hookRef.requestHeaders are used; other names are ignored. |
Omitting a field is a promise that the decision is reusable across changes to it. For example, omit Path to serve one host-and-tenant decision for every path. When cacheKey is absent, the proxy keys on scheme, method, path, and every header declared in hookRef.requestHeaders. The proxy accepts at most eight distinct key shapes per cache scope.
Trust a private upstream CA
A Rewrite or Allow decision can return decision.trustedCa, a PEM bundle of one or more CA certificates. When the upstream request is TLS, the proxy adds these CAs to the trust store for that request only — system roots still validate, and the supplied CAs cover additional chains, such as a self-signed cluster root that a sandbox needs to reach.
{
"decision": {
"type": "Allow",
"trustedCa": "-----BEGIN CERTIFICATE-----\nMIIB...==\n-----END CERTIFICATE-----\n"
}
}
Constraints:
- A hostname mismatch is never waived. Only chain-trust errors can be satisfied by the supplied CAs; any other TLS error still fails.
- The PEM payload is at most 64 KiB and at most 8 certificates. Each certificate must be a CA certificate; a leaf-only bundle is rejected.
- On a non-TLS request the proxy cannot apply
trustedCa. It logs a warning and uses default trust. - The proxy keeps at most 8 distinct TLS-scoped configurations per sandbox source IP and routing mode, evicting the least recently used.
Apply and verify
Apply the policy file the same way as any other egress policy, then exercise the matching rule from inside the sandbox and check the audit view.
- Bash
- PowerShell
aca sandbox egress apply -l name=demo-egress --file egress.yaml
aca sandbox egress decisions -l name=demo-egress
aca sandbox egress apply -l name=demo-egress --file egress.yaml
aca sandbox egress decisions -l name=demo-egress
Each decision the webhook returns is recorded like any other egress decision, with the matched rule name and the reason you supplied.