run under a unique User ID (UID) on bare metal or within a dedicated Pod in Kubernetes. Shared identities allow other processes to inherit the agent's network permissions or allow the agent to inherit broader permissions.
Bare Metal: Create a dedicated user.
sudo useradd -r -s /usr/sbin/nologin agent-runner
Kubernetes: Ensure the agent runs in a namespace with strict RBAC and a dedicated service account.
Step 2: Apply Kernel-Level Egress Rules
Once isolated, apply rules that drop all outbound traffic from the agent's identity except traffic destined for the proxy.
nftables Implementation (Bare Metal):
The following rule set creates a table that filters output based on socket UID. It allows loopback and DNS to a local resolver, permits traffic to the proxy, and drops everything else.
table inet agent_egress {
chain output {
type filter hook output priority 0; policy drop;
# Allow loopback
oifname "lo" accept
# Allow DNS to local resolver (replace 127.0.0.53)
ip daddr 127.0.0.53 udp dport 53 accept
ip daddr 127.0.0.53 tcp dport 53 accept
# Allow traffic to proxy (replace 10.0.0.100:8080)
ip daddr 10.0.0.100 tcp dport 8080 accept
# Drop all other traffic from agent UID
meta skuid agent-runner drop
}
}
Kubernetes NetworkPolicy:
NetworkPolicy enforces egress at the pod level. This policy allows only TCP traffic to the proxy service.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: agent-egress-restrict
namespace: agent-ns
spec:
podSelector:
matchLabels:
app: ai-agent
policyTypes:
- Egress
egress:
# Allow DNS
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow Proxy
- to:
- podSelector:
matchLabels:
app: scanning-proxy
ports:
- protocol: TCP
port: 8080
Step 3: Sanitize NO_PROXY
Even with kernel rules, NO_PROXY can create logical bypasses if internal services have outbound access. Restrict NO_PROXY to loopback addresses only. If internal services must be reached, route them through the proxy as well.
export NO_PROXY="127.0.0.1,localhost"
Code Example: Demonstrating the Bypass vs. Enforcement
The following TypeScript example illustrates how a subprocess can bypass environment variables, and why kernel rules are the only effective mitigation.
Bypass Demonstration (Application Layer Failure):
import { spawn } from 'child_process';
// The parent process has HTTPS_PROXY set.
// The child is spawned with an empty environment, dropping the hint.
const child = spawn('curl', ['https://external-api.io/data'], {
env: {}, // Clears all environment variables
stdio: 'inherit'
});
child.on('exit', (code) => {
console.log(`Process exited with code ${code}`);
});
In this scenario, curl connects directly to external-api.io. The proxy is bypassed. If only HTTPS_PROXY is relied upon, this traffic is unscanned.
Enforcement Reality (Kernel Layer Success):
When the same code runs under the nftables or NetworkPolicy rules defined above, the kernel intercepts the connect() syscall. The kernel identifies the socket owner as agent-runner (or the agent pod). It checks the egress rules, sees that external-api.io is not the proxy, and drops the packet. The curl command fails with a connection timeout or reset. The bypass is neutralized regardless of environment variables or library choices.
Pitfall Guide
1. The "Polite" Proxy Fallacy
- Explanation: Assuming
HTTPS_PROXY guarantees routing. Environment variables are optional; libraries can ignore them, and processes can drop them.
- Fix: Never rely on environment variables for security boundaries. Implement kernel-level egress controls.
2. Wildcard NO_PROXY Expansion
- Explanation: Using patterns like
*.cluster.local or *.internal in NO_PROXY. If any service matching the wildcard has outbound internet access, the agent can tunnel traffic through it.
- Fix: Minimize
NO_PROXY to 127.0.0.1 and localhost. Route internal traffic through the proxy to maintain scanning visibility.
3. UDP and DNS Blind Spots
- Explanation:
HTTPS_PROXY does not cover UDP. Agents can exfiltrate data via DNS queries or use QUIC/HTTP/3 over UDP to bypass TCP-based proxy rules.
- Fix: Ensure firewall rules explicitly drop UDP traffic unless required for DNS resolution to a trusted resolver. Block QUIC if not needed.
4. Shared Identity Risks
- Explanation: Running the agent as a shared UID (e.g.,
root or a generic app user). This prevents granular firewall rules and allows other processes to inherit agent permissions.
- Fix: Use a dedicated UID for the agent. In Kubernetes, use a dedicated pod with a specific label selector for NetworkPolicy.
5. Internal Gateway Tunneling
- Explanation: Internal services (e.g., LLM gateways, MCP servers, logging aggregators) often have outbound access. Agents can call these services directly (via
NO_PROXY) and use them as proxies to reach the internet.
- Fix: Treat internal services as untrusted egress points. Audit all services reachable by the agent for outbound capabilities. Restrict internal service egress or route agent traffic through the main proxy.
6. QUIC/HTTP/3 Fallback
- Explanation: Modern HTTP libraries and browsers may fall back to QUIC for performance. QUIC runs over UDP and typically ignores proxy variables.
- Fix: Disable QUIC in agent configurations where possible. Enforce HTTP/2 over TCP. Ensure UDP is blocked by kernel rules.
7. Incomplete Environment Sanitization
- Explanation: Assuming
env -i is the only way to clear variables. Processes can use execve or library calls to drop specific variables while retaining others.
- Fix: Kernel rules catch all execution paths. Do not attempt to patch application-layer environment handling; enforce at the kernel.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Kubernetes Cluster | NetworkPolicy + Pod Isolation | Native integration, scalable, declarative. | Low (Native feature). |
| Bare Metal / VM | nftables + Dedicated UID | Direct kernel control, covers all transports. | Medium (Requires sysadmin setup). |
| High-Security Enclave | nftables + Seccomp + Read-Only FS | Defense in depth; prevents process manipulation and file writes. | High (Complexity and latency). |
| Legacy App Constraints | Proxy + App-Layer Hardening | Kernel rules may break legacy apps; use as interim measure. | Medium (Risk remains). |
Configuration Template
nftables Script for Agent Egress Control:
#!/bin/bash
# agent-egress.nft
# Flush existing rules for this table
flush ruleset
table inet agent_egress {
chain output {
type filter hook output priority 0; policy drop;
# Allow loopback
oifname "lo" accept
# Allow DNS to local resolver
ip daddr 127.0.0.53 udp dport 53 accept
ip daddr 127.0.0.53 tcp dport 53 accept
# Allow traffic to proxy
ip daddr 10.0.0.100 tcp dport 8080 accept
# Drop all other traffic from agent UID
meta skuid agent-runner drop
}
}
Kubernetes NetworkPolicy Template:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: agent-strict-egress
namespace: production
spec:
podSelector:
matchLabels:
role: ai-agent
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- to:
- podSelector:
matchLabels:
app: egress-proxy
ports:
- protocol: TCP
port: 8080
Quick Start Guide
- Create Agent User: Run
sudo useradd -r -s /usr/sbin/nologin agent-runner on the host.
- Apply Firewall: Save the nftables script and load it with
sudo nft -f agent-egress.nft.
- Run Agent: Execute the agent process as the dedicated user:
sudo -u agent-runner ./run-agent.sh.
- Verify: Attempt a direct connection:
sudo -u agent-runner curl https://example.com. The command should hang or fail.
- Confirm Proxy: Ensure the agent is configured to use the proxy. Test a proxied request to verify legitimate traffic flows.
By shifting from environment variables to kernel-enforced identity rules, you eliminate the bypass surface inherent in application-layer hints. This approach provides deterministic egress control that remains effective regardless of process behavior, transport choices, or configuration manipulations.