Get startedDeploy on Kubernetes

Deploy on Kubernetes

Run the native agent as a per-node DaemonSet, put the JVM bridge in a sidecar, and avoid the two cluster-specific traps that fail silently.

What differs in a cluster

The language guidance in the SDK pages still applies. Two things change when the target is a pod, and both cause failures that produce no error message at all.

On a host or VMIn a cluster
One agent per machine, installed with makeOne agent per node, run as a DaemonSet from an image
executablePath is a path on that machineexecutablePath is the path inside the target container
Agent runs as the unprivileged liveprobe userSame, but the capability needs a file capability to take effect
Verified configurationKubernetes 1.36 (k3s) on an Ubuntu 24.04 x86-64 node, kernel 6.17, probing unmodified upstream container images under live load.

Check the node

shell
uname -m                                   # x86_64 only
test -r /sys/kernel/btf/vmlinux && echo "BTF ok"
mountpoint -q /sys/kernel/tracing ||
  sudo mount -t tracefs tracefs /sys/kernel/tracing

Node image: verified on Ubuntu, which on GKE means --image-type=UBUNTU_CONTAINERD. Container-Optimized OS ships BTF and may work now that the agent arrives as an image rather than being compiled onto the node, but that is unverified.

Build the agent image

There is no published image yet. Both binaries go in one image; the DaemonSet runs them as two containers so the privilege split survives. deploy/k8s/native-agent/Dockerfile in the repository builds it. Two lines in it are load-bearing:

dockerfile
# Runs the BPF audit first. Do not swap for a bare cargo build.
RUN make native-release

# Without this the agent can see every process and read none of them.
RUN setcap cap_sys_ptrace=ep /usr/local/bin/liveprobe-native-agent
Do not test x86-64 images on Apple SiliconBuilding them is fine. Running them locally is not: Docker executes amd64 images under Rosetta, and /proc/<pid>/exe then resolves to the translator instead of your service, so every discovery result is meaningless.

Check the kernel accepts the program

The eBPF program must pass your kernel’s verifier. That is a property of the kernel, not of your manifests, and no configuration works around a rejection. The loader loads the program at startup, so running it is the check:

shell
# k3s
sudo k3s ctr run --rm --privileged "$IMAGE" preflight \
  /usr/local/bin/liveprobe-bpf-loader /tmp/preflight.sock 0 0 /bin/true

# containerd generally
sudo ctr -n k8s.io run --rm --privileged "$IMAGE" preflight \
  /usr/local/bin/liveprobe-bpf-loader /tmp/preflight.sock 0 0 /bin/true
Use the Kubernetes image namespacePlain sudo ctr run uses containerd’s default namespace, where the image is not present, and fails in a way that looks nothing like a verifier problem.

If it stays running, the verifier accepted the program — press Ctrl-C. A rejection exits at once with an instruction dump ending in failed to load: -EACCES. /bin/true is a placeholder allowlist entry; nothing is attached.

Simpler still: deploy step 4 and read kubectl logs -n liveprobe ds/liveprobe-native-agent -c loader. The loader loads the program before anything else, so a rejection is the first thing it prints.

Create the credential

shell
curl --fail --silent --show-error \
  -H "Authorization: Bearer $LIVEPROBE_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"agentId":"node-agent","allowedServiceIds":["user-service"]}' \
  "$BROKER_URL/v1/native-credentials"

kubectl create namespace liveprobe
kubectl create secret generic liveprobe-native-credential -n liveprobe \
  --from-literal=credential='lp_native_...'

Confirm pods can reach the broker before blaming the agent — pod egress often differs from your laptop’s:

shell
kubectl run egress --rm -i --restart=Never --image=curlimages/curl -- \
  -sS -o /dev/null -w '%{http_code}\n' "$BROKER_URL/healthz"

Deploy the DaemonSet

Start from deploy/k8s/native-agent/daemonset.yaml. Three parts of it matter.

hostPID: true — procfs is per-PID-namespace, so this is what lets the pod’s own /proc list every process on the node. Without it the agent discovers nothing. No separate proc-root mount is needed.

The capability pairing that fails silently
yaml
securityContext:
  runAsUser: 10001
  runAsGroup: 10001          # must match the gid passed to the loader
  allowPrivilegeEscalation: true
  capabilities:
    add: ["SYS_PTRACE"]
    drop: ["ALL"]

Kubernetes has no ambient-capability support, so for a non-root user capabilities.add fills only the bounding set — CapPrm and CapEff stay empty and the capability is never held. The setcap in the image is what grants it, and allowPrivilegeEscalation: true is required or NO_NEW_PRIVS blocks the transition.

This does not make the agent root. The bounding set is still only SYS_PTRACE, so that is the one capability it can ever hold, and it still cannot load BPF.

Check the agent process, not a shell you exec into — the file capability applies to that binary alone:

shell
pid=$(pgrep -f 'liveprobe-native-agent /etc')
sudo grep -E '^Cap(Prm|Eff)' /proc/$pid/status
# CapEff: 0000000000080000   <- CAP_SYS_PTRACE. Zeros mean setcap or the flag is missing.

Paths are in-container. Both the config and the loader allowlist name the executable as the target process sees it, not as it appears on the node. If you are used to the host install, this is the likeliest thing to get wrong.

yaml
# ConfigMap
{ "serviceId": "user-service", "language": "cpp",
  "executablePath": "/usr/local/bin/UserService" }

# Loader args — the attach allowlist. It attaches to nothing else.
args: ["/run/liveprobe/loader.sock", "10001", "10001",
       "/usr/local/bin/UserService"]

To find the right value, from the node:

shell
sudo readlink /proc/<pid>/exe

Node, Python, and the JVM

Node and Python need nothing cluster-specific: install the package and supply BROKER_URL, LIVEPROBE_API_KEY and GIT_COMMIT from a Secret and your image build.

The JVM bridge belongs in the same pod as a sidecar. Containers in a pod share a network namespace, so JDWP stays on 127.0.0.1:5005 and is never exposed — exactly the property the JVM page asks for.

yaml
containers:
  - name: inventory
    args: ["-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:5005",
           "-jar", "/app/application.jar"]
  - name: liveprobe-bridge
    args: ["--service", "inventory-service", "--attach", "127.0.0.1:5005",
           "--broker", "$(BROKER_URL)", "--commit", "$(GIT_COMMIT)"]
Never expose JDWP publiclyDo not publish 5005 through a Service, and do not run the target pod with hostNetwork.

Troubleshooting

SymptomCause
Agent runs, logs nothing, no services appearCapEff is zero. The setcap and allowPrivilegeEscalation pairing. The most common failure, and it prints no error.
Probe reports armed but never returns dataCheck get_safety_overview. An evidence block with rejectedBatches above zero means captured evidence is being discarded, not that the line is cold.
Loader exits with a long instruction dumpThe kernel refused the BPF program. Not configuration — see the preflight above.
local-policy-denied: executable is not allowlistedThe loader allowlist has a node path where it needs the in-container path.
Service never appears, agent otherwise healthyNo running process matches executablePath, or hostPID is missing.
source-file-not-foundThe file was found; that line has no code. Optimizing compilers routinely attribute a statement to a neighbouring line. Try adjacent lines.
Permission denied on the loader socketrunAsGroup does not match the gid passed to the loader, which chowns the socket root:<gid> mode 0660.
Native services show no commitShaExpected. Native agents do not report a deployed commit, unlike the in-process SDKs.