Every team with a security posture has inbound filtering. The firewall blocks traffic that is not supposed to reach the service. Almost no one has outbound filtering: an explicit list of what the service is allowed to call, with everything else denied by default. This asymmetry means you have told the network who can knock on your door but have said nothing about where your service is allowed to go. A compromised service, a backdoored dependency, or a manipulated agent workload will use that silence.

What an egress allowlist is

A default-deny outbound policy with explicit permits for the destinations a service legitimately needs. A service that calls a payment API gets a rule permitting HTTPS to the payment API's address range. A service that fetches package updates gets a rule permitting HTTPS to the registry hostname. Everything else is dropped at the network layer before the connection completes.

This is the inverse of how most networks are configured. Most networks default to permit outbound and explicitly deny specific known-bad destinations. A denylist of known-bad destinations is a fundamentally weaker posture: it requires knowing what is bad before it becomes a threat. A new attacker domain, a compromised CDN endpoint, a freshly registered exfiltration target, none of these appear on the denylist until after an incident. The allowlist requires knowing what is good, which is work you can do in advance, from your own codebase.

Why ingress without egress is incomplete

Ingress filtering tells the network who can reach your service. It says nothing about what your service is allowed to do after a request arrives. A service that processes a malicious payload can respond to that payload by calling out: exfiltrating data, contacting attacker infrastructure, or establishing a connection that persists beyond the original request. Ingress filtering does not see any of this. It only filtered the front door.

Ingress filtering tells the network who can reach you. Egress filtering tells you what your own software is allowed to do.

The threat model for egress is not primarily external attackers sending traffic inward. It is your own software, in its current compromised or manipulated state, sending traffic out. The two halves of the firewall address different threats and neither substitutes for the other.

The supply chain problem

A compromised npm package, a malicious PyPI release, a backdoored Go module: each runs inside your service's process, with your service's network permissions. If your service can reach the public internet, so can the compromised dependency. The dependency does not need to escalate privileges or break out of any sandbox. It has exactly the network access your service has, because it is your service.

Egress filtering is one of the few controls that works even when the compromised code is inside your own service boundary. The filtering happens at the network layer, below the trust boundary of the application code. A dependency that tries to phone home finds no route. The call fails. The rest of the service continues to function, and the failed connection appears in your logs.

The same logic applies to server-side request forgery, prompt injection in LLM agent workloads, and deserialization gadget chains: any attack path where untrusted input can influence what the service calls over the network has its blast radius reduced by an egress policy that limits where those calls can go.

How to implement it

Three approaches, in increasing order of control and operational complexity:

ApproachHow it worksBest forLimitation
nftables rules on the host An outbound policy on the host that permits specific destination IPs and ports and drops everything else. Scriptable and auditable; fits in version-controlled config. Services with stable destination IP ranges: first-party APIs, known registries, fixed infrastructure endpoints. IP-based rules break when destinations move to CDNs or rotate addresses. Requires maintaining an IP inventory.
Network namespace isolation The service runs in a network namespace with a veth pair routed only to explicitly permitted destinations. No default route to the broader network exists, so no rule can be accidentally permissive. Workloads where structural isolation is more important than flexibility. Particularly appropriate for sandbox workloads and agent tasks. More setup than host rules. Managing the namespace lifecycle adds operational overhead.
Transparent egress proxy All outbound traffic routes through a proxy that enforces an allowlist by hostname. The proxy terminates TLS (with a CA cert installed in the service), inspects the destination, and forwards or drops. Services that call CDN-backed destinations or destinations that change IP ranges frequently. Provides the richest logging of what was called and when. Adds latency. Adds a proxy as a component that must be maintained. Requires certificate management in the service.

For most services, nftables rules are the right starting point. They are auditable, scriptable, and sufficient for destinations with stable IP ranges. Add the transparent proxy when you need hostname-level matching or detailed egress logs. Add network namespace isolation when the workload runs untrusted code or when the separation needs to be structural rather than policy-based.

The allowlist as an audit

Building an egress allowlist requires knowing what your service actually calls. This is harder than it sounds. Modern applications accumulate network dependencies quietly: a logging library that defaults to a cloud endpoint, a metrics client that initialises with a remote collector, a health check library that resolves an external hostname, an SDK that phones home during initialisation regardless of the feature it supports.

