Skip to content

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.

  • Your nodes meet the Requirements — NRI reachable, cgroup v2, kernel BTF, and a cluster that allows privileged/hostPID DaemonSet pods
  • The harden binary is installed locally
  • skopeo (or crane) is available for importing the result

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:

Terminal window
./hack/discovery-probe.sh

Exit 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.

Terminal window
helm install tracepod ./helm/tracepod \
--namespace tracepod \
--create-namespace

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.

Terminal window
kubectl exec -n tracepod daemonset/tracepod-sensor -- \
cat /profiles/<container-id>/files.json > manifest.json

Map container IDs to pod names:

Terminal window
kubectl get pods -A -o jsonpath=\
'{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].containerID}{"\n"}{end}' \
| sed 's|containerd://||g'
Terminal window
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.

Terminal window
skopeo copy oci:/tmp/hardened-nginx docker-daemon:myapp:hardened
docker run --rm myapp:hardened nginx -t # replace with your app's smoke test

Or push directly to a registry during the build:

Terminal window
harden build \
--manifest manifest.json \
--source nginx:1.25-alpine \
--output /tmp/hardened-nginx \
--push myregistry.com/myapp:hardened
Source: nginx:1.25-alpine (sha256:fac2017f...)
Auth: anonymous
Registry: docker.io
Files: 312 (289 direct, 23 inferred-elf, 0 manual/scratch-compat)
Confidence: 88/100 (High)
Layer: 8.3 MB (sha256:...)
OCI layout: /tmp/hardened-nginx
Next: skopeo copy oci:/tmp/hardened-nginx docker-daemon:myapp:hardened
Warning: /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 build now refuses to build at all (exit 3) when the manifest has zero direct (eBPF-observed) entries, since a build from inferred/manual entries alone with no direct observations is very likely broken. Pass --allow-empty to build anyway.
  • resolv.conf absent is expected — the container runtime bind-mounts it; exit code 2 is returned only for other missing scratch-compat files.
  • If harden build exits 0 but the hardened image fails to start, run with --verbose and use --include to 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:

Terminal window
# 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 write
sudo crictl stop <container-id>
# 5. Build from the manifest
harden build \
--manifest /tmp/tracepod-profiles/<container-id>/files.json \
--source nginx:1.25-alpine \
--output /tmp/hardened-nginx

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.

  1. Is the sensor connected? kubectl logs -n tracepod daemonset/tracepod-sensor | tail -20 — look for NRI connected.
  2. Was the container started via the CRI? Only kubelet or crictl containers are profiled — not docker run, nerdctl run, or docker-compose.
  3. Did the container stop? Profiles are written on container stop, not while running.
  4. Is the sensor tracking the container? kubectl logs -n tracepod daemonset/tracepod-sensor | grep tracking