Overview
Network Policies in Kubernetes are like firewalls for your pods - they control which pods can communicate with each other. kguardian observes actual traffic patterns and generates policies that allow only the connections your application actually uses.Best Practice: Let your application run under normal load for at least 5-15 minutes before generating policies. This ensures you capture all typical communication patterns.
Basic Usage
Generate for a Single Pod
The simplest use case - generate a policy for one specific pod:1
kguardian discovers the pod
The CLI queries the broker for all traffic involving
my-app in the production namespace2
Analyzes traffic patterns
- Identifies all unique source IPs (for ingress rules)
- Identifies all destination IPs (for egress rules)
- Resolves IPs to pods/services
- Groups by port and protocol
3
Generates YAML
Creates
production-my-app-standard-networkpolicy.yaml in the ./policies directoryBatch Generation
All Pods in a Namespace
Generate policies for every pod in a namespace:All Pods Across All Namespaces
For cluster-wide policy generation:Cilium Network Policies
Cilium is an advanced CNI that provides enhanced network policies with L7 (HTTP, gRPC, Kafka) visibility and identity-based rules.Generate Cilium Policies
toCIDR/fromCIDR entries appear only for IPs that can’t be resolved to a pod or Service. Label keys in the generated YAML carry Cilium’s k8s: source prefix.
Peers that resolve to a host-network pod are rendered as toEntities/fromEntities: [host, remote-node] rather than an endpoint selector — see Host-network peers and targets.
Cross-namespace peers
A CiliumNetworkPolicy is namespaced, and so are the endpoint selectors inside it. From Cilium’s Kubernetes integration docs: “the policy only applies to pods within that namespace. It’s possible, however, to grant access to and from pods in other namespaces” — by naming the namespace as a label on the peer, as in the docs’ own example:fromEndpoints/toEndpoints are scoped to the policy’s own namespace, so a rule that names a peer in another namespace without that label matches nothing and the traffic is denied. kguardian adds k8s:io.kubernetes.pod.namespace: <peer namespace> to every peer selector whose namespace differs from the target’s (pods and Services alike); same-namespace peers keep bare labels. Only the peers carry it — the docs also state that “Kubernetes prohibits specifying the namespace in the endpointSelector, as it would violate the namespace isolation principle of Kubernetes.” A prod/web pod talking to downloads/sonarr, the monitoring/prometheus Service and receiving from media/maintainerr renders as:
namespaceSelector, so it is unchanged.
Cilium vs Kubernetes Policies
Dry Run and Applying
Dry Run (Default)
By default,gen networkpolicy runs in dry-run mode - it saves policies to files without applying them to the cluster:
Applying directly from the CLI is not implemented. Passing
--dry-run=false currently behaves the same as dry-run — policies are only saved to files — and the CLI logs a warning to that effect. Apply the generated files with kubectl apply -f.Recommended Workflow
- Generate policies in dry-run mode
- Review the generated YAML files
- Test in a staging environment
- Apply with kubectl after validation:
Advanced Options
Custom Output Directory
Save policies to a specific location:Print to Stdout Only
Skip file creation and just print the policy:Understanding Generated Policies
Policy Structure
Every generated policy includes:- podSelector: Identifies which pods this policy applies to
- policyTypes: Declares whether it has Ingress, Egress, or both
- ingress: List of allowed incoming connections
- egress: List of allowed outgoing connections
How Peers are Identified
kguardian resolves IPs to Kubernetes resources. The resolution happens when the Broker ingests the flow and is stored on the traffic row; the generators read that stored identity and only fall back to a by-IP lookup (guarded by the peer pod’s start time) for rows written before it existed. A peer that nothing matches under the guard is emitted as anipBlock / CIDR with a # unattributed peer <ip> at <time> comment, never as a selector. Details: How peers are attributed.
Pod-to-Pod Traffic
Pod-to-Pod Traffic
When traffic is between pods, kguardian generates:This allows traffic from any pod with
app=my-service in the production namespace.Service Traffic
Service Traffic
For traffic to a Kubernetes Service, kguardian uses the service’s selector:The selector is the Service’s If the port is not in the stored Cilium resolves a named egress port by name across the cluster, limited to the endpoints the rule selects; NetworkPolicy resolves it on the selected pods.For a Service backed by host-network pods the rule’s peer is an
spec.selector as stored, never a label derived from the Service’s name: DNS egress to kube-system/kube-dns renders as k8s-app: kube-dns. When the Broker never stored the Service’s spec, the selector is unknown and the peer is pinned as an ipBlock on the observed address instead (the Policy Builder adds the # unattributed peer comment above it).Ports are the backend’s targetPort, not the Service port. The Controller records egress from the client socket, which still holds the ClusterIP and the Service port: kube-proxy rewrites the destination to a backend pod afterwards. NetworkPolicy and Cilium match that rewritten destination, so a rule allowing the Service port would drop the traffic whenever port and targetPort differ. kguardian looks the observed port up in the Service’s spec.ports (matching port and protocol) and emits its targetPort: a number as is, a named targetPort (http) as the named port, and an omitted one as the port itself. A Service api with port: 80, targetPort: 8080 therefore renders as:spec.ports (or the spec has none), the observed port is kept and the rule says so:ipBlock (or the host/remote-node entities), which has no endpoints to resolve a name against, so a named targetPort is resolved to the number in the backends’ containers[].ports[] (matching name and protocol; a host-network container port is the host port). Backends that disagree give one port each; a name no backend declares keeps the observed port with the comment above. Ingress ports are the target pod’s own and are never mapped.Services without a selector
Services without a selector
default/kubernetes (the API server) and Services whose Endpoints an operator manages have no spec.selector, so there are no backend labels to select. NetworkPolicy is evaluated after the Service DNAT, so an ipBlock on the ClusterIP never matches either. kguardian keeps the Service’s name and says so above the rule:kubectl get endpoints kubernetes -n default before applying. The Cilium form emits toEntities: [kube-apiserver] for the API server, which follows those endpoints; any other selector-less Service gets a toCIDR carrying the same note.External Traffic
External Traffic
For traffic to external IPs (e.g., public APIs), kguardian generates CIDR blocks:
DNS Traffic
DNS Traffic
kguardian automatically detects DNS queries:
Host-network peers and targets
Pods withhostNetwork: true (node-exporter, kube-proxy, CNI agents, the kguardian controller) have no network namespace of their own: their pod IP is the node IP, and so is kubelet’s, etcd’s and the API server’s on control-plane nodes. That changes how a policy has to describe them.
A podSelector (or Cilium toEndpoints/fromEndpoints) matches on the identity the CNI assigns to a pod’s own network namespace. Host-network pods don’t have one. The Kubernetes NetworkPolicy docs leave the behaviour undefined but describe the common case: the network plugin “ignores hostNetwork pods when matching podSelector and namespaceSelector. Traffic to/from hostNetwork pods is treated the same as all other traffic to/from the node IP. (This is the most common implementation.)” A selector built from a host-network pod’s labels is syntactically valid and silently ineffective; the same page notes that “you can allow traffic from a hostNetwork Pod using an ipBlock rule”.
kguardian records hostNetwork on every pod, and the generators switch rendering when a peer IP resolves to a host-network pod, or to a Service whose live backing pods are host-network. For such a Service the Kubernetes form lists one ipBlock per backend node IP rather than the ClusterIP — NetworkPolicy is evaluated after service DNAT, so the ClusterIP is never the peer — with a single comment naming the Service and its nodes.
Ports are kept in both forms. Kubernetes output has one
ipBlock rule per node IP; Cilium output has one entities rule per direction and port set, however many nodes were observed. A peer whose host_network is unknown (broker predating this field) keeps the old selector rendering.
The two forms differ in scope, and the difference is not kguardian’s choice:
ipBlockis per node but partly redundant. Kubernetes lists, among what a rule can select, “IP blocks (exception: traffic to and from the node where a Pod is running is always allowed, regardless of the IP address of the Pod or the node)”. So theipBlockfor the pod’s own node is allowed by the spec anyway; the rule is only load-bearing for other nodes’ IPs — and whether a given CNI enforces egress to a remote node IP throughipBlockis implementation-specific. Verify on your CNI before relying on it.- Cilium entities are per port but cluster-wide.
hostandremote-nodetogether are every node in the cluster; the rule cannot be narrowed to the node(s) actually observed. That is the price of the only form Cilium honours for node IPs — per its Layer 3 docs, “CIDR rules do not apply to traffic where both sides of the connection are either managed by Cilium or use an IP belonging to a node in the cluster (including host networking pods)”. To pin a specific node, hand-write a node-based rule (toNodes/fromNodeson node labels, requires--enable-node-selector-labels).
Example: Prometheus scraping nodes
Prometheus inmonitoring scrapes node-exporter on :9100 on two nodes and reaches an unresolved ClusterIP on :5432. The Kubernetes policy gets one ipBlock rule per observed node IP, each preceded by a comment naming the host-network workload the IP resolved to. The rule covers every host process on that node — kubelet :10250 or etcd :2381 on the same node IP would appear as further ports on the same rule, since they are not pods and resolve to the same host-network record:
toCIDR here would be as ineffective as the selector it replaces. Note the scope: this one rule admits port 9100 on every node, not only worker-1 and worker-2. Host-network peers with the same port set share one entities rule, carrying one comment line per peer:
from: [ipBlock] and fromEntities: [host, remote-node]. These examples are the generator fixtures in test/fixtures/generators/networkpolicy/ that the CLI, advisor and UI generators are all tested against.
Cilium entity semantics
Verified against the Cilium 1.20 documentation (Entities based):host— “The host entity includes the local host. This also includes all containers running in host networking mode on the local host.”remote-node— “Any node in any of the connected clusters other than the local host. This also includes all containers running in host-networking mode on remote nodes.”
host alone would break the moment the pod is rescheduled.
remote-node relies on Cilium’s remote-node identity (enable-remote-node-identity): per the Cilium upgrade notes it “has been enabled by default since Cilium 1.7”, was deprecated in 1.15 and removed in 1.16, so it is always on from 1.16. On 1.7–1.15 the generated rule only works if that flag was left at its default.
Two defaults matter when you read the generated rule:
- Cilium allows traffic from the local host to a pod without any policy: “Kubernetes will automatically allow all communication from the local host of all local endpoints. You can run the agent with the option
--allow-localhost=policyto disable this behavior” (Access to/from local host). That exemption is for traffic from the local host only. The same page’sdev-to-hostexample needs an explicittoEntities: [host]egress rule, and remote nodes get no exemption in either direction — which is why the generated entities rule is still required. - An endpoint is unrestricted until a policy selects it; the first policy with an egress section puts it into default-deny for egress (Policy Enforcement Modes,
policyEnforcementMode). The generated policy is that first policy.
When the target itself is host-network
A NetworkPolicy’spodSelector cannot select a host-network pod any more than a peer selector can match one. If you generate a policy for such a workload, the output is emitted with a leading warning and should not be applied as-is:
CiliumNetworkPolicy endpointSelector in place of NetworkPolicy podSelector; kguardian does not generate the cluster-wide policy for you. Per the Cilium Host Policies page: “Host policies take the form of a CiliumClusterwideNetworkPolicy with a Node Selector instead of an Endpoint Selector.” They require the host firewall (--set hostFirewall.enabled=true), and “In each selected node, they apply only to the host namespace, including host-networking pods.” Use the observed ports from the generated policy as the starting point for that rule. On other CNIs, Kubernetes gives you nothing to select a host-network pod with — the behaviour is undefined by the API and, in the common implementation, its traffic is simply node traffic.
Traffic to node IPs was not recorded by controllers up to 1.11.0 (see Upgrading). If a policy for a node-scraping workload has no node-IP rules, regenerate it after the controller upgrade and a fresh observation window.
Common Scenarios
Scenario 1: Microservices Application
You have a 3-tier app:frontend → backend → database.
frontendpolicy allows ingress from ingress controller, egress tobackendbackendpolicy allows ingress fromfrontend, egress todatabasedatabasepolicy allows ingress frombackendonly
Scenario 2: Multi-Namespace Communication
Your app instaging talks to a shared database in data:
Scenario 3: Default Deny + Allowlist
Start with default-deny, then allowlist only observed traffic:Now your namespace runs default-deny — only explicitly observed communication is allowed.
Troubleshooting
No Traffic Data
Symptom: CLI says “No traffic data found for pod”, or the Policy Builder shows “No traffic observed, so this policy allows nothing” A workload with no observed traffic gets an explicit deny-all:policyTypes: [Ingress, Egress] with no rules (Cilium: enableDefaultDeny both true plus one empty rule - {} per direction, which the CiliumNetworkPolicy CRD requires), the same deny the CLI and advisor produce. The Policy Builder marks it with a warning that cannot be dismissed and labels the button “Save Deny-All Policy”. A workload whose traffic read failed is shown as “traffic read failed” in the picker rather than “0 conns”; that is not evidence it is idle.
Solutions:
- Ensure the pod has been running for at least 5 minutes
- Generate some traffic (hit HTTP endpoints, trigger background jobs, etc.)
- Check broker has data:
Generated Policy Too Permissive
Symptom: Policy allows more than expected (e.g., allows entire namespace) Cause: Pods may lack specific labels, so kguardian falls back to broader selectors. Solution:- Add specific labels to your pods
- Manually edit generated policies to tighten selectors
- Re-generate after labeling
Policy Breaks Communication
Symptom: After applying policy, some connections fail Cause: Incomplete observation period - not all traffic was captured Solution:- Check pod logs for connection errors
- Identify missing ingress/egress rules
- Manually add missing rules or extend observation period
Generate Seccomp Profiles
Restrict syscalls based on observed behavior
CLI Reference
Complete flag documentation
Policy Gallery
Worked examples for nginx, Postgres, Prometheus, Istio, and more