Reproducible builds are a foundational security practice. If you cannot verify that a given binary was built from a given source commit, you cannot trust your supply chain. Earthly is a build tool designed around reproducibility — every build runs in a container with explicit inputs, producing deterministic outputs regardless of the host environment.
For security teams, this solves a class of problems that traditional build systems struggle with. This guide covers why reproducibility matters for security and how Earthly delivers it.
Why Reproducibility Matters for Security
Consider a standard build pipeline: a developer commits code, the CI system checks out the code, installs dependencies, compiles, and produces an artifact. Simple enough. But the artifact depends on:
- The CI runner's operating system and installed packages.
- The versions of compilers and build tools on the runner.
- The state of dependency registries at build time.
- Environment variables on the runner.
- Cached artifacts from previous builds.
Change any of these and you get a different artifact. An attacker who can influence any of these inputs — a compromised dependency, a tampered cache, a modified runner image — can alter the build output without touching the source code.
Reproducible builds eliminate this ambiguity. Given the same source code and the same Earthfile, you get the same output. Always.
Earthly's Security Model
Earthly uses a Dockerfile-like syntax called Earthfile to define builds:
VERSION 0.7
build:
FROM golang:1.21-alpine
WORKDIR /app
COPY go.mod go.sum .
RUN go mod download
COPY . .
RUN go build -o /app/server .
SAVE ARTIFACT /app/server AS LOCAL ./build/server
docker:
FROM alpine:3.18
COPY +build/server /usr/local/bin/server
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/server"]
SAVE IMAGE myapp:latest
Security-relevant properties:
- Containerized execution: Every target runs in a container. No host environment leakage.
- Explicit dependencies: Data flows between targets via
COPY +target/artifact. No implicit shared state. - Pinned base images: You specify exact base images. Earthly does not surprise you with a different
golang:1.21than the one you tested with. - Deterministic caching: Earthly's cache is content-addressed. The same inputs always produce the same cache hits.
Eliminating Build Environment Drift
One of the most common security failures in CI/CD is build environment drift. The CI runner's image gets updated, a system library changes version, and suddenly your builds are different. Most of the time this causes a build failure and you notice. Sometimes it causes a subtle change in behavior that slips through.
Earthly eliminates this by containerizing the entire build. The build environment is defined in the Earthfile, versioned alongside the source code, and identical everywhere:
build:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
gcc=4:11.2.0-1ubuntu1 \
make=4.3-4.1build1
COPY . /src
RUN cd /src && make
The exact packages, at exact versions, installed in a specific base image. This build produces the same result on any machine that runs Earthly.
Dependency Pinning and Verification
Earthly supports secrets and build arguments, but it also enables strict dependency management:
deps:
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json .
RUN npm ci --ignore-scripts
# npm ci uses the lockfile exactly, no resolution surprises
verify-deps:
FROM +deps
RUN npm audit --production --audit-level=high
RUN npx lockfile-lint \
--path package-lock.json \
--type npm \
--allowed-hosts npm \
--validate-https
The verify-deps target runs dependency auditing and lockfile validation. It checks that all dependencies come from expected registries over HTTPS. This can be a prerequisite for the build target.
Integrated Security Scanning
Because Earthly targets are composable, you can build a security scanning pipeline that runs consistently across all your projects:
VERSION 0.7
IMPORT github.com/myorg/security-targets AS security
build:
FROM golang:1.21-alpine
COPY . /src
RUN cd /src && go build -o /app/server .
SAVE ARTIFACT /app/server
scan-sast:
FROM returntocorp/semgrep:latest
COPY . /src
RUN semgrep --config auto --error /src
scan-container:
FROM aquasec/trivy:latest
COPY +docker/image.tar /image.tar
RUN trivy image --input /image.tar --exit-code 1 --severity HIGH,CRITICAL
scan-dependencies:
FROM +deps
RUN go install golang.org/x/vuln/cmd/govulncheck@latest
RUN govulncheck ./...
security-gate:
BUILD +scan-sast
BUILD +scan-container
BUILD +scan-dependencies
The security-gate target runs all security scans in parallel. If any scan fails, the gate fails. This target can be a prerequisite for the docker target that produces the deployable image.
Secret Handling
Earthly provides first-class secret support:
deploy:
FROM alpine:3.18
RUN --secret DEPLOY_KEY=+secrets/deploy-key \
./deploy.sh
Secrets are:
- Never written to image layers.
- Not cached.
- Passed through a dedicated mechanism, not environment variables.
- Scrubbed from build output.
Earthly secrets can be sourced from environment variables, files, or secret managers through Earthly's secret provider interface.
Satellite Builds and Security
Earthly Cloud offers "Satellites" — remote BuildKit instances for CI builds. From a security perspective:
- Satellites provide dedicated build infrastructure, not shared runners.
- Cache is per-satellite, not shared between organizations.
- Satellites can be provisioned in specific regions for data sovereignty requirements.
- TLS encryption protects data in transit between the Earthly CLI and the satellite.
For teams with strict build isolation requirements, self-hosted Earthly (running your own BuildKit instances) gives full control over the build infrastructure.
SBOM Generation
Earthly builds can generate SBOMs as part of the build process:
sbom:
FROM +build
RUN apk add --no-cache syft
RUN syft /app/server -o spdx-json > /sbom.json
SAVE ARTIFACT /sbom.json AS LOCAL ./build/sbom.json
Because the SBOM is generated from the exact build output in the same containerized environment, it accurately reflects what is in the artifact. No discrepancies between what was scanned and what was deployed.
Verifying Build Reproducibility
You can verify reproducibility by building the same target twice and comparing outputs:
earthly +build
sha256sum ./build/server
# a1b2c3d4...
earthly +build
sha256sum ./build/server
# a1b2c3d4... (same hash)
If the hashes match, the build is reproducible. If they do not, you have non-determinism to track down (timestamps in binaries, random ordering, etc.).
Some languages and build systems are harder to make reproducible than others. Go is excellent. Java and C++ require more effort. Node.js and Python have their own challenges with bytecode compilation.
How Safeguard.sh Helps
Safeguard.sh ingests the SBOMs and build attestations produced by Earthly builds to maintain a continuous inventory of what your software contains. Because Earthly's reproducible builds guarantee that the SBOM matches the deployed artifact, Safeguard.sh can provide high-confidence vulnerability tracking — every CVE mapped to the exact artifacts affected. When combined with policy-as-code enforcement, Safeguard.sh ensures that only builds passing your security gates reach production, and maintains the provenance chain from source commit through Earthly build to deployed container.