Outputs
A successful harden build writes everything into the --output directory. This page describes each artifact.
Minimized OCI image
Section titled “Minimized OCI image”The primary output: an OCI image layout containing a FROM scratch image with a single deterministic layer — sorted paths, zero mtimes — holding only:
- files directly observed at runtime (
direct), - their recursively resolved ELF shared-library dependencies (
inferred-elf), - runtime companion files (
inferred-runtime, e.g. Python.pysources implied by observed.pycfiles), - operator-included paths (
directory-inclusion,manual), and - scratch-compatibility files the hardener adds automatically (
/etc/passwd,/etc/group, the dynamic linker, TLS certificates).
The original image config is preserved — Entrypoint, Cmd, Env, User, WorkingDir, ExposedPorts, Healthcheck — and com.tracepod.* provenance labels are added.
Use the layout directly with any OCI-aware tool:
# Import into a local Docker daemon:skopeo copy oci:/tmp/hardened docker-daemon:myapp:hardened
# Or push during the build:harden build ... --push myregistry.com/myapp:hardenedDeterministic layer construction means rebuilding from the same manifest and source image produces the same layer digest.
Removal manifest
Section titled “Removal manifest”Written to <output-dir>/removal-manifest.json on every successful build. It is a set-difference fact: every OS package (dpkg/apk/rpm) present in the source image whose owned files are entirely absent from the hardened image, with the file paths that drove each removal.
Key properties:
- Facts only — no reachability, justification, or VEX vocabulary; the consumer decides what a removal means.
- Partial retention is not removal — a package with any retained file is absent from the manifest; multi-owner files keep every owning package “retained.”
- No scanner involved — the hardener runs no vulnerability scanner; CVE association is a downstream concern.
The build summary reports it as Removed pkgs: <n> (<path>). A source-scan failure is non-fatal (warning only). The formal schema is at docs/removal-manifest-schema/ in the repository.
SBOMs (CycloneDX + SPDX)
Section titled “SBOMs (CycloneDX + SPDX)”Pass --sbom to generate both formats via a syft subprocess run against the hardened OCI layout:
<output-dir>/sbom.cyclonedx.json<output-dir>/sbom.spdx.jsonBoth formats are produced because enterprise toolchains typically require one or the other. syft must be on PATH; SBOM failure is non-fatal (a warning, not a build error).
Because the SBOM is generated from the hardened image, it reflects only the packages that actually ship — and included_because justifications from manual manifest entries propagate into it, giving auditors traceability for every operator-added path.
Cosign signing
Section titled “Cosign signing”Pass --sbom-sign-key <path-to-cosign-private-key> (requires --sbom) to sign both SBOM files with cosign. Each SBOM gets a .sig sidecar in the output directory. cosign must be on PATH.
harden build \ --manifest manifest.json \ --source myapp:1.0 \ --output /tmp/hardened \ --sbom \ --sbom-sign-key cosign.keyBuild summary and confidence
Section titled “Build summary and confidence”The build prints a summary — source digest, auth source, file counts by observation source, the confidence score, layer size and digest — plus warnings for missing scratch-compat files, missing --include paths, and Very Low confidence.
Exit codes:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Fatal error — missing required flags, pull failure, unresolved ELF (DT_NEEDED) dependencies, or a failed smoke test |
2 |
Non-fatal warning: a scratch-compat file other than resolv.conf was absent from the source image layers (resolv.conf absence is expected — the container runtime bind-mounts it) |
Smoke test
Section titled “Smoke test”Pass --smoke-test to load the built image into the local Docker daemon and run it briefly (--smoke-window, default 5s). The build fails (exit 1) if the minimized image cannot boot. This is the fastest signal that the profile was complete enough.