Skip to content

A NetworkPolicy Restricting Egress From an Inference Pod to a Model Provider

10 min read · updated August 11, 2026

The requirement is usually written as “the inference pod may only talk to our model provider”. Kubernetes will express that, but not in the terms it was written in: a NetworkPolicy matches IP ranges and ports, and a provider endpoint is a DNS name. This page writes the policy that works and is specific about the gap.

This does nothing without the right CNI

The Kubernetes documentation is blunt about it: network policies are implemented by the network plugin, and creating a NetworkPolicy without a controller that implements it has no effect. There is no error, no warning and no event — kubectl apply succeeds and traffic continues to flow. Calico and Cilium implement it; a cluster running plain flannel does not.

Check before you write anything. On managed clusters this is often an opt-in add-on that must be enabled at cluster creation, so discovering it late can mean rebuilding a cluster. The cheapest confirmation is to apply a deny-all policy in a scratch namespace and watch a pod in it lose connectivity. If it does not, stop here.

Kubernetes documentation on network policies

Deny all egress first

NetworkPolicy is allow-list only; there is no deny rule. Isolation is a side effect of selection: once any policy with Egress in its policyTypes selects a pod, that pod may only make the connections some policy explicitly allows. So the default-deny is a policy with an empty rule set.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: inference-default-deny-egress
  namespace: serving
spec:
  podSelector:
    matchLabels:
      app: inference
  policyTypes:
    - Egress

Note what is absent: there is no egress: key at all. That is deliberate, and it is why policyTypes must be written out. Kubernetes documents that when policyTypes is omitted it always includes Ingress and includes Egress only if the policy has egress rules — so a policy with neither the field nor any rules is an ingress policy that does nothing to egress.

NetworkPolicy is namespaced and the podSelector is evaluated within the policy’s own namespace. A pod in another namespace with the same labels is untouched.

Allow DNS or nothing resolves

This is the step that turns a five-minute task into an afternoon. With the deny-all applied, the pod can no longer reach CoreDNS, so every hostname lookup fails. The symptom is not a connection refusal — it is a resolution timeout, usually surfacing in an SDK as a generic connection error after several seconds, which reads like the provider being down.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: inference-allow-dns
  namespace: serving
spec:
  podSelector:
    matchLabels:
      app: inference
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Both protocols matter. UDP carries the ordinary query; TCP is used when a response exceeds what fits in a UDP datagram and the resolver retries, which happens more often than people expect against endpoints with long CNAME chains. Note the single list item containing both namespaceSelector and podSelector — that is an AND. Written as two list items it becomes an OR, and you would be allowing every pod in kube-system plus every kube-dns pod in every namespace. This is the most commonly mis-written stanza in the entire API.

Allow the provider

Now the actual rule. ipBlock takes a CIDR and an optional except list of narrower CIDRs to carve out of it.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: inference-allow-provider
  namespace: serving
spec:
  podSelector:
    matchLabels:
      app: inference
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 203.0.113.0/24
        - ipBlock:
            cidr: 198.51.100.0/22
            except:
              - 198.51.100.0/24
      ports:
        - protocol: TCP
          port: 443

The CIDRs above are documentation ranges and are placeholders. Take the real ones from whatever the provider publishes — several publish a machine-readable egress range file, and if yours does not, that is a finding you should surface before committing to this design rather than after.

Provider IP ranges are revised without notice and without a deprecation window. A policy that hard-codes them is a scheduled outage unless something re-reads the published list. Treat the CIDR list as configuration with an owner, not as a constant in a manifest.

One subtlety about ipBlock: it matches the destination IP as the plugin sees it, and cluster-internal traffic to a Service goes to a virtual IP that is translated afterwards. Do not try to express “may reach the sidecar” with a CIDR; use a podSelector for in-cluster destinations and reserve ipBlock for things genuinely outside the cluster.

Verify it

  1. Apply all three policies to the namespace: kubectl apply -f policies/ -n serving. Order does not matter; policies are additive.
  2. Confirm the pod is selected. kubectl describe networkpolicy prints the resolved selector, but the check that counts is kubectl get pods -n serving -l app=inference returning the pods you meant.
  3. From inside the pod, prove resolution still works: kubectl exec -n serving deploy/inference -- getent hosts api.example.com. If this hangs, the DNS rule is wrong, not the provider rule.
  4. Prove the allowed path works, with a timeout so a blocked connection fails fast rather than hanging: kubectl exec -n serving deploy/inference -- curl -sS -m 5 -o /dev/null -w '%{http_code}' https://api.example.com/v1/models.
  5. Prove something else is blocked. Curl an unrelated public host with the same timeout and expect exit code 28 — the timeout — rather than a refusal. Egress policies drop rather than reject, so “hangs then times out” is what success looks like here.

Where CIDR allowlisting breaks down

The Kubernetes documentation lists targeting by name among the things network policies cannot do. That limitation is the whole difficulty here, and it has three consequences worth stating plainly.

  • Providers front their APIs with CDNs. The address behind a hostname can move between announcements, and the published range file may be broad enough that allowing it also allows a large slice of unrelated internet.
  • A broad allow is still a real control. Even a /16 stops a compromised container reaching an arbitrary exfiltration endpoint, and it stops the far more common accident: a dependency phoning home, or a debug script pointed at the wrong base URL. Do not let the imperfection argue you out of the policy.
  • If you need names, you need more than the core API. Cilium’s CiliumNetworkPolicy supports DNS-based rules, and an egress proxy gives you hostname and even path control at the cost of running a proxy. Both are outside the portable API, and both are the honest answer when “only this hostname” is a hard requirement.