Generate Kubernetes NetworkPolicies from observed traffic
A frontend pod cannot connect to an API pod. The Service and endpoints exist,
both pods are healthy, and the application reports only a timeout. A restrictive
Kubernetes NetworkPolicy is a likely cause, but manually identifying and
maintaining every required pod-to-pod rule is slow and error-prone.
Inspektor Gadget can connect diagnosis with remediation. First, trace_tcp
shows the TCP lifecycle: the frontend attempts to connect, but the API pod
never accepts the connection. Then, advise_networkpolicy separately observes
a successful, representative request and generates Kubernetes NetworkPolicy
suggestions from the traffic and workload labels it sees. You can review the
suggestion, merge the required rule into the existing policy, and verify the
complete connection lifecycle.
This guide generates standard Kubernetes NetworkPolicy resources. You can
generate Cilium CiliumNetworkPolicy resources instead by running
advise_networkpolicy with --policy-format cilium.
From blocked connection to minimum policy
The scenario separates diagnosis from policy generation. First, observe the blocked connection without changing the workload. Then allow a controlled, representative request in a safe environment so the advisor can describe the required labels, direction, protocol, and port. Review that output before updating the policy managed in source control.
What this scenario answers
Use this scenario when you need to answer:
- Is the application attempting to connect to the expected destination and port?
- Does the client emit a
connectevent without a corresponding server-sideacceptevent? - Which label selectors and ports describe the intended communication?
- How should an existing
NetworkPolicybe updated without broadly allowing traffic?
This guide uses standard Kubernetes networking.k8s.io/v1 policies. Your
cluster must use a network plugin that enforces NetworkPolicy, and Inspektor
Gadget must already be deployed.
Confirm the available parameters before starting:
kubectl gadget run trace_tcp -h
kubectl gadget run advise_networkpolicy -h
Create two pods and a Service
Create an isolated namespace with an nginx API pod, a Service, and a client pod:
kubectl create namespace network-policy-demo
kubectl -n network-policy-demo run api \
--image=nginx:1.27-alpine \
--labels=app=api
kubectl -n network-policy-demo expose pod api \
--name=api \
--port=80 \
--target-port=80
kubectl -n network-policy-demo run frontend \
--image=busybox:1.36 \
--labels=app=frontend \
--command -- sleep infinity
kubectl -n network-policy-demo wait \
--for=condition=Ready pod/api pod/frontend \
--timeout=90s
Save the Service address and confirm the request succeeds before introducing a policy:
API_IP=$(kubectl -n network-policy-demo get service api \
-o jsonpath='{.spec.clusterIP}')
kubectl -n network-policy-demo exec frontend -- \
wget -T 3 -qO- "http://${API_IP}"
The command should print the nginx welcome page.
Reproduce a blocked connection
Apply an ingress policy that selects the API pod but permits no incoming connections:
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-ingress
namespace: network-policy-demo
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress: []
EOF
Retrying the request now times out:
kubectl -n network-policy-demo exec frontend -- \
wget -T 3 -qO- "http://${API_IP}"
If the request still succeeds, the cluster network plugin is not enforcing this policy or another cluster-specific exception applies.
Trace the blocked TCP lifecycle
In one terminal, trace the complete TCP lifecycle across the demo namespace:
kubectl gadget run trace_tcp \
-n network-policy-demo \
--timeout 20 \
--fields k8s.podName,src,dst,type,error
While the trace is running, retry the request from another terminal:
API_IP=$(kubectl -n network-policy-demo get service api \
-o jsonpath='{.spec.clusterIP}')
kubectl -n network-policy-demo exec frontend -- \
wget -T 3 -qO- "http://${API_IP}"
A policy that silently drops the packets produces only the client-side connection event:
K8S.PODNAME SRC DST TYPE ERROR
frontend p/network-policy-demo/frontend:... s/network-policy-demo/api:80 connect
The important evidence is:
k8s.podNameidentifies the workload making the connection.dstconfirms that it is targeting the expected Service and port.type=connectshows that the application attempted to establish TCP.- No
acceptevent appears for the API pod during the same trace, and the application times out.
trace_tcp can report errors such as ECONNREFUSED, but a silently dropped SYN
does not necessarily produce ETIMEDOUT within a short trace. The application
can abandon its request before the kernel's TCP timeout expires. Use the
attempted destination, the application timeout, and the missing server-side
accept event together; use trace_tcpdrop if you also need to investigate
kernel-reported packet drops.
An empty error value on the client connect event does not by itself prove
that the request completed. Interpret the event as part of the lifecycle and
look for the matching server-side accept.
An immediate ECONNREFUSED means the destination was reachable but nothing
accepted the connection on that port. That points toward the Service, its
target port, or the server process rather than a silent policy drop.
Generate policy suggestions from observed traffic
Network policy generation is based on observed traffic, not application
intent. The required connection must therefore succeed while
advise_networkpolicy is running.
Use a staging environment or a controlled observation window. Do not remove a production isolation policy merely to collect traffic. For this disposable demo, start the advisor and write its final output to a file:
kubectl gadget run advise_networkpolicy \
-n network-policy-demo \
--timeout 20 \
> network-policy.generated.yaml
While it is running, use another terminal to remove the intentionally broken policy and make one representative request:
kubectl -n network-policy-demo delete networkpolicy api-ingress
API_IP=$(kubectl -n network-policy-demo get service api \
-o jsonpath='{.spec.clusterIP}')
kubectl -n network-policy-demo exec frontend -- \
wget -T 3 -qO- "http://${API_IP}" >/dev/null
When the advisor exits, inspect the generated resources:
cat network-policy.generated.yaml
The relevant API policy should contain an ingress rule equivalent to:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-network
namespace: network-policy-demo
spec:
podSelector:
matchLabels:
app: api
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 80
protocol: TCP
policyTypes:
- Ingress
- Egress
The generated file can contain policies for both pods and other traffic that occurred during the observation window. Treat it as a suggestion:
- Confirm that every observed connection is legitimate.
- Remove unrelated or one-time traffic.
- Check whether probes, metrics, DNS, control-plane traffic, and external dependencies were exercised during the capture.
- Prefer stable workload labels over generated pod names or IP addresses.
- Merge rules into policies already managed in source control instead of applying the entire generated file blindly.
Update the existing policy
Merge only the required ingress rule into the original api-ingress policy:
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-ingress
namespace: network-policy-demo
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 80
EOF
This keeps the API pod isolated while allowing only pods labeled
app=frontend in the same namespace to connect to TCP port 80.
For communication across namespaces, the generated rule also includes a
namespaceSelector. Review both selectors: a podSelector nested beside a
namespaceSelector matches pods satisfying both conditions, whereas separate
from entries are additive and can allow a much broader set of sources.
Verify the updated policy
Repeat the request:
API_IP=$(kubectl -n network-policy-demo get service api \
-o jsonpath='{.spec.clusterIP}')
kubectl -n network-policy-demo exec frontend -- \
wget -T 3 -qO- "http://${API_IP}"
The nginx page should be returned. A final TCP trace can confirm that the client connects and the API pod accepts the connection:
kubectl gadget run trace_tcp \
-n network-policy-demo \
--timeout 20 \
--fields k8s.podName,src,dst,type,error
Run the request again while the trace is active. A successful lifecycle looks similar to:
K8S.PODNAME SRC DST TYPE ERROR
api p/network-policy-demo/api:80 p/network-policy-demo/frontend:... accept
frontend p/network-policy-demo/frontend:... s/network-policy-demo/api:80 connect
frontend p/network-policy-demo/frontend:... s/network-policy-demo/api:80 close
api p/network-policy-demo/api:80 p/network-policy-demo/frontend:... close
The frontend connect now has a matching api accept, followed by close
events from both sides when the request finishes. That complete lifecycle
confirms that the reviewed policy permits the intended connection.
Limitations
advise_networkpolicy cannot infer communication that did not occur, decide
whether observed traffic was authorized, or guarantee that a short capture
covered every application path. It also ignores several non-identity labels,
including rollout and topology labels, when creating selectors.
Generated policies should therefore be reviewed, tested in a non-production environment, stored in source control, and rolled out with the same safeguards as manually authored security policy.
Remove the demo resources and generated file when finished:
kubectl delete namespace network-policy-demo
rm network-policy.generated.yaml