
The gateway terminates every connection that an agent makes and decides it against
the egress policy before any byte leaves. The policy is the `gateway.destination`
governance key. [Author and pull policy](/guides/author-and-pull-policy/) explains
the authoring surface and how the snapshot reaches the enclave. This page covers
the key itself: what a rule matches, how scopes compose, and what enforcement
looks like in the audit stream.

## What a rule matches

A rule names a destination by host, CIDR, or both, with an optional port. Host
matching is a dot-bounded suffix match: the rule `example.com` matches
`example.com` and `api.example.com`, and does not match `notexample.com`.
Matching ignores case and a trailing dot. A rule with neither a host nor a CIDR
matches nothing. (`internal/policy/policy.go`, the `Rule` type.)

A deny rule carries a reason. The audit record carries the rule's reason into
every deny verdict, so an operator reading a deny sees why the rule exists.

## How scopes compose

The key resolves for each agent across the four scopes, platform, org, group,
and agent, under three laws (`internal/policy/resolver.go`,
`ResolveDestination`):

- **Deny rules union.** Every applicable scope's denies apply, always. A lower
  scope can add denies and cannot remove a deny that a higher scope set. The union
  does not need a lock.
- **Allow rules pick one scope.** The most-specific scope that contributes an
  allow rule wins, unless a broader scope locked the key. A lock pins the allow
  surface at the locked scope, and no more-specific scope or installed pack can
  broaden it.
- **Deny beats allow, and the default is deny.** A host with a matched deny is
  refused even when an allow covers it. A host with no matched rule at all is
  refused too. The gateway never permits an ungoverned destination.

The resolution is lock-aware and the controlplane enforces the invariants at
authoring time: the authoring command refuses a `--lock` at agent scope
(agents never lock), and a
write under a key that a higher scope has locked fails with an error naming the
locking scope (`internal/controlplane/govpolicy/author.go`, `Author.Set`).

## Author the policy

Write the key with `dvx policy destination`. The scope flags select the target:
omit them for platform, or pass `--org`, plus `--group` or `--agent`:

{{< terminal title="dvx" prompt="$" >}}
$ dvx policy destination \
    --controlplane https://cp.example.com \
    --org org_acme --agent agent-a \
    --allow example.com --allow api.partner.example.com \
    --deny evil.example --deny-reason "known exfil host"
{{< /terminal >}}

Each call replaces the whole allow/deny list at that scope, so restate the
complete list every time. The command requires at least one `--allow` or
`--deny` rule. Add `--lock` at platform, org, or group scope to pin the key
against more-specific overrides.

The controlplane publishes the new snapshot and nudges the enclave. The
gateway's policy-pull sidecar fetches it and the gateway hot-reloads it within
five seconds (`snapshotPollInterval` in `internal/enclave/gateway/layeredengine.go`).
A read-back of the snapshot on the gateway pod shows the authored rules; see
[Author and pull policy](/guides/author-and-pull-policy/) for the pull path.

## What enforcement looks like

The gateway evaluates every flow with the agent's attributed identity and
records the verdict as a `gateway.egress` event. Each event carries the
outcome, `allow` or `deny`, plus the policy scope, reason code, and matched rule
that decided (`withDecision` in `internal/enclave/gateway/policyengine.go`).
The durable rows land in the `audit.network_flow` table in the enclave's audit
store. Read them with the audit CLI:

{{< terminal title="dvx" prompt="$" >}}
$ dvx audit events --action gateway.egress --outcome deny --limit 20
{{< /terminal >}}

The reason codes tell you which branch decided:

- `allowed`: the host matched an allow rule.
- `denied`, or the deny rule's own reason: the host matched a deny rule. The
  event names the rule and the scope that imposed it.
- `default_deny`: no rule matched. Denied.
- `no_identity`: attribution is in force but the flow could not be attributed
  to an agent. Denied.
- `contained`: the agent is contained. The gateway denies all of the agent's
  egress, ahead of allow rules and every other floor.

## When it fails closed

The enforcement path refuses to permit what it cannot govern:

- The gateway refuses to start without a policy snapshot. A failed snapshot
  reload keeps the last good snapshot in force (`layeredengine.go`).
- A malformed policy value in the snapshot contributes no allow rules, so the
  flow falls through to default-deny (`ResolveDestination`).
- An unattributed flow is denied when attribution is in force (`no_identity`).
- The gateway denies all egress from a contained agent (`contained`).
- A deny is absolute because the Cilium redirect forces all agent egress
  through the gateway
  (`deploy/policy/agent-egress-redirect.yaml`).

One allowance exists: a configured assert-identity destination is reachable by
default under mandatory identity assertion (`assert_identity_floor`). It applies
only on a pure default-deny, so an explicit deny at any scope still blocks it.

To watch it happen, curl a resolvable host that no rule allows from inside an
agent pod. The example uses `www.wikipedia.org`, which resolves in public DNS
but appears on no allowlist, so the gateway itself makes the denial. (A host
that fails DNS resolution never reaches the gateway, so it produces no
`gateway.egress` row.) The gateway refuses the connection, and records a
`gateway.egress` deny with `default_deny`:

{{< terminal title="sh" prompt="$" >}}
$ kubectl exec -n dvx-enclave <agent-pod> -c agent -- curl -sv https://www.wikipedia.org
...
$ dvx audit events --action gateway.egress --outcome deny --limit 5
{{< /terminal >}}
