A tool for collecting network interactions between pods via Hubble and automatically generating CiliumNetworkPolicy based on real traffic.
- Native gRPC connection to Hubble Relay (no
hubbleCLI required) - Automatic port-forward to
svc/hubble-relay— no manual setup needed - Automatic generation of egress and ingress rules based on real traffic
- Detection of internal and external IPs using Pod CIDR and Service CIDR
- Multi-namespace support:
-n prod -n staging - Smart label selection: prefers
app.kubernetes.io/name→app→ other stable labels - Merge-on-write: new rules merged into existing policy files, ports deduplicated
- Cluster-dedup (
--cluster-dedup): skips policies already matching live cluster state - Unhandled flow summary: skipped flows reported by reason at end of run
- Automatic filtering of Kubernetes/Cilium service labels (commit, pod-template-hash, etc.)
- Support for external sources (LoadBalancer, Ingress Controller)
- Automatic DNS rule addition
- Default port substitution for popular infrastructure components
- Policy validation before saving
Intel (x64):
chmod +x kubectl-hubble-collector-darwin-amd64
./kubectl-hubble-collector-darwin-amd64 -n production -o flows.jsonApple Silicon (M1/M2/M3):
chmod +x kubectl-hubble-collector-darwin-arm64
./kubectl-hubble-collector-darwin-arm64 -n production -o flows.jsonchmod +x kubectl-hubble-collector-linux-amd64
./kubectl-hubble-collector-linux-amd64 -n production -o flows.jsonkubectl-hubble-collector-windows-amd64.exe -n production -o flows.json# Install dependencies
go mod download
# Build for current platform
make build
# Or manually
go build -o kubectl-hubble-collector ./cmd/collector
# Cross-compile for Linux
make build-linux
# Or manually
GOOS=linux GOARCH=amd64 go build -o kubectl-hubble-collector-linux-amd64 ./cmd/collector
# Build for all platforms
make build-allkubectlconfigured with access to the cluster- Hubble enabled in the cluster (Cilium with Hubble Relay)
hubbleCLI — optional, used only as fallback if auto port-forward fails
# Collect flows for the last 60 seconds (auto port-forward to Hubble Relay)
./kubectl-hubble-collector -n production -o flows.json
# Collect flows for 30 minutes
./kubectl-hubble-collector -n production -o flows.json --duration 1800
# Continuous monitoring (Ctrl+C to stop)
./kubectl-hubble-collector -n production -o flows.json --follow
# Multiple namespaces
./kubectl-hubble-collector -n production -n staging -o flows.json
# All namespaces
./kubectl-hubble-collector -A -o flows.json
# Connect to Hubble Relay directly (skip auto port-forward)
./kubectl-hubble-collector -n production -o flows.json --server localhost:4245# Collect flows and generate policies
./kubectl-hubble-collector -n production -o flows.json \
--cilium true \
--pod-cidr "10.39.0.0/16" \
--service-cidr "10.40.0.0/16"
# For a specific application
./kubectl-hubble-collector -n production -o flows.json \
--from-label "app=backend-api" \
--cilium true
# Generate policies across multiple namespaces
./kubectl-hubble-collector -n production -n staging -o flows.json \
--cilium true
# Skip policies already matching live cluster state
./kubectl-hubble-collector -n production -o flows.json \
--cilium true \
--cluster-dedup# By source label
./kubectl-hubble-collector -n dev01 -o flows.json \
--from-label "app.kubernetes.io/name=notifications-push"
# By destination label
./kubectl-hubble-collector -n prod -o flows.json \
--to-label "app=postgres"
# Only dropped connections
./kubectl-hubble-collector -n prod -o dropped.json --verdict DROPPED
# Combined filters
./kubectl-hubble-collector -n dev01 -o flows.json \
--from-label "app=api" \
--to-label "app=database" \
--verdict FORWARDED| Option | Description | Default |
|---|---|---|
-n |
Namespace to monitor (repeatable: -n prod -n staging) |
required (or use -A) |
-A |
Observe all namespaces | false |
-o |
Output JSON file | required |
--server |
Hubble Relay gRPC address; omit for auto port-forward | auto |
--duration |
Seconds to collect flows | 60 |
--follow |
Continuous monitoring until Ctrl+C | false |
--from-label |
Filter by source pod label | none |
--to-label |
Filter by destination pod label | none |
--verdict |
Filter by verdict (FORWARDED, DROPPED, ERROR, AUDIT, REDIRECTED, TRACED) | none |
--cilium |
Generate CiliumNetworkPolicy files (true/false) | false |
--cilium-output-dir |
Directory for generated policy files | ./cilium-policies |
--cluster-dedup |
Skip policies whose spec already matches live cluster state | false |
--pod-cidr |
Cluster Pod CIDR | 10.39.0.0/16 |
--service-cidr |
Cluster Service CIDR | 10.40.0.0/16 |
--debug-flows |
Save raw flows to this file for debugging | none |
The tool connects to Hubble Relay via gRPC. Three modes, tried in order:
1. Explicit server (--server)
./kubectl-hubble-collector -n production -o flows.json --server localhost:4245Uses the specified address directly. Set this if you already have a port-forward running.
2. Auto port-forward (default)
./kubectl-hubble-collector -n production -o flows.jsonAutomatically runs kubectl port-forward -n kube-system svc/hubble-relay <random-port>:80, waits for it to be ready, then connects. Cleans up on exit. No manual setup required.
3. Hubble CLI fallback
If auto port-forward fails (e.g. hubble-relay is not present), the tool falls back to running hubble observe as a subprocess. Requires the hubble CLI to be installed.
--cluster-dedup is useful when running the tool repeatedly or from different machines where local policy files may not exist.
./kubectl-hubble-collector -n production -o flows.json \
--cilium true \
--cluster-dedupBefore writing each policy file, the tool fetches all live CiliumNetworkPolicy objects from the cluster. If the generated spec matches what is already deployed, the file is skipped:
Loading live CiliumNetworkPolicy objects from cluster...
Loaded 12 live policies from cluster
Skipped (matches cluster): production/backend-api
Skipped (matches cluster): production/postgres
Created policy: production/new-service-cnp.yaml (egress: 3, ingress: 1)
Created/updated 1 policy files in "./cilium-policies"
The --pod-cidr and --service-cidr parameters are critical for correct policy generation.
The tool must distinguish between:
- Pod IPs (internal pod IPs)
- Service IPs (ClusterIP services)
- External public IPs
Without specifying CIDRs, the tool uses default ranges:
- 10.39.0.0/16 (Pod CIDR)
- 10.40.0.0/16 (Service CIDR)
- 172.16.0.0/12 (RFC1918)
- 192.168.0.0/16 (RFC1918)
- 100.64.0.0/10 (Shared address space)
If your cluster uses different ranges, specify them explicitly:
./kubectl-hubble-collector -n production -o flows.json \
--cilium true \
--pod-cidr "10.39.0.0/16" \
--service-cidr "10.40.0.0/16"# Method 1: Via kube-controller-manager
kubectl -n kube-system get pod -l component=kube-controller-manager -o yaml | grep -E "cluster-cidr|service-cluster-ip-range"
# Method 2: Via cluster-info
kubectl cluster-info dump | grep -m 1 cluster-cidr
kubectl cluster-info dump | grep -m 1 service-cluster-ip-range
# Method 3: From configmap (if available)
kubectl -n kube-system get cm kubeadm-config -o yaml | grep -E "podSubnet|serviceSubnet"Without correct CIDRs, pod IPs are treated as external addresses, producing unstable toCIDR rules instead of toEndpoints:
# Wrong (without CIDR):
egress:
- toCIDR:
- 10.39.36.20/32 # Pod IP — breaks when pod restarts# Correct (with CIDR):
egress:
- toEndpoints:
- matchLabels:
app: backend-api # Stable across restarts| Scenario | Duration | Reason |
|---|---|---|
| Quick test | 60-300 sec | Fast iteration |
| Production policies | 300-600 sec | Good coverage without stale pods |
| Full coverage | 1800-3600 sec | All traffic patterns, risk of dead pods |
Hubble stores historical flows. If a pod was restarted during collection, its old IP appears in flows but no longer has a matching pod. The tool skips such flows with a warning and tracks them in the unhandled summary.
./kubectl-hubble-collector -n production -o flows.json \
--follow \
--cilium true \
--pod-cidr "10.39.0.0/16" \
--service-cidr "10.40.0.0/16"
# Stop with Ctrl+C# Step 1: Collect flows for 1 hour
./kubectl-hubble-collector -n production -o flows.json \
--duration 3600 \
--cilium true \
--pod-cidr "10.39.0.0/16" \
--service-cidr "10.40.0.0/16"
# Step 2: Review generated policies
ls -la ./cilium-policies/production/
cat ./cilium-policies/production/backend-api-cnp.yaml
# Step 3: Dry-run
kubectl apply -f ./cilium-policies/ --dry-run=server
# Step 4: Apply
kubectl apply -f ./cilium-policies/# Collect additional flows and merge into existing policy files
./kubectl-hubble-collector -n production -o flows.json \
--duration 1800 \
--cilium true
# New rules are merged into existing files — ports deduplicated automatically
# Use --cluster-dedup to skip policies already applied to the cluster
./kubectl-hubble-collector -n production -o flows.json \
--duration 1800 \
--cilium true \
--cluster-dedup# Collect all allowed traffic
./kubectl-hubble-collector -n production -o actual.json --duration 1800
# Find blocked connections
./kubectl-hubble-collector -n production -o blocked.json --verdict DROPPED
# Find what a specific service is being blocked from
./kubectl-hubble-collector -n prod -o api-blocked.json \
--from-label "app=api" --verdict DROPPED# Generate policies for multiple namespaces in one run
./kubectl-hubble-collector -n production -n staging -o flows.json \
--duration 600 \
--cilium true \
--pod-cidr "10.39.0.0/16" \
--service-cidr "10.40.0.0/16"# Step 1: Collect flows
./kubectl-hubble-collector -n production -o flows.json \
--duration 7200 --cilium true
# Step 2: Apply in test namespace
for policy in ./cilium-policies/production/*.yaml; do
sed 's/namespace: production/namespace: test/' "$policy" | kubectl apply -f -
done
# Step 3: Check for dropped flows in test
./kubectl-hubble-collector -n test -o validation.json \
--verdict DROPPED --duration 600
# Step 4: Apply in production
kubectl apply -f ./cilium-policies/{
"namespace": "production",
"collected_at": "2024-12-31T10:30:00Z",
"total_flows": 1234,
"filters": {
"from_label": "app=api",
"to_label": "",
"verdict": "FORWARDED"
},
"connections": [
{
"source": "backend-api-7d9f8b6c5-x9k2m",
"destination": "postgres-0:5432/TCP",
"flows_count": 245
}
]
}apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: backend-api
namespace: production
spec:
endpointSelector:
matchLabels:
app: backend-api
egress:
- toEndpoints:
- matchLabels:
app: postgres
toPorts:
- protocol: TCP
ports:
- port: "5432"
- toCIDR:
- "8.8.8.8/32"
toPorts:
- protocol: UDP
ports:
- port: "53"
- toEndpoints:
- matchLabels:
io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- protocol: UDP
ports:
- port: "53"
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- protocol: TCP
ports:
- port: "8080"
- fromCIDR:
- "203.0.113.5/32"
toPorts:
- protocol: TCP
ports:
- port: "8080"For matchLabels in endpoint selectors, stable labels are preferred in order:
app.kubernetes.io/nameapp.kubernetes.io/componentapp- All remaining non-system labels (fallback)
System labels are always excluded: io.cilium.*, io.kubernetes.pod.*, pod-template-hash, controller-revision-hash, commit, etc.
| Connection Type | IP Address | Egress Rule | Ingress Rule |
|---|---|---|---|
| Pod in same NS | 10.39.1.20 | toEndpoints + matchLabels |
fromEndpoints + matchLabels |
| Pod in other NS | 10.39.2.30 | toEndpoints + matchExpressions |
fromEndpoints + matchExpressions |
| External IP | 8.8.8.8 | toCIDR |
fromCIDR |
| Dead/unknown pod | 10.39.36.20 | skipped | skipped |
When a policy file already exists, incoming rules are merged rather than overwriting:
- Rules with matching selectors have their port lists merged and deduplicated
- Non-matching rules are appended
- Metadata (name, namespace) is preserved from the existing file
At the end of each run, a summary of skipped flows is printed:
Skipped flows summary:
empty_namespace: 12
nil_endpoint: 3
no_l4: 8
world_no_ip: 5
| Reason | Meaning |
|---|---|
nil_endpoint |
Flow has no source or destination endpoint data |
no_l4 |
Flow has no L4 (TCP/UDP) information |
empty_namespace |
Both source and destination namespace are empty |
world_no_ip |
External flow with no resolvable IP |
unknown_protocol |
Protocol not supported (e.g. ICMP) |
When Hubble cannot determine the port, known ports are substituted for popular components:
| Component | Port | Protocol |
|---|---|---|
| RabbitMQ | 5672 | TCP |
| RabbitMQ Management | 15672 | TCP |
| Redis | 6379 | TCP |
| Redis Sentinel | 26379 | TCP |
| PostgreSQL | 5432 | TCP |
| VictoriaMetrics (vmagent) | 8429 | TCP |
| VictoriaMetrics (vmsingle) | 8429 | TCP |
| VictoriaMetrics (vmselect) | 8481 | TCP |
| VictoriaMetrics (vminsert) | 8480 | TCP |
| VictoriaMetrics (vmstorage) | 8482 | TCP |
| Prometheus | 9090 | TCP |
| Alertmanager | 9093 | TCP |
| Grafana | 3000 | TCP |
| DNS (kube-dns, coredns) | 53 | UDP |
Detection is based on app, app.kubernetes.io/name, app.kubernetes.io/component, k8s-app labels.
# Validate before applying
kubectl apply -f ./cilium-policies/ --dry-run=server
# Apply
kubectl apply -f ./cilium-policies/
# Check status
kubectl get ciliumnetworkpolicies -n production
kubectl describe ciliumnetworkpolicy backend-api -n production
# Monitor dropped flows after applying
./kubectl-hubble-collector -n production -o dropped.json \
--verdict DROPPED --followAuto port-forward failed (...), using hubble CLI
Either hubble-relay is not running, or kubectl is not configured. Check:
kubectl get pods -n kube-system | grep hubble-relay
kubectl get svc -n kube-system hubble-relayIf hubble-relay is running but port-forward still fails, specify the address manually:
kubectl port-forward -n kube-system svc/hubble-relay 4245:80
./kubectl-hubble-collector -n production -o flows.json --server localhost:4245# Check namespace has traffic
kubectl get pods -n <namespace>
# Verify Hubble sees flows
hubble observe --namespace <namespace> --last 10
# Increase collection duration
./kubectl-hubble-collector -n prod -o flows.json --duration 300Skip pod 'some-pod': no labels
Add labels to pods in Deployment/StatefulSet, or use --from-label to focus on labeled workloads.
# Delete the policy
kubectl delete ciliumnetworkpolicy <name> -n <namespace>
# Re-collect with longer duration to capture all traffic patterns
./kubectl-hubble-collector -n production -o flows.json \
--duration 7200 --cilium true--duration 300 # Testing: 5 minutes
--duration 1800 # Production: 30 minutes
--duration 7200 # Full coverage: 2 hours (include peak load)- Run collection during representative traffic period
- Review generated policies before applying
- Apply with
--dry-run=serverfirst - Apply in a test namespace, monitor for dropped flows
- Apply in production
- Use
--cluster-dedupon subsequent runs to track only what changed