Skip to main content
Learn how to generate Kubernetes and Cilium Network Policies from observed runtime traffic.

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 namespace
2

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 directory
Generated policy example:

Batch Generation

All Pods in a Namespace

Generate policies for every pod in a namespace:
This will create one policy file per pod in the namespace.

All Pods Across All Namespaces

For cluster-wide policy generation:
This can generate hundreds of files in large clusters. Use with caution and review before applying.

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

Cilium policy example:
Each peer gets a single endpoint selector built from that peer’s labels: the Service’s selector when the IP resolves to a Service (as with kube-dns above), the pod’s labels otherwise. 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:
Bare labels in 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:
The Kubernetes NetworkPolicy form already carried the namespace in a namespaceSelector, so it is unchanged.

Cilium vs Kubernetes Policies

If you’re running Cilium CNI, use --type cilium for better security and features. Otherwise, stick with --type kubernetes (the default).

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.
  1. Generate policies in dry-run mode
  2. Review the generated YAML files
  3. Test in a staging environment
  4. Apply with kubectl after validation:
Use with caution! Applying network policies can break communication if the observed traffic was incomplete. Always test in non-production first.

Advanced Options

Custom Output Directory

Save policies to a specific location:
Skip file creation and just print the policy:
Useful for piping to other tools:

Understanding Generated Policies

Policy Structure

Every generated policy includes:
  1. podSelector: Identifies which pods this policy applies to
  2. policyTypes: Declares whether it has Ingress, Egress, or both
  3. ingress: List of allowed incoming connections
  4. 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 an ipBlock / CIDR with a # unattributed peer <ip> at <time> comment, never as a selector. Details: How peers are attributed.
When traffic is between pods, kguardian generates:
This allows traffic from any pod with app=my-service in the production namespace.
For traffic to a Kubernetes Service, kguardian uses the service’s selector:
The selector is the Service’s 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:
If the port is not in the stored spec.ports (or the spec has none), the observed port is kept and the rule says so:
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 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.
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:
Replace the ipBlock with the addresses from 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.
For traffic to external IPs (e.g., public APIs), kguardian generates CIDR blocks:
kguardian automatically detects DNS queries:

Host-network peers and targets

Pods with hostNetwork: 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:
  • ipBlock is 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 the ipBlock for 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 through ipBlock is implementation-specific. Verify on your CNI before relying on it.
  • Cilium entities are per port but cluster-wide. host and remote-node together 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/fromNodes on node labels, requires --enable-node-selector-labels).

Example: Prometheus scraping nodes

Prometheus in monitoring 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:
The Cilium form uses entities instead of CIDRs, because Cilium does not evaluate CIDR rules against node IPs (quoted above) — a 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:
Ingress from a host-network peer (a host-network ingress controller, or kubelet probing a pod) renders the same way: 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.”
kguardian emits both because the scrape target may be on the same node as the workload or on any other node; 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=policy to disable this behavior” (Access to/from local host). That exemption is for traffic from the local host only. The same page’s dev-to-host example needs an explicit toEntities: [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’s podSelector 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:
The Cilium output carries the same header with 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.
Result:
  • frontend policy allows ingress from ingress controller, egress to backend
  • backend policy allows ingress from frontend, egress to database
  • database policy allows ingress from backend only

Scenario 2: Multi-Namespace Communication

Your app in staging talks to a shared database in data:
Generated egress rule includes:

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:
  1. Ensure the pod has been running for at least 5 minutes
  2. Generate some traffic (hit HTTP endpoints, trigger background jobs, etc.)
  3. 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:
  1. Add specific labels to your pods
  2. Manually edit generated policies to tighten selectors
  3. Re-generate after labeling

Policy Breaks Communication

Symptom: After applying policy, some connections fail Cause: Incomplete observation period - not all traffic was captured Solution:
  1. Check pod logs for connection errors
  2. Identify missing ingress/egress rules
  3. 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