Quickstart
This is the shortest real 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”- NRI is enabled in containerd on every node — see Installation
- The
hardenbinary is installed locally skopeo(orcrane) is available for importing the result
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 should be 70+ for a production build; see Observation sources & confidence for what lowers the score.
- Files count should be non-zero — 0 direct observations means the sensor was not active or profiling captured no file-opens.
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”- Is NRI enabled?
grep disable /etc/containerd/config.toml | grep nrishould printdisable = false(or nothing). Restart containerd after changing it. - 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 machinery under the hood
- Kubernetes deployment — chart values and profile retrieval in depth
- GitHub Action — harden images in CI