Earthly has been in the "interesting build system" bucket for me since its 0.6 release. The project split off as a commercial company in 2022, kept the core tool open source, and now sits in an odd position: more approachable than Bazel, more structured than a Dockerfile, and weirdly underrated by the security community. I have deployed Earthly for two clients in the past year, and this post is a concrete look at the supply chain properties that emerged from those deployments.
Earthly is currently at 0.8.x (we tested on 0.8.9). The design center is containerized, reproducible builds with a syntax that feels like a Makefile and a Dockerfile had a child. The commercial offering, Earthly Cloud (with SATELLITE remote runners), layers remote build execution on top. Both the open-source and commercial sides share the same Earthfile language.
The Earthly Execution Model
Every Earthly target runs in a container. That single design decision drives most of the supply chain story. An Earthfile looks like this:
VERSION 0.8
FROM golang:1.22-alpine
WORKDIR /src
deps:
COPY go.mod go.sum .
RUN go mod download
SAVE ARTIFACT /root/go/pkg /gopkg
build:
FROM +deps
COPY . .
RUN go build -o payments-api ./cmd/server
SAVE ARTIFACT payments-api AS LOCAL ./dist/payments-api
docker:
FROM alpine:3.19
COPY +build/payments-api /bin/payments-api
ENTRYPOINT ["/bin/payments-api"]
SAVE IMAGE --push registry.internal/payments-api:latest
Each target is a container stage. The FROM directive pulls a base image, which Earthly resolves through BuildKit. COPY and RUN run inside that stage. SAVE ARTIFACT exports a file to be consumed by other stages or the host.
The supply chain properties that fall out of this model:
- Everything runs in a container. No host filesystem access outside explicit
COPYdirectives. No host$PATHleakage. No "works on my machine" drift. - Base images are explicit. Every target
FROMs something, and Earthly resolves that reference through BuildKit's registry client, which supports digest pinning. - Caching is content-addressed. BuildKit's cache is keyed by the hash of inputs and commands. Cache hits produce identical outputs.
Digest Pinning: The First Thing to Enable
The default FROM golang:1.22-alpine pulls by tag, which is mutable. A new image published to the golang:1.22-alpine tag will change your build output. For any production pipeline, pin to digest:
FROM golang:1.22-alpine@sha256:0466223b8544fb7d4ff04748acd4d75a608234bf4e79563bff208d2060c0dd79
Earthly respects digest pins and will refuse to pull if the registry returns a different digest. This is straightforward Docker supply chain hygiene, but it is surprising how often Earthly deployments skip it.
An alternative is to use Earthly's IF and ARG features to centralize digests:
VERSION 0.8
ARG GOLANG_DIGEST=sha256:0466223b8544fb7d4ff04748acd4d75a608234bf4e79563bff208d2060c0dd79
ARG ALPINE_DIGEST=sha256:c5b1261d6d3e43071626931fc004f10139076e7fe0dcc4c46a6deadc4df94e85
base:
FROM golang:1.22-alpine@$GOLANG_DIGEST
We maintain the digests in a dedicated digests.earthlib file and IMPORT it across the repo. Updates go through a dedicated PR that triggers a vulnerability scan against the new digest before merge.
BuildKit Under the Hood
Earthly runs on BuildKit (Moby's build backend). This matters because most of Earthly's interesting security properties are BuildKit's, not Earthly's. BuildKit gives you:
- Content-addressed caching (LLB graph)
- Secret mounts that do not leak into image layers (
--mount=type=secret) - SSH agent forwarding without baking keys into layers (
--mount=type=ssh) - Frontend-to-backend separation that limits what the Dockerfile syntax can do
Earthly uses BuildKit's Go API directly, so all these properties carry over. The Earthfile syntax exposes them through RUN --secret and RUN --ssh.
A common pattern for pulling private dependencies:
deps:
COPY go.mod go.sum .
RUN --ssh --secret GITHUB_TOKEN \
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" \
GOPRIVATE=github.com/internal/* \
go mod download
The --secret GITHUB_TOKEN mounts the secret for the duration of the RUN command only. It is not copied into any layer, not cached, and not visible to other stages. This is genuinely safer than the ARG GITHUB_TOKEN anti-pattern you still see in many Dockerfiles.
Remote Runners: SATELLITE
Earthly Satellite is Earthly Cloud's managed remote-runner service. Satellites are persistent VMs that run BuildKit and maintain their cache across builds. They sit behind Earthly Cloud's auth and TLS.
From a supply chain perspective, Satellite is roughly equivalent to a shared BuildKit runner. The security posture depends on:
- Who can submit builds. Satellites are scoped to Earthly Cloud organizations. Anyone in the org can submit builds and read cached outputs.
- Cache isolation. Each Satellite maintains its own cache. Cache keys are content-addressed, so cross-contamination between builds is bounded by the LLB hash function.
- Network policy. Satellites have full network access by default. For sensitive builds, you want to run a self-hosted Satellite in your own VPC with egress restrictions.
For the banking client, we deployed a self-hosted Satellite cluster on EC2 with no internet egress except to approved registries and package mirrors. The Satellites fetch from our internal Artifactory and push to our internal ECR. No direct pulls from Docker Hub or random GitHub archives.
Reproducibility
Earthly builds are reproducible to the extent that BuildKit's LLB graph is reproducible. In practice:
- Base image pulls are reproducible if you pin to digest.
RUNcommands are reproducible if the command itself is deterministic.- Filesystem operations (
COPY,SAVE ARTIFACT) are reproducible. - Timestamps in
SAVE IMAGEoutputs are not reproducible by default.
To get reproducible image outputs, set SOURCE_DATE_EPOCH and use Earthly's SAVE IMAGE --reproducible flag (added in 0.7.10):
docker:
FROM alpine:3.19@$ALPINE_DIGEST
COPY +build/payments-api /bin/payments-api
ENTRYPOINT ["/bin/payments-api"]
SAVE IMAGE --push --reproducible registry.internal/payments-api:1.4.2
With --reproducible and a pinned digest, we achieved bit-identical image hashes across three different build hosts for the same commit. That is the foundation for useful attestations.
Attestations and Provenance
Earthly 0.8 added native support for SLSA v1.0 attestations via RUN --push signing. Combined with cosign, you can sign and push both the image and the attestation in a single Earthfile target:
sign:
FROM +docker
ARG COSIGN_KEY
RUN --push --secret COSIGN_PASSWORD \
--mount type=bind,source=$COSIGN_KEY,target=/cosign.key \
cosign sign --key /cosign.key registry.internal/payments-api:1.4.2
The provenance attestation captures the Earthfile source, the base image digests, and the build arguments. Consumers can verify that an image they are deploying was built by the expected Earthfile from the expected commit.
Weaknesses and Sharp Edges
- Earthly's CLI runs as root in most configurations because it needs Docker socket access. On developer laptops this is fine; on shared CI, ensure rootless BuildKit is configured.
- The
LOCALLYdirective bypasses the container sandbox. AnyLOCALLYblock runs on the host. Audit Earthfiles forLOCALLYusage, especially in third-party imports. - Earthly's import mechanism (
IMPORT,FROM +target) can pull in code from other Earthfiles, including remote ones viaearthly.dev. Treat remote imports as third-party code and pin them. - Cache poisoning is possible if you share a Satellite with untrusted builds. Segregate production builds to a dedicated Satellite with write access limited to trusted CI identities.
How Safeguard Helps
Safeguard ingests Earthly-built images through their OCI registry metadata and combines them with SBOM generation from each build stage to produce CycloneDX 1.5 artifact graphs that cover base images, vendored dependencies, and LOCALLY-built components. Policy gates can block Earthfiles that use unpinned base image tags, reference remote imports without digest verification, or invoke LOCALLY blocks without approved context. For teams running Satellites, Safeguard correlates Satellite-emitted provenance with source commits to verify that each production image was built by expected infrastructure. That extends Earthly's container isolation with the attestation verification and governance needed to trust its outputs in regulated environments.