Daevix Docs

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.

View as Markdown Edit on Tangled

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
 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
 dvx audit events --action gateway.egress --outcome deny --limit 20

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:

sh
 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