DevSecOps

Earthly Reproducible Builds and Security

How Earthly's reproducible, containerized build system eliminates environment drift and strengthens build integrity for security-conscious teams.

Bob
Senior Security Consultant
6 min read

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.21 than 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.

Never miss an update

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