Skip to main content

Enable Network Egress Filtering

Tasks

These steps turn on the egress-filtering proxy for a workload, from generating a trust root through writing your first policy. For what this feature is and why it exists, see Network Egress Filtering.

Step 1 — Get a CA certificate

The egress proxy terminates TLS itself, so it needs a root CA that agent processes will trust. Choose one:

Leave networkFilter.ca.existingSecret unset. On first startup with networkFilter.enabled: true, AIControls generates an ECDSA P-256 CA (10-year validity) and persists it in its own state store, so it survives restarts as long as the same volume is reused. You don't need to run any command — the CA appears the first time the proxy starts.

Retrieve the generated cert later using the verification step below.

Step 2 — Configure the egress proxy

Network Egress Filtering has two independent switches — a process-level feature flag and a runtime enabled toggle. The feature flag gates whether the subsystem exists in the running process at all (no listener, no admin API routes, and the Settings UI section / Network Traffic nav item stay hidden until it's set) — off by default, and changing it always requires a restart. The runtime toggle, enabled below, only controls whether the listener is currently bound once the feature is present, and — as the tip below explains — is adjustable live from the UI without a restart.

Enable both featureEnabled and enabled in your Helm values.yaml and upgrade:

networkFilter:
featureEnabled: true # process-level flag — requires a Helm upgrade/restart to change
enabled: true
listenAddr: ":8443"
# Leave ca.existingSecret empty to use the auto-generated CA from Step 1,
# or point it at the Secret you created with gen-egress-ca:
ca:
existingSecret: "aicontrols-networkfilter-ca"
# Hosts that use certificate pinning and cannot be intercepted — relayed
# as a raw TCP stream with no policy applied, logged only:
passthrough: []
# - "*.docker.io"
# Trust an additional CA for the proxy's own OUTBOUND connection to an
# upstream that presents a certificate from a private/internal CA (e.g.
# a Kubernetes API server's own serving cert) — added to the system root
# pool, not a replacement for it, so public HTTPS upstreams stay trusted:
upstreamCACertFile: ""

Set upstreamCACertFile when agents need to reach an upstream whose certificate isn't already covered by the public CA pool — mount the upstream's CA into the AIControls pod (for a cluster's own API server, typically /var/run/secrets/kubernetes.io/serviceaccount/ca.crt) and point this field at it.

helm upgrade aicontrols \
oci://ghcr.io/nirmata/charts/aicontrols \
--version $VERSION \
-n aicontrols \
-f values.yaml
note

This enables interception with policy visibility only — traffic is intercepted and logged/enforced, but an agent process can still bypass it by not trusting the CA or by not routing through the listener. Step 6 below adds the Kubernetes-level controls that make interception mandatory rather than opt-in.

Runtime changes don't need a Helm upgrade

Once featureEnabled: true has been deployed, an admin can toggle network egress filtering enabled on/off and edit the passthrough list and SSRF allowlist at runtime from Settings → Network Filtering in the AIControls UI, and view live traffic under Network Traffic in the nav — both are hidden until featureEnabled is set. Those changes take effect immediately on the running listener — no Helm upgrade or pod restart required.

Step 3 — Trust the CA in agent pods

For an agent's HTTP client to accept the proxy's on-the-fly certificates instead of failing TLS verification, the agent container needs to trust the CA certificate from Step 1. Mount it into the container and point the runtime's trust variable at it — the exact variable depends on the agent's language/runtime:

Agent workloads in a different namespace

Kubernetes Secrets are namespace-scoped. If your agent workloads run in a different namespace than the AIControls release (the common case for a centralized deployment — see Step 6), you can't reference aicontrols-networkfilter-ca directly from there. Download the CA with kubectl get secret aicontrols-networkfilter-ca -n aicontrols -o jsonpath='{.data["tls.crt"]}' | base64 -d > ca.crt (or via the verification step below) and create a ConfigMap/Secret from it in the agent namespace instead.

env:
- name: NODE_EXTRA_CA_CERTS # Node.js
value: /etc/aicontrols/networkfilter-ca/tls.crt
- name: SSL_CERT_FILE # Python / most Go binaries
value: /etc/aicontrols/networkfilter-ca/tls.crt
- name: REQUESTS_CA_BUNDLE # Python requests library
value: /etc/aicontrols/networkfilter-ca/tls.crt
volumeMounts:
- name: networkfilter-ca
mountPath: /etc/aicontrols/networkfilter-ca
readOnly: true
volumes:
- name: networkfilter-ca
secret:
secretName: aicontrols-networkfilter-ca

Step 4 — Identify agents making egress calls

Embed the agent's token as the proxy URL's username, so HTTP_PROXY/HTTPS_PROXY carry it as a Proxy-Authorization credential on every request:

env:
- name: HTTP_PROXY
value: "http://$(AGENT_TOKEN)@aicontrols.aicontrols.svc.cluster.local:8443"
- name: HTTPS_PROXY
value: "http://$(AGENT_TOKEN)@aicontrols.aicontrols.svc.cluster.local:8443"

No agent-side code change is needed beyond these two environment variables — curl, kubectl, gh, and any other proxy-aware HTTP client picks this up automatically. See Agent identity for network egress calls for why this mechanism exists, how it's resolved, precedence vs. the MCP/LLM header, and a worked policy example.

Step 5 — Write a first CEL policy

Egress calls are matched with object.http.host != "" — the same matchConditions/validations/validationActions shape as any other policy. Start with an allowlist in Audit mode to see what your agents actually reach before enforcing:

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: egress-domain-allowlist
spec:
matchConditions:
- expression: 'object.http.host != ""'
validations:
- expression: |
object.http.host in ["api.github.com", "registry.npmjs.org"] ||
object.http.host.endsWith(".anthropic.com")
message: "Egress to this host is not on the allowlist"
validationActions: [Audit]

Once you've reviewed the audit log and are confident in the allowlist, switch to [Deny]. To route a specific class of request to human review instead of blocking it outright, add a warn-approve rule — for example, requiring approval for any write-like call outside a trusted API host:

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: egress-warn-approve-for-writes
annotations:
proxy.nirmata.io/enforcement-mode: warn-approve
spec:
matchConditions:
- expression: 'object.http.host != ""'
- expression: 'object.http.method in ["POST", "PUT", "DELETE"]'
validations:
- expression: 'object.http.host == "api.github.com"'
message: "Write-like egress calls outside api.github.com require approval"
validationActions: [Deny]

See Policy Context for the full set of object.http.* fields, and Write a Policy for the general policy-authoring workflow.

Step 6 — Verify

Download the CA certificate from the admin API (requires an admin or viewer session token — developer/builder tokens cannot call this endpoint):

curl -s -H "Authorization: Bearer $TOKEN" \
https://YOUR_WORKSPACE_URL/api/v1/networkfilter/ca.pem \
-o aicontrols-egress-ca.pem

Check that the proxy reports the feature as enabled:

curl -s -H "Authorization: Bearer $TOKEN" \
https://YOUR_WORKSPACE_URL/api/v1/networkfilter/config | jq .

From inside an agent pod that trusts the CA and is configured to route through the listener, make a test request and confirm it appears in the audit log with object.http.host populated:

curl -s https://api.github.com/octocat

If the destination is not on your allowlist policy, the request fails closed with a policy_denied response instead of reaching the upstream — this confirms enforcement is active, not just passthrough logging.

Step 7 — Enforce mandatory routing

Steps 1-5 give you visibility and policy control over any traffic that happens to go through the egress listener — but an agent that isn't configured to route through it (or that's configured incorrectly) isn't governed at all. Which control closes that gap depends on how AIControls is deployed relative to your agent workloads.

If AIControls is a standalone service in the cluster — not a sidecar in the same pod as any agent — there's no shared network namespace to install a kernel-level redirect into. Instead, make routing through the proxy the only way agent pods can reach the network at all:

1. Point agent workloads at the AIControls Service. Set HTTP_PROXY/HTTPS_PROXY on your agent Deployments to the AIControls Service's address (the default port matches networkFilter.listenAddr, :8443):

env:
- name: HTTP_PROXY
value: "http://aicontrols.aicontrols.svc.cluster.local:8443"
- name: HTTPS_PROXY
value: "http://aicontrols.aicontrols.svc.cluster.local:8443"
- name: NO_PROXY
# Scoped to ONLY localhost and the AIControls Service's own hostname —
# not a blanket .svc.cluster.local exclusion. This avoids the self-loop
# where a proxy-aware client would otherwise tunnel the agent's own
# MCP (:8890) and LLM (:8891) calls through the egress listener (:8443)
# first, since all three live on the same hostname, just different
# ports — MCP/LLM calls are already governed by their own dedicated
# pipeline and were never meant to transit the egress plane.
#
# Deliberately narrow, not broad: any OTHER cluster-internal
# destination should still attempt to route through the egress proxy,
# not bypass it. The NetworkPolicy below is what actually decides
# whether that connection is *allowed* (default-deny — nothing else is,
# unless you extend allowEgress) — NO_PROXY only decides whether the
# *client* tries to send it through the proxy or directly. Excluding
# more hosts here than just AIControls' own doesn't grant any extra
# network access, but it does mean anything you later add to
# allowEgress would be reachable *and* silently ungoverned (no CEL
# policy, no audit) if it's also excluded here. If your agents
# legitimately need another internal destination, prefer adding it to
# `networkFilter.ssrf.allowCIDRs` (the SSRF floor blocks private IP
# ranges by default) and a CEL policy, and route it through the proxy
# like everything else — don't add it to NO_PROXY.
value: "localhost,127.0.0.1,aicontrols.aicontrols.svc.cluster.local"

Adjust aicontrols.aicontrols.svc.cluster.local (in all three env vars) to your actual release name and namespace (<service-name>.<namespace>.svc.cluster.local).

2. Lock down agent egress with a NetworkPolicy in the agent namespace. This is the actual bypass-resistance backstop in this topology: an agent that skips or misconfigures HTTP_PROXY gets no egress at all, rather than silently reaching the internet ungoverned. Apply this in the agent workloads' own namespace — it is not part of the AIControls Helm chart, since that chart doesn't own the agent namespace or its pod labels:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: agent-egress-through-aicontrols
namespace: <agent-namespace>
spec:
podSelector:
matchLabels:
<agent-pod-label-selector> # e.g. app: my-agent
policyTypes:
- Egress
egress:
# DNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# The AIControls Service only
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: <aicontrols-namespace>
podSelector:
matchLabels:
app.kubernetes.io/instance: <aicontrols-release-name>
ports:
- protocol: TCP
port: 8443

Replace the placeholders with your agent namespace/label selector and your AIControls release's namespace/name. This stops an agent from reaching the internet directly if it skips HTTP_PROXY — it does not by itself force an agent to send a specific destination through the proxy if that destination happens to be reachable within the allowed egress (there is none here besides the AIControls Service and DNS, so in practice this is a hard backstop for this policy as written).

Troubleshooting: traffic isn't showing up in AIControls

If an agent's calls aren't appearing in the audit log or the Network Traffic view after setting HTTP_PROXY, the most common cause is an HTTP client library that doesn't read the proxy environment variables the way you'd expect — some runtimes need proxy configuration passed explicitly to the client/transport rather than relying on process-wide env vars, or read a different variable name for HTTPS than for HTTP. Check your agent's specific HTTP client/language documentation for how it discovers a proxy, rather than assuming HTTP_PROXY/HTTPS_PROXY are universally honored.

See also