Quickstart
This is the shortest path from a running container to a hardened image. It follows the Kubernetes route; a standalone single-host variant is at the end.
Before you start
Section titled “Before you start”- Your nodes meet the Requirements — NRI reachable, cgroup v2, kernel BTF, and a cluster that allows
privileged/hostPIDDaemonSet pods - The
hardenbinary is installed locally skopeo(orcrane) is available for importing the result
0. Run the discovery probe
Section titled “0. Run the discovery probe”Before installing anything, confirm the node can run the sensor. If NRI is unreachable the sensor pod will exit and go CrashLoopBackOff rather than install cleanly — check this first instead of finding out from a crashing pod:
./hack/discovery-probe.shExit 0 means proceed; exit 1 means don’t install yet (follow the remediation it prints); exit 2 means the probe itself couldn’t run. See Requirements for the full breakdown of what it checks.
1. Install the sensor DaemonSet
Section titled “1. Install the sensor DaemonSet”helm install tracepod ./helm/tracepod \ --namespace tracepod \ --create-namespace2. Exercise your workload
Section titled “2. Exercise your workload”The sensor profiles containers while they run. Send real or synthetic traffic to your workload — hit every endpoint and content type you care about. The profile is written when the container stops (for example, when you scale the deployment down or roll a pod), to /var/lib/tracepod/profiles/<container-id>/files.json on the node.
The longer and more representative the run, the higher your confidence score. The recommended minimum profiling window is 10 minutes.
3. Retrieve the manifest
Section titled “3. Retrieve the manifest”kubectl exec -n tracepod daemonset/tracepod-sensor -- \ cat /profiles/<container-id>/files.json > manifest.jsonMap container IDs to pod names:
kubectl get pods -A -o jsonpath=\'{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].containerID}{"\n"}{end}' \ | sed 's|containerd://||g'4. Build the hardened image
Section titled “4. Build the hardened image”harden build \ --manifest manifest.json \ --source nginx:1.25-alpine \ --output /tmp/hardened-nginx--source must be the same image reference the profile was recorded against. Add --platform linux/arm64 if your deployment target is not linux/amd64.
5. Validate and import
Section titled “5. Validate and import”skopeo copy oci:/tmp/hardened-nginx docker-daemon:myapp:hardeneddocker run --rm myapp:hardened nginx -t # replace with your app's smoke testOr push directly to a registry during the build:
harden build \ --manifest manifest.json \ --source nginx:1.25-alpine \ --output /tmp/hardened-nginx \ --push myregistry.com/myapp:hardenedWhat a successful build looks like
Section titled “What a successful build looks like”Source: nginx:1.25-alpine (sha256:fac2017f...)Auth: anonymousRegistry: docker.ioFiles: 312 (289 direct, 23 inferred-elf, 0 manual/scratch-compat)Confidence: 88/100 (High)Layer: 8.3 MB (sha256:...)OCI layout: /tmp/hardened-nginxNext: skopeo copy oci:/tmp/hardened-nginx docker-daemon:myapp:hardenedWarning: /etc/resolv.conf not found in image layers (bind-mounted at runtime — OK)Key things to check:
- Confidence of 80+ (High) is safe to promote with normal change management; 60–79 (Medium) is acceptable for staging but should be reviewed before production. See Observation sources & confidence for the full score bands and what lowers the score.
- Files count should be non-zero —
harden buildnow refuses to build at all (exit3) when the manifest has zerodirect(eBPF-observed) entries, since a build from inferred/manual entries alone with no direct observations is very likely broken. Pass--allow-emptyto build anyway. resolv.confabsent is expected — the container runtime bind-mounts it; exit code 2 is returned only for other missing scratch-compat files.- If
harden buildexits 0 but the hardened image fails to start, run with--verboseand use--includeto add missing directories. The runtime presets cover known gaps for common runtimes (nginx, Python, Java, Postgres, and more).
Standalone (single Linux host, no Kubernetes)
Section titled “Standalone (single Linux host, no Kubernetes)”The sensor can run directly on a Linux host and profile containers started via crictl:
# 1. Enable NRI in containerd (see Installation)
# 2. Run the sensor binary (Linux only, requires root)sudo ./sensor \ --profile-dir /tmp/tracepod-profiles \ --verbose
# 3. Start a container via crictl (not docker run)sudo crictl run container.json sandbox.json
# 4. Stop the container — this triggers the manifest writesudo crictl stop <container-id>
# 5. Build from the manifestharden build \ --manifest /tmp/tracepod-profiles/<container-id>/files.json \ --source nginx:1.25-alpine \ --output /tmp/hardened-nginxTroubleshooting: no profiles appear
Section titled “Troubleshooting: no profiles appear”If the sensor pod itself is CrashLoopBackOff with fatal: NRI unavailable in its logs, NRI is unreachable on that node — fix the containerd config (see Requirements) before working through the rest of this list, which assumes the sensor pod is Ready.
- Is the sensor connected?
kubectl logs -n tracepod daemonset/tracepod-sensor | tail -20— look forNRI connected. - Was the container started via the CRI? Only kubelet or
crictlcontainers are profiled — notdocker run,nerdctl run, ordocker-compose. - Did the container stop? Profiles are written on container stop, not while running.
- Is the sensor tracking the container?
kubectl logs -n tracepod daemonset/tracepod-sensor | grep tracking
Next steps
Section titled “Next steps”- How profiling works — the eBPF sensor internals
- Kubernetes deployment — chart values and profile retrieval in depth
- GitHub Action — harden images in CI