Enable Hubble on Cilium to get real-time eBPF-based flow visibility, deploy Hubble UI and Relay, and integrate flow metrics with Prometheus and Grafana for full Kubernetes network observability.
Prerequisites
- A running Kubernetes cluster with Cilium installed as the CNI
- kubectl and Helm 3 configured against the cluster
- Cluster admin access to install CRDs and DaemonSets
- An ingress controller with cert-manager for exposing Hubble UI
- Prometheus Operator or a standalone Prometheus instance for metrics scraping
What this solves
Cilium's eBPF dataplane already sees every packet that crosses your pods, but without Hubble you have no way to query that traffic. This tutorial enables Hubble on an existing Cilium installation, deploys Hubble Relay and Hubble UI, configures the CLI for live flow inspection, and wires flow metrics into Prometheus and Grafana.
By the end you will be able to see which pods talk to which services, which network policies drop traffic, and why, without tailing application logs.
Prerequisites and Cilium CNI verification
Confirm cluster and tooling
You need a running Kubernetes cluster (kubeadm, EKS, GKE, or self-managed) with Cilium as the CNI, kubectl configured against it, and Helm 3 installed on your workstation.
kubectl version --client
helm version
kubectl get nodes -o wideInstall the Cilium CLI
The Cilium CLI is used to check connectivity, install Hubble, and validate the dataplane. Install the latest release binary directly.
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gzCILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-amd64.tar.gz
sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin
rm cilium-linux-amd64.tar.gzVerify Cilium status and connectivity
Before enabling Hubble, confirm Cilium itself is healthy across every node. Any node reporting degraded status will produce incomplete flow data later.
cilium status --wait
kubectl -n kube-system get pods -l k8s-app=ciliumRun a full connectivity test if this is a new cluster. It deploys temporary test pods and validates pod-to-pod, pod-to-service, and cross-node traffic.
cilium connectivity test --test-namespace cilium-testInstalling and enabling Hubble in Cilium
Enable Hubble on the Cilium DaemonSet
Hubble ships as part of Cilium but is disabled by default. Enabling it turns on the local Hubble server inside each Cilium agent, which exposes flow data over a Unix socket.
cilium hubble enable --uiThis patches the Cilium ConfigMap, restarts the agent pods, and additionally deploys Hubble Relay and Hubble UI in one step. Wait for the rollout to finish.
kubectl -n kube-system rollout status daemonset/cilium
cilium status --waitConfirm Hubble is reporting flows
Check that the Hubble metrics server and flow API are active inside the agent pods.
kubectl -n kube-system exec -it ds/cilium -- cilium status | grep HubbleExpected output shows Hubble: Ok along with the relay address once the next steps are complete.
Deploying Hubble Relay and Hubble UI
Verify Relay and UI pods
Hubble Relay aggregates flow data from every agent into a single gRPC endpoint. Hubble UI is the web frontend that queries Relay. Both were deployed by the enable command above, so confirm they are running.
kubectl -n kube-system get pods -l k8s-app=hubble-relay
kubectl -n kube-system get pods -l k8s-app=hubble-uiAccess Hubble UI locally for testing
Use port forwarding to reach the UI before you expose it externally through Ingress.
cilium hubble uiThis opens a browser tab pointed at http://localhost:12000. If you prefer manual port forwarding instead of the CLI wrapper, use kubectl directly.
kubectl -n kube-system port-forward svc/hubble-ui 12000:80Configuring Hubble CLI for flow inspection
Install the Hubble CLI binary
The Hubble CLI queries Relay directly from your terminal, which is faster than the UI for scripting and troubleshooting.
HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt)
curl -L --fail --remote-name-all https://github.com/cilium/hubble/releases/download/${HUBBLE_VERSION}/hubble-linux-amd64.tar.gz
sudo tar xzvfC hubble-linux-amd64.tar.gz /usr/local/bin
rm hubble-linux-amd64.tar.gzPoint Hubble CLI at Relay
Forward Relay's gRPC port locally, then export it as the Hubble server address.
kubectl -n kube-system port-forward svc/hubble-relay 4245:80 &
export HUBBLE_SERVER=localhost:4245Query live flows
Observe flows in real time, filter by namespace, or watch only dropped packets to debug connectivity issues.
hubble observe --server $HUBBLE_SERVER --last 20
hubble observe --server $HUBBLE_SERVER --namespace production --follow
hubble observe --server $HUBBLE_SERVER --verdict DROPPED --followEach line shows source and destination identity, port, protocol, and the verdict (forwarded, dropped, or redirected), which is far more useful than raw packet captures for debugging Kubernetes network policies.
Enabling network policy visibility and observability metrics
Turn on policy verdict metrics
By default Hubble exposes basic flow metrics. Enable additional metrics for DNS, HTTP, and policy verdicts by editing the Cilium Helm values or patching the ConfigMap.
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,icmp,http,policy}" \
--set hubble.metrics.enableOpenMetrics=trueThis restarts the Cilium agents again, so run it during a maintenance window on production clusters.
kubectl -n kube-system rollout status daemonset/ciliumTest policy visibility with a deny rule
Apply a simple CiliumNetworkPolicy and confirm Hubble reports the resulting drops. This is the same mechanism used by Implement Kubernetes network policies for pod-to-pod security and traffic isolation, but Hubble gives you the visibility layer that policy alone does not.
cat <<'EOF' | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: deny-egress-test
namespace: production
spec:
endpointSelector:
matchLabels:
app: checkout
egress:
- toEndpoints:
- matchLabels:
app: catalog
EOFhubble observe --server $HUBBLE_SERVER --namespace production --verdict DROPPED --followExposing Hubble UI securely via Ingress
Create a dedicated namespace and basic auth secret
Never expose Hubble UI publicly without authentication. It reveals internal service topology and traffic patterns that should stay behind an authenticated proxy.
sudo apt install -y apache2-utils
htpasswd -c auth hubble-admin
kubectl -n kube-system create secret generic hubble-ui-basic-auth --from-file=authauth file only needs to be readable by your own user before it becomes a Kubernetes secret. Keep it at 600 permissions and delete it locally once the secret is created.chmod 600 auth
rm authCreate the Ingress resource
This assumes an NGINX ingress controller is already installed, as covered in Configure Kubernetes ingress controller with NGINX and SSL certificates using cert-manager.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hubble-ui
namespace: kube-system
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: hubble-ui-basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication required"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- hubble.example.com
secretName: hubble-ui-tls
rules:
- host: hubble.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hubble-ui
port:
number: 80kubectl apply -f /tmp/hubble-ui-ingress.yaml
kubectl -n kube-system get ingress hubble-uiRestrict access at the network layer too
Add a CiliumNetworkPolicy that only allows the ingress controller to reach Hubble UI, so even a leaked credential cannot be used from an arbitrary pod.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: restrict-hubble-ui-ingress
namespace: kube-system
spec:
endpointSelector:
matchLabels:
k8s-app: hubble-ui
ingress:
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: ingress-nginxkubectl apply -f /tmp/hubble-ui-policy.yamlIntegrating Hubble metrics with Prometheus and Grafana
Expose Prometheus scrape annotations
Hubble metrics are already exposed on port 9965 by default once enabled in an earlier step. Confirm the endpoint and annotate the service for scraping if you use annotation-based discovery instead of ServiceMonitors.
kubectl -n kube-system get svc hubble-metrics
curl -s http://localhost:9965/metrics | head -n 20Create a ServiceMonitor for Prometheus Operator
If your cluster uses the Prometheus Operator, as set up in Set up Kubernetes monitoring with Prometheus Operator and custom metrics, add a ServiceMonitor that targets the Hubble metrics service.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: hubble-metrics
namespace: kube-system
labels:
release: prometheus
spec:
selector:
matchLabels:
k8s-app: hubble
namespaceSelector:
matchNames:
- kube-system
endpoints:
- port: hubble-metrics
interval: 30skubectl apply -f /tmp/hubble-servicemonitor.yamlImport a Grafana dashboard for flow data
Cilium publishes an official Hubble Grafana dashboard. Import it via the Cilium community JSON model, then confirm the panels populate using the metrics enabled earlier.
curl -o hubble-dashboard.json https://raw.githubusercontent.com/cilium/cilium/main/examples/kubernetes/addons/prometheus/files/grafana/dashboards/hubble-dashboard.jsonImport this file through the Grafana UI under Dashboards, Import, and select your Prometheus data source. Key panels to watch are dropped packets by policy, DNS response codes, and top talkers by namespace.
Troubleshooting traffic flows and connectivity issues
Trace a specific pod-to-pod connection
Use Hubble's identity-aware filters to isolate traffic between two workloads without noise from the rest of the cluster.
hubble observe --server $HUBBLE_SERVER \
--from-pod production/checkout-7f9c8d \
--to-pod production/catalog-5b6d9f \
--last 50Check for DNS resolution failures
DNS problems often masquerade as generic connectivity failures. Filter for DNS flows specifically.
hubble observe --server $HUBBLE_SERVER --protocol dns --followInspect agent-level errors
If Hubble itself reports no data for a node, check the Cilium agent logs on that node directly.
kubectl -n kube-system logs ds/cilium -c cilium-agent --since=10m | grep -i hubbleVerify your setup
cilium status --wait
hubble observe --server $HUBBLE_SERVER --last 5
kubectl -n kube-system get pods -l k8s-app=hubble-relay
kubectl -n kube-system get pods -l k8s-app=hubble-ui
curl -sk https://hubble.example.com -u hubble-admin:yourpassword -o /dev/null -w "%{http_code}\n"Common issues
| Symptom | Cause | Fix |
|---|---|---|
| hubble observe returns no flows | CLI not pointed at Relay, or Relay not running | Confirm kubectl -n kube-system get pods -l k8s-app=hubble-relay is Running, then re-export HUBBLE_SERVER |
| Hubble UI shows empty service map | hubble.metrics.enabled missing flow types | Re-run the Helm upgrade with the full metrics list including flow and http |
| Ingress returns 502 for hubble.example.com | CiliumNetworkPolicy blocking ingress controller traffic | Verify the restrict-hubble-ui-ingress policy matches the actual ingress-nginx namespace label |
| Prometheus target for hubble-metrics is down | ServiceMonitor selector labels do not match the service | Run kubectl -n kube-system get svc hubble-metrics --show-labels and align the matchLabels |
| DROPPED verdicts on expected traffic | CiliumNetworkPolicy default-deny without an explicit allow rule | Add an explicit ingress or egress rule for the required label pair, then re-check with hubble observe |
| Agent restarts loop after enabling Hubble | Resource limits too low for the added Hubble server process | Increase CPU and memory requests on the cilium DaemonSet via Helm values |
Next steps
- Configure Kubernetes network policies for enhanced cluster security
- Implement Kubernetes network policies with Calico for microsegmentation
- Configure Cilium BGP peering with MetalLB integration for Kubernetes load balancing
- Implement Cilium Tetragon runtime security for Kubernetes with eBPF monitoring and threat detection
- Set up Kubernetes monitoring with Prometheus Operator and custom metrics
- Configure Hubble flow-based alerting with Prometheus Alertmanager
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Configure Kubernetes network monitoring with Hubble + Cilium
# ---------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[+] $*${NC}"; }
log_warn() { echo -e "${YELLOW}[!] $*${NC}"; }
log_error() { echo -e "${RED}[x] $*${NC}"; }
usage() {
cat <<EOF
Usage: $0 [-n cilium-test-namespace] [-t] [-p hubble-ui-port]
-n NAMESPACE Namespace used for cilium connectivity test (default: cilium-test)
-t Run full cilium connectivity test (slow, creates workloads)
-p PORT Local port to port-forward Hubble UI to (default: 12000)
-h Show this help message
EOF
exit 1
}
TEST_NAMESPACE="cilium-test"
RUN_CONN_TEST="false"
UI_PORT="12000"
while getopts ":n:tp:h" opt; do
case "$opt" in
n) TEST_NAMESPACE="$OPTARG" ;;
t) RUN_CONN_TEST="true" ;;
p) UI_PORT="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
TMP_ARCHIVE=""
cleanup() {
local exit_code=$?
if [ -n "$TMP_ARCHIVE" ] && [ -f "$TMP_ARCHIVE" ]; then
rm -f "$TMP_ARCHIVE"
fi
if [ $exit_code -ne 0 ]; then
log_error "Script failed at line $BASH_LINENO. Rolling back partial state where possible."
fi
exit $exit_code
}
trap cleanup ERR EXIT
TOTAL_STEPS=9
# ---------------------------------------------------------------------------
# [1/9] Root / sudo check
# ---------------------------------------------------------------------------
echo "[1/${TOTAL_STEPS}] Checking privileges..."
if [ "$(id -u)" -ne 0 ]; then
if ! command -v sudo >/dev/null 2>&1; then
log_error "This script requires root or sudo. Neither is available."
exit 1
fi
SUDO="sudo"
else
SUDO=""
fi
log_info "Privilege check passed."
# ---------------------------------------------------------------------------
# [2/9] Detect distro (Debian-based vs RHEL-based)
# ---------------------------------------------------------------------------
echo "[2/${TOTAL_STEPS}] Detecting distribution..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian) PKG_MGR="apt"; PKG_INSTALL="$SUDO apt install -y" ;;
almalinux|rocky|centos|rhel|ol|fedora) PKG_MGR="dnf"; PKG_INSTALL="$SUDO dnf install -y" ;;
amzn) PKG_MGR="yum"; PKG_INSTALL="$SUDO yum install -y" ;;
*) log_error "Unsupported distro: $ID"; exit 1 ;;
esac
else
log_error "/etc/os-release not found. Cannot detect distro."
exit 1
fi
log_info "Detected distro: $ID (using $PKG_MGR)"
# ---------------------------------------------------------------------------
# [3/9] Prerequisite tools: curl, tar, kubectl, helm
# ---------------------------------------------------------------------------
echo "[3/${TOTAL_STEPS}] Checking prerequisite tooling..."
if [ "$PKG_MGR" = "apt" ]; then
$SUDO apt update -y
fi
for tool in curl tar; do
if ! command -v "$tool" >/dev/null 2>&1; then
log_warn "$tool not found, installing..."
$PKG_INSTALL "$tool"
fi
done
if ! command -v kubectl >/dev/null 2>&1; then
log_error "kubectl not found. Install and configure kubectl against your cluster before running this script."
exit 1
fi
if ! command -v helm >/dev/null 2>&1; then
log_error "Helm 3 not found. Install Helm before running this script."
exit 1
fi
kubectl version --client >/dev/null
helm version >/dev/null
log_info "kubectl and helm are present."
if ! kubectl get nodes -o wide >/dev/null 2>&1; then
log_error "kubectl cannot reach the cluster. Check your kubeconfig."
exit 1
fi
log_info "Cluster reachable."
# ---------------------------------------------------------------------------
# [4/9] Install Cilium CLI (arch-aware, works across all supported distros)
# ---------------------------------------------------------------------------
echo "[4/${TOTAL_STEPS}] Installing Cilium CLI..."
if command -v cilium >/dev/null 2>&1; then
log_warn "cilium CLI already installed, skipping download."
else
ARCH="amd64"
case "$(uname -m)" in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) log_error "Unsupported architecture: $(uname -m)"; exit 1 ;;
esac
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
if [ -z "$CILIUM_CLI_VERSION" ]; then
log_error "Could not determine latest cilium-cli version."
exit 1
fi
WORKDIR=$(mktemp -d)
TMP_ARCHIVE="${WORKDIR}/cilium-linux-${ARCH}.tar.gz"
curl -L --fail --remote-name-all \
"https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${ARCH}.tar.gz" \
-o "$TMP_ARCHIVE"
$SUDO tar xzvfC "$TMP_ARCHIVE" /usr/local/bin
$SUDO chmod 755 /usr/local/bin/cilium
rm -f "$TMP_ARCHIVE"
TMP_ARCHIVE=""
log_info "Cilium CLI ${CILIUM_CLI_VERSION} installed."
fi
# ---------------------------------------------------------------------------
# [5/9] Verify Cilium is healthy before enabling Hubble
# ---------------------------------------------------------------------------
echo "[5/${TOTAL_STEPS}] Verifying Cilium CNI health..."
if ! kubectl -n kube-system get pods -l k8s-app=cilium >/dev/null 2>&1; then
log_error "No Cilium pods found in kube-system. Install Cilium CNI before running this script."
exit 1
fi
cilium status --wait
log_info "Cilium reports healthy status."
if [ "$RUN_CONN_TEST" = "true" ]; then
log_warn "Running full connectivity test in namespace '${TEST_NAMESPACE}' — this may take several minutes."
cilium connectivity test --test-namespace "$TEST_NAMESPACE"
fi
# ---------------------------------------------------------------------------
# [6/9] Enable Hubble with UI and Relay
# ---------------------------------------------------------------------------
echo "[6/${TOTAL_STEPS}] Enabling Hubble (relay + UI) on Cilium..."
cilium hubble enable --ui
log_info "Waiting for Cilium DaemonSet rollout..."
kubectl -n kube-system rollout status daemonset/cilium --timeout=300s
cilium status --wait
# ---------------------------------------------------------------------------
# [7/9] Verify Hubble flow reporting and Relay/UI pods
# ---------------------------------------------------------------------------
echo "[7/${TOTAL_STEPS}] Verifying Hubble is active..."
if ! kubectl -n kube-system exec -it ds/cilium -- cilium status | grep -q "Hubble:.*Ok"; then
log_error "Hubble does not report Ok status inside the Cilium agent."
exit 1
fi
log_info "Hubble server active inside Cilium agents."
log_info "Waiting for Hubble Relay pods..."
kubectl -n kube-system wait --for=condition=Ready pod -l k8s-app=hubble-relay --timeout=180s
kubectl -n kube-system get pods -l k8s-app=hubble-relay
log_info "Waiting for Hubble UI pods..."
kubectl -n kube-system wait --for=condition=Ready pod -l k8s-app=hubble-ui --timeout=180s
kubectl -n kube-system get pods -l k8s-app=hubble-ui
# ---------------------------------------------------------------------------
# [8/9] Configure Hubble CLI locally for flow inspection
# ---------------------------------------------------------------------------
echo "[8/${TOTAL_STEPS}] Configuring Hubble CLI..."
if ! command -v hubble >/dev/null 2>&1; then
ARCH="amd64"
case "$(uname -m)" in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
esac
HUBBLE_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/hubble/master/stable.txt)
WORKDIR=$(mktemp -d)
TMP_ARCHIVE="${WORKDIR}/hubble-linux-${ARCH}.tar.gz"
curl -L --fail --remote-name-all \
"https://github.com/cilium/hubble/releases/download/${HUBBLE_VERSION}/hubble-linux-${ARCH}.tar.gz" \
-o "$TMP_ARCHIVE"
$SUDO tar xzvfC "$TMP_ARCHIVE" /usr/local/bin
$SUDO chmod 755 /usr/local/bin/hubble
rm -f "$TMP_ARCHIVE"
TMP_ARCHIVE=""
log_info "Hubble CLI ${HUBBLE_VERSION} installed."
else
log_warn "Hubble CLI already installed, skipping."
fi
# Start a background port-forward to Hubble Relay so the local CLI can query flows.
PF_PID_FILE="/tmp/hubble-relay-portforward.pid"
if [ -f "$PF_PID_FILE" ] && kill -0 "$(cat "$PF_PID_FILE")" 2>/dev/null; then
log_warn "Existing hubble relay port-forward already running."
else
kubectl -n kube-system port-forward svc/hubble-relay 4245:80 >/tmp/hubble-relay-pf.log 2>&1 &
echo $! > "$
Review the script before running. Execute with: bash install.sh