Skip to main content

Restrict Network Egress

Task

Configure a scoped allowlist of network domains for MCP tool traffic. Calls to servers outside the effective list are denied with an appealable message; approved exceptions open specific domains for specific people, temporarily.

Configure the allowlist

Egress rules are a five-tier scoped setting (egressRules in the settings API): global, teams, users, agents, and tokens. Each tier lists allowed domain patterns — an exact domain (api.example.com) or a suffix wildcard (*.example.com, which matches subdomains but not the apex).

{
"global": [
{ "allowedDomains": ["*.internal.corp", "github.com", "registry.npmjs.org"] }
],
"teams": [
{ "scope": "platform-eng", "rules": [ { "allowedDomains": ["github.com", "registry.npmjs.org"] } ] }
]
}

Rules are validated on save: every team/user/agent/token domain must be covered by a global domain (child tiers can only narrow the organization ceiling, never widen it), and patterns outside the exact-or-*.suffix grammar are rejected.

How enforcement works

For every MCP tool call, the proxy:

  1. Resolves the effective allowed-domain list — the intersection (AND) of every tier that applies to the caller. The most restrictive combination wins.
  2. Overlays exceptions — active approved exceptions for the caller add their granted domains to the resolved list.
  3. Clamps mutations — a patchType: Defaults mutating policy may narrow the list further, but any attempt to widen it is clamped back.
  4. Checks the target — the call's destination host is matched against the final list. No match means the call is denied with a message naming the controller (network.egress) and the host, so the developer can request an exception.

With no egress rules configured (and no legacy label, below), no restriction applies — all egress is allowed, matching pre-existing behavior. Policies can also read the resolved list as object.defaults.egress.allowedDomains.

In observe mode, egress denials are recorded as shadow decisions and the call proceeds.

Exceptions

A denied developer can be granted a time-bounded exception: an approved exception targeting controller network.egress with the denied host as its subject adds that domain to the developer's effective list on their next call. When the exception expires (or is revoked), the domain is dropped automatically — no cleanup needed. Expiry fires an exception_expired notification.

See Request a config-target exception for how a developer requests one and how you approve it.

Migrating from the egress label

The per-agent nirmata.io/allowed-egress-domains identity label (comma-separated domains) is deprecated. It still works — each label domain d is folded into the resolution above as the pattern pair {d, *.d}, preserving its legacy "domain and all subdomains" match — and when both the label and egress rules are present, the effective list is their intersection.

Migrate by expressing each labeled agent's domains in egressRules (typically an agents-tier entry), remembering the mapping: label domain d ⇒ patterns d and *.d. A deprecation warning is logged (and the aicontrols_egress_label_fold_total metric counts label folds) while the label remains in use; removal will come in a later, announced release.

Two label behaviors changed deliberately with this transition:

  • Hostname matching is now case-insensitive. The legacy check compared hostnames case-sensitively — a bug, since DNS names are case-insensitive; a mixed-case URL could slip past (or be wrongly denied by) a lowercase label entry. Hosts and patterns are now compared case-insensitively.
  • Wildcard entries in the label are now live patterns. The legacy check treated a label entry like *.example.com as a literal string that could never match (an effective deny for that entry); such entries now work as suffix wildcards.

A label that is present but contains no valid domains (for example, only whitespace or commas) denies all egress — the same fail-closed behavior the legacy check had for that misconfiguration.

See also

  • Policy Context — reading object.defaults.egress from policies
  • Policies — Defaults mutations and the widening clamp