Safeguard
Engineering

Reproducible Builds: Why Bit-for-Bit Identical Matters

If two builds of the same source produce different binaries, you cannot prove what you shipped. How determinism breaks, the flags that fix it, and why auditors care.

Elena Kovacs
Compliance Architect
6 min read

A reproducible build produces a bit-for-bit identical artifact every time it is run from the same source, toolchain, and instructions — which turns "trust our build server" into "verify our build server," because any independent party can rebuild and compare hashes. That single property is the strongest integrity control in supply chain security, and it is also the one most teams skip because the first attempt at rebuilding an artifact almost never matches, and the diff looks like chaos.

The chaos is finite. In practice, three or four categories of nondeterminism cause nearly all mismatches, and each has a known fix.

Why a signature is not enough

Code signing tells you who built an artifact. It says nothing about what they built. SolarWinds is the canonical counterexample: the SUNSPOT implant ran on the build server and swapped a source file in memory during compilation. Every resulting binary was properly signed by SolarWinds, because the build server itself was the liar. Signing verified the compromised machine's identity flawlessly.

Reproducibility attacks that problem from the other side. If an independent rebuilder — another CI system, another vendor, a customer, the Debian rebuilderd network — compiles the same tagged source and gets a different hash than the published artifact, something interfered with the build. Bitcoin Core has run this model for years through Gitian and now Guix: multiple parties build each release and publish signed attestations of the hashes they got. A build server compromise there would have to compromise every rebuilder simultaneously.

What actually breaks determinism

Almost every non-reproducible build fails for reasons on this list:

CauseTypical symptom
TimestampsArchive entries and embedded build dates differ per run
Build paths/home/ci/build-4821 baked into debug info and panic messages
Filesystem orderingreaddir order differs across machines, reordering archive contents
ParallelismLink order or resource concatenation varies with scheduling
Locale and environmentSorting and encoding differ with LC_ALL, TZ, USER
RandomnessMap iteration order, temporary file names, UUIDs in metadata

The universal timestamp fix is the SOURCE_DATE_EPOCH environment variable — set it to your commit timestamp (git log -1 --pretty=%ct) and any tool that honors the spec uses it instead of the wall clock. For everything else, fixes are per-toolchain.

The fixes, by ecosystem

Go is nearly reproducible by default. Add -trimpath to strip build directories, pin the toolchain in go.mod, and set CGO_ENABLED=0 unless you genuinely need cgo. That is usually the whole job.

Rust needs path remapping: build with RUSTFLAGS="--remap-path-prefix=$PWD=/build" and pin the toolchain in rust-toolchain.toml. Cargo respects SOURCE_DATE_EPOCH for embedded timestamps.

C and C++ want -ffile-prefix-map=$PWD=. (covers both debug and macro paths) plus deterministic archives via ar with the D modifier.

Java is the historical offender because JARs are zip files full of timestamps. Maven fixed it in one property:

<properties>
  <project.build.outputTimestamp>2025-12-04T08:00:00Z</project.build.outputTimestamp>
</properties>

Gradle needs two settings on archive tasks:

tasks.withType(AbstractArchiveTask).configureEach {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
}

Tarballs and zips built by hand need explicit normalization: tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner. Debian's strip-nondeterminism tool cleans residual metadata from a dozen archive formats and is worth running even outside Debian packaging.

Containers are the hardest case because image layers embed file timestamps and image configs embed creation dates. BuildKit supports rewrite-timestamp=true in the output options combined with SOURCE_DATE_EPOCH; kaniko has a --reproducible flag. Pin base images by digest, not tag — FROM alpine:3.21 is a moving target, FROM alpine@sha256:... is a fact.

Proving it: diffoscope and a second builder

Asserting reproducibility means rebuilding and diffing. The tool for the diff is diffoscope, from the Reproducible Builds project — it recursively unpacks archives, disassembles binaries, and shows you semantically where two artifacts differ instead of dumping raw hex:

diffoscope --text report.txt artifact-ci.jar artifact-rebuild.jar

For CI, reprotest builds your project twice while deliberately varying time, locale, umask, and working directory, then reports whether the outputs match. Wire it in as a nightly job rather than a per-commit gate at first; fixing variance is iterative work. Debian's experience is encouraging: after a decade of this, roughly 96 percent of its packages are reproducible. The long tail is real, but the first 80 percent typically falls to timestamps and paths alone — a few days of work for a mid-sized service, not a quarter.

Why compliance teams should care as much as security teams

I spend most of my time on the audit side, and reproducibility is quietly becoming the substantiation layer under several frameworks. SLSA provenance is an attestation — a signed claim about how an artifact was built. Reproducibility is verification — independent evidence the claim holds. NIST SSDF (SP 800-218) practice PW.6 asks you to configure compilation and build processes to improve executable security, and archived, deterministic build configuration is the cleanest evidence you can hand an assessor. The EU Cyber Resilience Act's integrity requirements point the same direction.

There is also a mundane payoff: a reproducible build with a pinned dependency set produces an SBOM that is actually stable across rebuilds, which makes drift detection meaningful. If you manage SBOMs centrally in something like SBOM Studio, a hash change with no source change becomes a signal instead of noise, and your SCA pipeline stops re-triaging phantom "changes." For the adjacent problem of making builds hermetic — no network, declared inputs only — see our post on Bazel hermetic builds; hermeticity and reproducibility compound each other.

Frequently asked questions

Is reproducibility required for SLSA compliance?

No. SLSA Build Level 3 requires non-forgeable provenance from a hardened builder, not deterministic output. The two are complementary: provenance tells you what the builder claims happened, reproducibility lets a third party check the claim without trusting the builder at all.

How long does it take to make an existing build reproducible?

For a typical Go or Rust service, often under a week — timestamps and embedded paths cover most of the diff. JVM builds with many plugins, and container images, take longer. Budget iteratively: run reprotest, fix the top diffoscope finding, repeat.

Do reproducible builds slow the build down?

Not meaningfully. The changes are flags and environment pinning, not extra work at compile time. The real cost is the second build you run to verify — which you can schedule nightly or run on independent infrastructure so it never blocks a release.

If we sign our builds, why bother?

A signature attests identity; it verifies nothing about the toolchain's honesty. Every SUNBURST-infected SolarWinds binary carried a valid signature. Reproducibility is the control that catches a compromised build server, because the attacker must then also compromise every independent rebuilder.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.