Three Things "Set HTTPS_PROXY" Cannot Stop
Enforcing Egress: Why Environment Variables Fail for Agent Security
Current Situation Analysis
Modern AI agents and automation frameworks increasingly operate with broad system access, making egress control a critical security requirement. A pervasive operational pattern involves setting the HTTPS_PROXY environment variable to route agent traffic through a scanning gateway. This approach assumes that the proxy becomes the mandatory choke point for all outbound communication.
This assumption is fundamentally flawed. Environment variables are application-layer hints, not kernel-enforced rules. The Linux kernel does not inspect process environments when routing packets; it routes based on network namespaces, IP tables, and socket ownership. Consequently, any process that can manipulate its execution context or choose its transport layer can bypass HTTPS_PROXY without triggering a single kernel alarm.
The industry overlooks this gap because developers conflate configuration with enforcement. Setting HTTPS_PROXY works for cooperative HTTP libraries, but it provides zero protection against:
- Environment manipulation: Subprocesses spawned without the proxy variable.
- Transport diversity: Raw TCP, UDP, QUIC, and ICMP traffic that ignores HTTP proxy semantics.
- Internal routing loops: Services listed in
NO_PROXYthat possess their own outbound capabilities, effectively tunneling traffic around the proxy boundary.
In default Linux configurations, the kernel sees these bypasses but lacks the rules to block them. The result is a false sense of security where audit logs show clean proxy traffic while the agent silently exfiltrates data or accesses unauthorized resources via alternative paths.
WOW Moment: Key Findings
The distinction between application-layer hints and kernel-level enforcement is not incremental; it is categorical. The following comparison demonstrates why environment variables cannot serve as a security control.
| Control Mechanism | Bypass Surface | Transport Scope | Enforcement Level | Identity Binding |
|---|---|---|---|---|
HTTPS_PROXY Env Var | High (Env clear, lib choice, NO_PROXY abuse) | HTTP/HTTPS only | Application Hint | None (Process can drop vars) |
| Kernel UID/Pod Rule | Low (Requires privilege escalation) | All Protocols (TCP/UDP/ICMP) | Hard Enforcement | Strong (Socket ownership) |
| NetworkPolicy (K8s) | Low (Requires pod escape) | Per-Protocol/Port | Hard Enforcement | Strong (Pod identity) |
Why this matters: Moving from environment variables to identity-based kernel rules closes all bypass shapes simultaneously. The kernel does not care whether the process intends to use a proxy, which library it loads, or what hostname it targets. It only checks the source identity against the rule set. This shift transforms egress control from a "polite request" to a deterministic security boundary.
Core Solution
The robust solution requires identity-centric egress control. You must isolate the agent process under a dedicated identity and enforce a deny-all egress policy that permits traffic only to the proxy endpoint. This approach is transport-agnostic and environment-independent.
Step 1: Isolate the Agent Identity
The agent must 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.
```bash
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_PROXYguarantees 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.localor*.internalinNO_PROXY. If any service matching the wildcard has outbound internet access, the agent can tunnel traffic through it. - Fix: Minimize
NO_PROXYto127.0.0.1andlocalhost. Route internal traffic through the proxy to maintain scanning visibility.
3. UDP and DNS Blind Spots
- Explanation:
HTTPS_PROXYdoes 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.,
rootor a genericappuser). 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 -iis the only way to clear variables. Processes can useexecveor 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
- Assign Dedicated Identity: Create a unique UID for the agent or deploy it in a dedicated pod with a distinct label.
- Draft Egress Rules: Write nftables rules or NetworkPolicy YAML to deny all egress except loopback, DNS, and the proxy endpoint.
- Audit
NO_PROXY: Review theNO_PROXYlist. Remove wildcards and internal services. Restrict to loopback addresses. - Verify Internal Services: Identify all internal services the agent can reach. Ensure they do not have unrestricted outbound internet access.
- Test Bypasses: Run audit commands (e.g.,
env -i curl,nc -z) as the agent identity to confirm traffic is blocked. - Monitor Drops: Configure logging for dropped packets to detect bypass attempts and misconfigurations.
- Disable QUIC: Ensure agent libraries are configured to prefer HTTP/2 and do not fall back to QUIC.
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-runneron 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.
