Free lesson · Forward Deployed GenAI Engineering

Enforce service isolation with K8s NetworkPolicy

You build a NetworkPolicyGenerator that produces namespace-scoped egress/ingress rules for LLM proxy access, inter-service traffic, and external API restrictions.

Course: AI Solution Delivery · Chapter 6 · Deploying in Customer Environments

Free to read — no subscription required.

Introduction

When you deploy an AI service into a customer Kubernetes cluster, pods are reachable by every other workload in the namespace by default — a posture no security-conscious customer will accept. Manually authoring NetworkPolicy YAML for each deployment is tedious, error-prone, and hard to keep consistent as your service evolves across environments. This lesson teaches you how to build a NetworkPolicyGenerator that produces default-deny, ingress, and egress policies from a structured specification, so you can automate secure network segmentation as part of every AI service deployment without writing raw YAML by hand.

Key Terminology

  • NetworkPolicy — A namespace-scoped Kubernetes resource that selects pods via label selectors and defines which ingress and egress traffic is explicitly permitted; once any policy selects a pod, the CNI plugin drops all traffic not covered by an allow rule.
  • Default-deny policy — A NetworkPolicy with a blank podSelector: {} and both Ingress and Egress listed in policyTypes, which causes the CNI plugin to silently drop all traffic to every pod in the namespace until an explicit allow rule matches.
  • NetworkPolicySpec — The Pydantic model in this lesson that captures the generator's full security intent: namespace, service_labels for pod selection, and typed lists of EgressRule and IngressRule objects describing what traffic is explicitly permitted.
  • NetworkPolicyGenerator — The class that consumes a NetworkPolicySpec and emits Kubernetes manifest dictionaries — a default-deny policy first, then egress and ingress allow-list policies — eliminating hand-authored YAML while keeping output internally consistent across environments.
  • CNI plugin — The Container Network Interface implementation (Calico, Cilium, or equivalent) running in the customer cluster that reads NetworkPolicy objects and enforces traffic rules at the packet level, silently dropping packets that match no allow rule.
  • EgressRule — A structured representation of one permitted outbound traffic path, specifying a destination via to_cidr, to_namespace, or to_pod_selector plus allowed ports; the generator translates each EgressRule into the corresponding to entry in the Kubernetes manifest.

Concepts

Loading diagram...

Why Default-Deny Is the Right Starting Posture

Kubernetes gives every pod in a namespace a flat network by default: any pod can reach any other pod on any port, with no configuration required. For a customer cluster running sensitive workloads alongside your AI service, that permissive default is a non-starter. The correct approach is the opposite — block everything first, then carve out exactly what the service legitimately needs.

A NetworkPolicy achieves this in two steps: it selects a set of pods via a label selector, then defines the traffic those pods are allowed to send and receive. The critical behavior to internalize is that the moment any NetworkPolicy selects a pod, the CNI plugin enforces the policy's allow list and silently drops everything else. A policy with a blank podSelector: {} selects every pod in the namespace and, when policyTypes includes both Ingress and Egress with no explicit rules, creates a namespace-wide block. Subsequent allow-list policies layer on top, opening only the ports and peers the service actually requires.

The Generator Pattern: Structured Spec Over Raw YAML

Writing NetworkPolicy YAML by hand across multiple customer environments leads to drift — a port missed in staging, a namespace selector wrong in production. The generator pattern separates what traffic is needed (the NetworkPolicySpec) from how to express it as a Kubernetes manifest (the NetworkPolicyGenerator).

NetworkPolicySpec is a Pydantic model that encodes complete security intent: the target namespace, the pod label selector via service_labels, and typed lists of EgressRule and IngressRule objects. Each rule carries a human-readable description alongside its machine-consumable destination and port fields — making the spec both auditable by a security reviewer and programmatically translatable into manifests. When enable_default_deny is True, the generator emits the default-deny manifest first and the allow-list manifests after, ensuring the output is always a coherent, layered policy set (see Code Walkthrough).

Zero-Trust Egress for AI Service Deployments

AI inference services carry a specific egress risk that generic microservices do not: they may attempt to reach external LLM provider APIs directly, bypassing the cluster-internal proxy and its associated rate limiting, cost controls, and audit logging.

The generator addresses this structurally. An EgressRule targeting to_namespace="llm-proxy" and to_pod_selector={"app": "llm-proxy"} on the designated port opens exactly one outbound path. The default-deny policy blocks everything else at the packet level — no application-layer firewall rule, no SDK flag, no environment variable can substitute for this. Even a misconfigured or compromised pod attempting a direct call to an external provider endpoint will have its packets dropped by the CNI plugin before they leave the cluster network. This is zero-trust egress: the network enforces the boundary, not the application code.

Code Walkthrough

Now that you understand how a NetworkPolicy selects pods via label selectors and enforces default-deny once any policy applies to a pod, let's build a generator that automates the three policy categories your AI service deployment needs.