A network trace during integration testing surfaces this inventory before you commit to a policy. Run the service through its normal operation with tcpdump or ss capturing outbound connections. The list of destinations the service contacts, as opposed to the list of destinations you expected it to contact, is the starting point for the allowlist and often reveals surprises.

The exercise has value independent of the policy. The inventory of your service's network behaviour is a useful artefact: it tells you what you are depending on, what breaks when a dependency is unavailable, and where your service's data is going beyond your own infrastructure.

The agent workload case

For services that run untrusted workloads or LLM agent tasks, egress filtering moves from good practice to a required control. An agent that can reach arbitrary internet endpoints, internal services, or cloud metadata endpoints has not been confined. The microVM or container boundary stops direct process and filesystem access to the host; it does not stop the agent from calling out through the network interface it was given.

What an unconfined agent can do over the network

  • Call the cloud metadata endpoint and retrieve instance credentials.
  • Reach internal services that trust traffic from the sandbox subnet.
  • Exfiltrate workspace contents to an external endpoint.
  • Establish a persistent outbound connection that survives the task's intended lifetime.

None of these require breaking the microVM boundary or escalating privileges. They require only the outbound network access that most sandbox configurations provide by default.

A strict egress allowlist for agent workloads has a narrow permitted set: the specific external APIs the task legitimately needs and nothing else. A logged, proxied egress path for all permitted connections gives you a record of every call the agent made. Combined, these two controls tell you what the agent did and contain what it could have done if it tried to do something it was not supposed to.

The operational friction of maintaining the allowlist, knowing precisely what each class of agent task is allowed to call, is also a forcing function. If you cannot enumerate the network destinations a task legitimately needs, you do not fully understand the task's scope. The allowlist does not just restrict the agent; it documents the intended behaviour in a form that the network can enforce.

Most teams filter inbound traffic and stop there: the firewall blocks what should not reach the service but says nothing about where it may go. An egress allowlist closes that gap: a default-deny outbound policy permitting only the destinations a service legitimately needs, dropping everything else at the network layer.

Allowlist, not denylist

Most networks permit outbound by default and block known-bad destinations, the weaker posture: you must know a destination is bad before it is used, so a fresh attacker domain stays permitted until after the incident. An allowlist needs you to know what is good, work you can do in advance from your codebase.

It also catches what ingress cannot: a compromised service can call out to exfiltrate data or reach attacker infrastructure that ingress never sees.

Ingress filtering tells the network who can reach you. Egress filtering tells you what your own software is allowed to do.

It survives a supply-chain compromise

A malicious npm, PyPI, or Go dependency runs inside your process with your network permissions, no privilege escalation required. Egress filtering works below the application trust boundary, so a dependency that phones home finds no route and the failure shows up in your logs. The same holds for SSRF, prompt injection, and deserialisation: any path where untrusted input steers an outbound call.

How to implement it

Three approaches, by increasing control and cost:

  • nftables rules on the host: permit specific destination IPs and ports, drop the rest. Auditable and version-controlled, but breaks when destinations move to CDNs.
  • Network namespace isolation: run the service in a namespace with no default route, only a veth to permitted destinations. Structural, so no rule can be accidentally permissive.
  • Transparent egress proxy: route all outbound through a proxy that allowlists by hostname and terminates TLS. Handles changing IP ranges, but adds latency and certificate management.

Start with nftables for stable IP ranges, add the proxy for hostname matching, add namespace isolation for untrusted code. Building any of them forces you to know what your service actually calls, which a tcpdump or ss trace surfaces, often with surprises.

Agent workloads make it mandatory

For sandboxes running untrusted code or LLM agents, egress filtering becomes a required control. The microVM or container stops process and filesystem access to the host, not the agent calling out through its NIC. An unconfined agent can reach the cloud metadata endpoint for credentials, hit internal services that trust the sandbox subnet, or exfiltrate the workspace, no escape needed.

A strict allowlist permits only the APIs the task needs; a logged, proxied path records every call. The friction is a forcing function: if you cannot enumerate a task's destinations, you do not understand its scope.