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 VM | In a cluster |
|---|---|
| One agent per machine, installed with make | One agent per node, run as a DaemonSet from an image |
| executablePath is a path on that machine | executablePath is the path inside the target container |
| Agent runs as the unprivileged liveprobe user | Same, but the capability needs a file capability to take effect |
Check the node
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/tracingNode 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:
# 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/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:
# 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/truesudo 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
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:
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.
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:
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.
# 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:
sudo readlink /proc/<pid>/exeNode, 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.
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)"]hostNetwork.Troubleshooting
| Symptom | Cause |
|---|---|
| Agent runs, logs nothing, no services appear | CapEff is zero. The setcap and allowPrivilegeEscalation pairing. The most common failure, and it prints no error. |
| Probe reports armed but never returns data | Check 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 dump | The kernel refused the BPF program. Not configuration — see the preflight above. |
local-policy-denied: executable is not allowlisted | The loader allowlist has a node path where it needs the in-container path. |
| Service never appears, agent otherwise healthy | No running process matches executablePath, or hostPID is missing. |
source-file-not-found | The 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 socket | runAsGroup does not match the gid passed to the loader, which chowns the socket root:<gid> mode 0660. |
| Native services show no commitSha | Expected. Native agents do not report a deployed commit, unlike the in-process SDKs. |