The NetworkPolicySpec Pydantic model captures everything the generator requires: the target namespace, service_labels for pod selection, and lists of EgressRule and IngressRule objects that describe what traffic is explicitly permitted. All other traffic is blocked once the default-deny policy is in place.

Code snippetpython
1from pydantic import BaseModel 2from typing import List, Optional 3 4class EgressRule(BaseModel): 5 description: str 6 to_cidr: Optional[str] = None 7 to_namespace: Optional[str] = None 8 to_pod_selector: Optional[dict] = None 9 ports: List[dict] # e.g. [{"port": 443, "protocol": "TCP"}] 10 11class IngressRule(BaseModel): 12 description: str 13 from_namespace: Optional[str] = None 14 from_pod_selector: Optional[dict] = None 15 from_cidr: Optional[str] = None 16 ports: List[dict] 17 18class NetworkPolicySpec(BaseModel): 19 namespace: str 20 service_labels: dict 21 egress_rules: List[EgressRule] 22 ingress_rules: List[IngressRule] 23 enable_default_deny: bool = True

NetworkPolicyGenerator uses this spec to produce three manifest types. The generate_default_deny method returns a policy with podSelector: {} and both Ingress and Egress in policyTypes — this blank-slate policy causes the CNI plugin to drop all traffic to every pod in the namespace until an explicit allow rule matches. The generate_egress_policy method iterates over egress_rules, translating each EgressRule into a Kubernetes to entry using ipBlock, namespaceSelector, or both, attaching the permitted ports.

A key egress rule for every AI service is access to the cluster-internal LLM proxy. In this platform architecture, inference pods must reach the proxy for model calls, but should never contact external provider APIs directly:

Code snippetpython
1# EgressRule, IngressRule, NetworkPolicySpec defined above 2 3llm_proxy_egress = EgressRule( 4 description="Allow egress to LLM proxy service", 5 to_namespace="llm-proxy", 6 to_pod_selector={"app": "llm-proxy"}, 7 ports=[{"port": 8443, "protocol": "TCP"}], 8) 9 10spec = NetworkPolicySpec( 11 namespace="ai-workloads", 12 service_labels={"app": "inference-service"}, 13 egress_rules=[llm_proxy_egress], 14 ingress_rules=[], 15 enable_default_deny=True, 16)

When enable_default_deny is True, the generator produces the default-deny manifest first and then the egress allow-list manifest. Together they enforce zero-trust networking: inference pods reach the proxy on port 8443, and all other outbound traffic — including direct calls to external LLM provider endpoints — is silently dropped at the packet level by the CNI plugin.

Confirm that when you instantiate a NetworkPolicySpec with enable_default_deny=True and one EgressRule, the generator produces exactly two manifest dictionaries: a default-deny policy with podSelector: {} and an egress policy whose podSelector matches your service_labels.

Do's and Don'ts

Having walked through the material above, the following Do's and Don'ts distill it into practice.

Do's

  1. Do set enable_default_deny=True on every NetworkPolicySpec — omitting it means NetworkPolicyGenerator skips the blank-podSelector deny manifest entirely, leaving all pods in the namespace reachable by every workload until a separate deny policy is manually applied.
  2. Do scope each EgressRule with the narrowest combination of to_namespace, to_pod_selector, and ports — the LLM proxy rule, for example, should specify namespace llm-proxy, selector {"app": "llm-proxy"}, and port 8443 only, so the CNI plugin permits that single path while silently dropping all other outbound traffic including direct calls to external provider APIs.
  3. Do verify that NetworkPolicyGenerator produces exactly two manifest dictionaries when enable_default_deny=True and at least one EgressRule is present — the first must carry podSelector: {} with both Ingress and Egress in policyTypes, and the second must carry podSelector matching service_labels; applying only one of them leaves the zero-trust posture incomplete.

Don'ts

  1. Don't author raw NetworkPolicy YAML per deployment — hand-writing per-service YAML bypasses the NetworkPolicySpec-driven pipeline, making it easy to omit the default-deny policy or misalign podSelector labels across environments as the service evolves.
  2. Don't add an EgressRule with a broad to_cidr or open ports list to "unblock" model calls — inference pods must route model traffic through the cluster-internal LLM proxy on port 8443; adding a wide-open egress rule re-opens the direct external-provider paths that the default-deny policy exists to close.
  3. Don't conflate the default-deny manifest's podSelector: {} with the egress policy's podSelector — the blank selector in generate_default_deny is intentional and applies the policy to every pod in the namespace; replacing it with service_labels silently leaves non-matching pods unrestricted.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the Forward Deployed GenAI Engineering subscription.

From · cancel anytime · Already a subscriber? Sign in →

More free lessons in AI Solution Delivery

All free lessons in Forward Deployed GenAI Engineering