BuildKit’s buildx can securely pass secrets to your build process, but it’s easy to accidentally expose them if you’re not careful.
Here’s how a typical build with secrets looks in practice, and then we’ll dive into the mechanisms.
# syntax=docker/dockerfile:1
FROM alpine:latest
RUN --mount=type=secret,id=mysecret SECRET=$(cat /run/secrets/mysecret) && echo "Secret is $SECRET" >> /app/secret.txt
And the buildx command to run it:
buildx build --secret id=mysecret,src=./mysecret.txt -t myimage .
If mysecret.txt contains verysecretpassword, the output will show Secret is verysecretpassword. Crucially, this secret never appears in the final image layers.
The Problem: Accidental Exposure
The most common way secrets leak is by being written to build cache or, worse, directly into a container filesystem that isn’t ephemeral.
Consider this Dockerfile:
# syntax=docker/dockerfile:1
FROM alpine:latest
RUN --mount=type=secret,id=mysecret echo "My secret is $(cat /run/secrets/mysecret)" > /app/secret.txt
RUN cat /app/secret.txt
If you run this with buildx build --secret id=mysecret,src=./mysecret.txt -t myimage ., the output will contain the secret. Even worse, if you were to later inspect the intermediate layers of the image (which are often cached), the RUN cat /app/secret.txt command might be cached with the secret already written to a file within that layer’s filesystem state.
Another common mistake is using --mount=type=cache alongside --mount=type=secret in a way that allows the secret to be written to the cache. BuildKit’s cache is designed to be immutable, but if you’re not careful about how you’re copying or writing data that could be a secret, you might inadvertently persist it.
How BuildKit Handles Secrets
BuildKit’s secret mounting works by providing a temporary, in-memory filesystem accessible only during the RUN instruction that requests it. This filesystem is mounted at /run/secrets/ by default, or wherever you specify with target.
When you use --mount=type=secret,id=mysecret,src=./mysecret.txt, BuildKit reads the content of ./mysecret.txt and makes it available in a temporary, encrypted, in-memory buffer. This buffer is then mounted as a read-only filesystem within the specific RUN instruction. Once the RUN instruction finishes, this mount is unmounted and the data is garbage collected. It is not written to the image layers.
Common Causes and Fixes
-
Secrets written to a file that persists:
- Diagnosis: Inspect the Dockerfile. Look for
RUNcommands that write secret content to files after the--mount=type=secretinstruction has finished, or commands that don’t use the secret mount at all but are expected to. - Fix: Ensure any
RUNcommand that accesses a secret uses the--mount=type=secretdirective. Never directlyechoorcata secret into a file that is intended to be part of the final image layer unless it’s within the scope of the secret mount.
The# INCORRECT (leaks secret to layer) FROM alpine:latest RUN --mount=type=secret,id=mysecret \ echo $(cat /run/secrets/mysecret) > /app/leaked_secret.txt # CORRECT (secret only available during RUN) FROM alpine:latest RUN --mount=type=secret,id=mysecret \ echo "Secret processed" > /app/processed.txtecho $(cat /run/secrets/mysecret) > /app/leaked_secret.txtline would write the secret to/app/leaked_secret.txt, which then becomes part of the image layer. The corrected version only writes a log message.
- Diagnosis: Inspect the Dockerfile. Look for
-
Secrets being part of the build cache:
- Diagnosis: If you’re using
--mount=type=cache, and the value being cached could have been derived from a secret, there’s a risk. BuildKit’s cache is keyed by the build step’s content hash. If aRUNcommand using a secret also produces output that influences the cache key, and that output is problematic, the cache could become a vector. - Fix: Avoid caching steps that directly process or output raw secrets. If you must cache data derived from secrets, ensure it’s sanitized or only used in ephemeral contexts.
The fix here isn’t a Dockerfile change but a build strategy change: don’t cache steps that operate on secrets, or ensure the cached output is scrubbed.# Potentially problematic if 'output.txt' contains sensitive info from MYSECRET RUN --mount=type=secret,id=mysecret \ --mount=type=cache,target=/app/cache \ SECRET_VAL=$(cat /run/secrets/mysecret) && \ echo "$SECRET_VAL" > /app/output.txt && \ echo "$SECRET_VAL" >> /app/cache/data.log
- Diagnosis: If you’re using
-
Using
--mount=type=bindwith secret files:- Diagnosis: Accidentally binding a file containing secrets into the build context using
--mount=type=bindinstead of--mount=type=secret. Bind mounts are more permanent and can be more easily inspected. - Fix: Always use
--mount=type=secretfor sensitive data. Bind mounts are for general file access.
The# INCORRECT (binds the actual secret file, which might be inspected) buildx build --mount type=bind,src=./mysecret.txt,dst=/app/secret.txt -t myimage . # CORRECT buildx build --secret id=mysecret,src=./mysecret.txt -t myimage .--mount=type=bindmakes./mysecret.txtavailable at/app/secret.txtwithin the container, which is generally not what you want for secrets.
- Diagnosis: Accidentally binding a file containing secrets into the build context using
-
Secrets in
docker buildarguments (--build-arg):- Diagnosis: Using
ARGin the Dockerfile and passing values viadocker build --build-arg MY_SECRET=.... These are visible in the build history and can be easily inspected. - Fix: Use
--mount=type=secretexclusively for secrets.ARGis for build-time configuration that isn’t sensitive.# INCORRECT (secret exposed via ARG) ARG MY_SECRET RUN echo "My secret is $MY_SECRET" > /app/secret.txt # CORRECT (using secret mount) RUN --mount=type=secret,id=mysecret \ echo "Secret is $(cat /run/secrets/mysecret)" > /app/secret.txt
- Diagnosis: Using
-
Not cleaning up temporary files containing secrets:
- Diagnosis: Even when using secret mounts correctly, if a
RUNcommand copies the secret from/run/secrets/mysecretto a temporary file and then forgets tormit, that temporary file might persist if theRUNstep is part of a larger multi-stage build or if an error occurs. - Fix: Always ensure temporary files are cleaned up within the same
RUNinstruction that creates them, or usemktempwith careful cleanup.
Or, more robustly:# INCORRECT (temp file might persist on error) RUN --mount=type=secret,id=mysecret \ echo $(cat /run/secrets/mysecret) > /tmp/tempsecret.txt && \ # ... do something with tempsecret.txt ... # CORRECT (cleanup within the same RUN) RUN --mount=type=secret,id=mysecret \ echo $(cat /run/secrets/mysecret) > /tmp/tempsecret.txt && \ # ... do something with tempsecret.txt ... rm /tmp/tempsecret.txtRUN --mount=type=secret,id=mysecret \ SECRET_VAL=$(cat /run/secrets/mysecret) && \ TEMP_FILE=$(mktemp) && \ echo "$SECRET_VAL" > "$TEMP_FILE" && \ # ... do something with "$TEMP_FILE" ... rm -f "$TEMP_FILE"
- Diagnosis: Even when using secret mounts correctly, if a
-
Using
docker cpon a running container from a build:- Diagnosis: If you run a container from an image that was built with secrets (even if the secrets aren’t supposed to be there), and then accidentally
docker cpfiles from a running container that might have contained transient secret data, you could leak it. - Fix: This is more about operational security. Understand what sensitive data could have been transiently present in a running container derived from a build, and avoid
docker cp-ing arbitrary files from such containers. Always use--mount=type=secretto ensure secrets are truly ephemeral.
- Diagnosis: If you run a container from an image that was built with secrets (even if the secrets aren’t supposed to be there), and then accidentally
The next hurdle is understanding how BuildKit’s cache itself can be a vector if not managed with secrets in mind.