How It Works
The PandoCore admission webhook uses Kubernetes' MutatingAdmissionWebhook API to automatically inject the PandoCore sidecar into pods at creation time.
When a pod is created with the label pandocore.io/protect: "true", the webhook intercepts the request and adds:
- The
pando-sidecarcontainer with health probes, resource limits, and security context shareProcessNamespace: true(required for sidecar observation)- The
pando-sidecarservice account reference - License key from a Kubernetes Secret (not inline)
- Downward API environment variables (
POD_NAME,POD_NAMESPACE)
This is the same injection performed by inject-pandocore.sh, but automated cluster-wide.
Label Reference
| Label / Annotation | Type | Values | Description |
|---|---|---|---|
pandocore.io/protect |
Label | "true" |
Enables sidecar injection for pods with this label |
pandocore.io/mode |
Annotation | "monitor", "alert", "isolate", "enforce", "debug" |
Overrides the default operating mode for this pod. See Operating Modes. |
pandocore.io/profile |
Annotation | "http-server" |
Marks the workload as an HTTP server regardless of image name or CMD. Applies HTTP-tuned detection behavior and sensitivity. Use this when your framework (Django, FastAPI, Flask, Express, Rails, etc.) runs on a language base image or inside a shell-wrapped entrypoint. |
pandocore.io/audit-network |
Annotation | "true" or "false" |
Per-pod override for the cluster-wide networkAudit.enabled Helm value. When enabled, the sidecar baselines the set of remote IPs during learning and emits attestation evidence on connections to addresses outside the baseline + allowlist. Detects in-process exfil beacons and C2 callbacks that entropy drift alone cannot disambiguate. |
pandocore.io/network-allowlist |
Annotation | Comma-separated CIDRs | Per-pod override for networkAudit.allowlistCIDRs. Connections to addresses inside any allowlisted CIDR are never flagged. Example: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16". |
HTTP-server auto-detection
The webhook sets PANDO_HTTP_SERVER=true automatically from any of three signals — you rarely need to set the profile annotation by hand:
- Image-pattern match. Purpose-built web-server images:
nginx,caddy,apache,traefik,envoy,haproxy,lighttpd,openresty,httpd. - CMD/args basename match. Common Python, Node, and Ruby framework server entrypoints running on language base images (
python:*,node:*, etc.) are auto-tagged by the webhook. - Profile annotation.
pandocore.io/profile: http-serveron the pod template.
CMD/args basename matching looks at command[0]. If your Dockerfile or Helm chart uses sh -c "exec uvicorn main:app --host 0.0.0.0", the webhook sees sh, not uvicorn, and will not auto-tag. Add pandocore.io/profile: http-server to the pod template to cover this case.
Workloads that are not HTTP servers (Celery workers, ETL jobs, Jupyter notebooks, ML batch jobs) should be left untagged — the default profile is tuned for their behavior, and incorrectly marking them would mistune sensitivity.
Configuration
The webhook is configured via Helm values:
| Value | Default | Description |
|---|---|---|
licenseKey |
(required) | Your PandoCore license key |
defaultMode |
monitor |
Default operating mode for injected sidecars. One of monitor, alert, isolate, enforce. |
excludedNamespaces |
[kube-system, kube-public, kube-node-lease] |
Namespaces where injection is always skipped |
sidecar.image |
Latest sidecar from registry | Sidecar container image to inject |
replicas |
2 |
Webhook server replicas (2 recommended for HA) |
tls.certManager |
false |
Use cert-manager for TLS (otherwise self-signed) |
networkAudit.enabled |
false |
Cluster-wide default for the network connection audit (Phase H5.2). Per-pod override via the pandocore.io/audit-network annotation. |
networkAudit.allowlistCIDRs |
"10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8,fd00::/8" |
Comma-separated CIDRs that are never flagged as unexpected. Default covers RFC1918, loopback, and ULA. Per-pod override via the pandocore.io/network-allowlist annotation. |
Per-Namespace Setup
Each namespace with protected pods needs two resources:
# 1. ServiceAccount for the sidecar
kubectl create serviceaccount pando-sidecar -n your-namespace
# 2. RoleBinding to the cluster-wide sidecar ClusterRole
kubectl create rolebinding pando-sidecar \
--clusterrole=pando-sidecar \
--serviceaccount=your-namespace:pando-sidecar \
-n your-namespace
# 3. Copy the license Secret into the namespace
kubectl create secret generic pando-license \
--from-literal=license-key="YOUR_LICENSE_KEY" \
-n your-namespace
Upgrading
# Upgrade the webhook (picks up new sidecar image + webhook config)
helm upgrade pando-webhook \
oci://us-central1-docker.pkg.dev/pandocore-prod/charts/pando-webhook \
--set licenseKey="YOUR_LICENSE_KEY"
# Existing pods are NOT affected. To inject the new sidecar version,
# restart protected deployments:
kubectl rollout restart deployment/your-app -n your-namespace
Troubleshooting
Sidecar not being injected
- Verify the label:
kubectl get pod <name> --show-labels - Check the namespace is not excluded:
helm get values pando-webhook -n pando-system - Check webhook logs:
kubectl logs -n pando-system -l app.kubernetes.io/name=pando-webhook - Verify the webhook is registered:
kubectl get mutatingwebhookconfigurations
TLS certificate issues
- Check the TLS setup job completed:
kubectl get jobs -n pando-system - Verify the TLS secret exists:
kubectl get secret -n pando-system | grep tls - If using cert-manager, check the Certificate resource:
kubectl get certificates -n pando-system
Pod creation failing
The webhook is configured with failurePolicy: Ignore, so webhook failures will not block pod creation. If pods are failing for other reasons after injection, check:
- The license Secret exists in the pod's namespace
- The
pando-sidecarServiceAccount exists in the pod's namespace - The sidecar image is pullable (check image pull secrets)