Author an egress policy
Write the destination allow and deny rules that the enclave gateway enforces on every agent connection, scope and lock them, and read the audit verdicts that prove what the policy did.
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 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:
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"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 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:
dvx audit events --action gateway.egress --outcome deny --limit 20The 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:
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