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
NetworkPolicywith a blankpodSelector: {}and bothIngressandEgresslisted inpolicyTypes, 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_labelsfor pod selection, and typed lists ofEgressRuleandIngressRuleobjects describing what traffic is explicitly permitted. - NetworkPolicyGenerator — The class that consumes a
NetworkPolicySpecand 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
NetworkPolicyobjects 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, orto_pod_selectorplus allowedports; the generator translates eachEgressRuleinto the correspondingtoentry in the Kubernetes manifest.
Concepts
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
- ✓Do set
enable_default_deny=Trueon everyNetworkPolicySpec— omitting it meansNetworkPolicyGeneratorskips the blank-podSelector deny manifest entirely, leaving all pods in the namespace reachable by every workload until a separate deny policy is manually applied. - ✓Do scope each
EgressRulewith the narrowest combination ofto_namespace,to_pod_selector, andports— the LLM proxy rule, for example, should specify namespacellm-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. - ✓Do verify that
NetworkPolicyGeneratorproduces exactly two manifest dictionaries whenenable_default_deny=Trueand at least oneEgressRuleis present — the first must carrypodSelector: {}with bothIngressandEgressinpolicyTypes, and the second must carrypodSelectormatchingservice_labels; applying only one of them leaves the zero-trust posture incomplete.
Don'ts
- ✗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 misalignpodSelectorlabels across environments as the service evolves. - ✗Don't add an
EgressRulewith a broadto_cidror 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. - ✗Don't conflate the default-deny manifest's
podSelector: {}with the egress policy'spodSelector— the blank selector ingenerate_default_denyis intentional and applies the policy to every pod in the namespace; replacing it withservice_labelssilently 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
- Ch 4Package prototypes with Dockerfiles, Helm charts, and K8s manifests
- Ch 5Detect and redact PII with Presidio and LlamaGuard 4
- Ch 6Generate K8s manifests from customer-parameterized Jinja2 templates
- Ch 6Manage K8s secrets with rotation and init-container injection
- Ch 6Enforce service isolation with K8s NetworkPolicyYou are here
- Ch 6Log compliance events as OTEL traces with structured attributes
- Ch 7Provision isolated K8s demo environments with TTL teardown