Enable Network Egress Filtering
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:
- Auto-generate on first startup (default)
- Generate with the CLI (pin/rotate via GitOps)
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.
Run the gen-egress-ca subcommand to generate the CA yourself, then hand the files to Kubernetes as a Secret so the chart can reference it via networkFilter.ca.existingSecret:
aicontrols gen-egress-ca --out-cert ./networkfilter-ca.crt --out-key ./networkfilter-ca.key
The command prints the CA's subject, SHA-256 fingerprint, and expiry, followed by the exact kubectl create secret command to load it:
kubectl create secret generic aicontrols-networkfilter-ca \
--from-file=tls.crt=./networkfilter-ca.crt \
--from-file=tls.key=./networkfilter-ca.key \
-n aicontrols
Use this path when you want the CA under version control / GitOps rotation instead of the proxy managing it implicitly.
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
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.
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:
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.
- Primary: centralized deployment (the default)
- Alternative: sidecar deployment
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).
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.
If AIControls instead runs as a container inside the same pod as the agent workload (proxy.deploy: sidecar), two Kubernetes-level controls — both configured through the Helm chart, both scoped to that shared pod — close the escape route without any agent-side cooperation:
networkFilter:
transparentRedirect:
enabled: true
networkPolicy:
enabled: true
# allowEgress:
# - to:
# - ipBlock:
# cidr: 10.0.0.0/8
# ports:
# - protocol: TCP
# port: 443
transparentRedirect adds an init container that installs iptables NAT/REDIRECT rules forcing outbound TCP :80/:443 traffic from that pod to the egress listener — this only works because the init container shares a network namespace with the agent; it has no effect on traffic originating from a different pod. networkPolicy adds a default-deny-egress NetworkPolicy scoped to AIControls' own pod, permitting DNS, its own listener ports, and outbound TCP :80/:443 to any destination (plus anything you add under allowEgress). That last rule is intentionally broad but safe in combination with transparentRedirect: the iptables rule redirects all other pod traffic on those ports to the egress listener before it can leave the pod, exempting only the AIControls process's own UID — so the outbound :80/:443 allow only ever carries proxy-originated traffic that has already passed policy evaluation, never a workload container bypassing it.
The transparent-redirect rule exempts traffic from the AIControls container's own UID (podSecurityContext.runAsUser, default 65534) to avoid the egress proxy's own outbound connections looping back into itself. Your workload/agent containers must run as a different UID. If a workload container shares the same UID as the AIControls container, its egress traffic will be exempted from redirect too — silently bypassing enforcement — or, if the UIDs collide the other way, loop and hang. Set an explicit, distinct securityContext.runAsUser on your agent containers.
If you customize the deployment (extra containers, a different probe path, etc.), make sure liveness/readiness probes always target the admin port (/healthz, /readyz), never the egress listener port. The listener speaks MITM'd HTTP(S), not a plain health-check protocol, and a probe pointed at it will fail or hang. The chart's default probes already target the admin port — this only matters if you override them.