Private Repos sicher im Build nutzen
ARG und ENV landen fast immer sichtbar im fertigen Image, auch wenn man sie loescht. BuildKit bietet mit --secret und --ssh einen Weg, Zugangsdaten und SSH-Keys nur fluechtig waehrend eines RUN-Schritts bereitzustellen, ohne dass sie in einer Layer landen.
Inhaltsverzeichnis
- 1. Warum ARG und ENV fuer Secrets ungeeignet sind
- 2. Funktionsweise von RUN --mount=type=secret
- 3. Praxisbeispiel: private Composer-Pakete im Build
- 4. SSH Agent Forwarding mit --ssh
- 5. Praxisbeispiel: privates Git-Repo per SSH klonen
- 6. Secrets in Docker Compose Builds einbinden
- 7. Einsatz in GitHub Actions und GitLab CI
- 8. Sicherheitsaspekte und typische Fallstricke
- 9. Best Practices und Methodenvergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ARG und ENV fuer Secrets ungeeignet sind
Ein klassischer Fehler beim Bauen von Docker-Images ist, Zugangsdaten fuer private Package-Repositories oder Git-Hosts per ARG oder ENV in den Build zu reichen. Das Problem: Jede ARG- und ENV-Anweisung wird Teil der Image-Metadaten und ist mit docker history oder docker inspect auslesbar, selbst wenn eine spaetere Anweisung den Wert scheinbar ueberschreibt. Wer glaubt, ein RUN unset VAR am Ende des Dockerfiles wuerde das Secret entfernen, irrt sich, denn die urspruengliche Layer mit dem Klartext-Wert bleibt im Image-Verlauf erhalten und laesst sich mit wenigen Handgriffen extrahieren.
Besonders kritisch wird es, wenn Images in eine oeffentliche oder auch nur teamweite Registry gepusht werden, denn dann ist das Secret potenziell fuer jeden mit Pull-Zugriff sichtbar. Genau fuer dieses Problem wurde BuildKit, die seit Docker 18.09 verfuegbare und inzwischen standardmaessig aktive Build-Engine, um zwei Mechanismen erweitert: --secret fuer beliebige Zugangsdaten wie Composer-Auth-Tokens oder npm-Tokens, und --ssh fuer die Weiterleitung eines SSH-Agenten, um private Git-Repositories per SSH zu klonen. Beide Mechanismen stellen die sensiblen Daten nur waehrend eines einzelnen RUN-Befehls per In-Memory-Mount bereit und schreiben sie nicht in eine Layer.
2. Funktionsweise von RUN --mount=type=secret
Die Syntax RUN --mount=type=secret,id=
Beim Aufruf von docker build wird das Secret dann per Flag uebergeben, entweder als Datei mit --secret id=composer_auth,src=./auth.json oder direkt aus einer Umgebungsvariable mit --secret id=npm_token,env=NPM_TOKEN. Letzteres ist besonders praktisch in CI-Pipelines, weil das Secret dort meist ohnehin schon als Umgebungsvariable aus einem Vault oder den CI-Secrets vorliegt und nicht erst als Datei auf die Platte geschrieben werden muss. Wichtig ist, das Secret innerhalb des RUN-Befehls per cat oder Umleitung zu konsumieren und nicht versehentlich als Shell-Argument zu loggen, denn Build-Logs koennten sonst selbst zur Leckquelle werden.
# syntax=docker/dockerfile:1
# Secret als Umgebungsvariable aus der CI uebergeben
docker build \
--secret id=composer_auth,env=COMPOSER_AUTH \
--secret id=npm_token,env=NPM_TOKEN \
-t meinshop/app:latest .
# Alternativ als Datei uebergeben (lokal, nicht ins Repo committen)
docker build --secret id=composer_auth,src=./auth.json -t meinshop/app:latest .
3. Praxisbeispiel: private Composer-Pakete im Build
Ein typischer Anwendungsfall bei Magento- oder Symfony-Projekten ist der Zugriff auf ein privates Composer-Repository wie repo.magento.com oder einen firmeneigenen Satis-Server. Ohne Secret-Mount muesste man entweder die auth.json mit Klartext-Credentials ins Image kopieren, was sie dauerhaft sichtbar macht, oder umstaendlich mit Multi-Stage-Builds und manuellem Loeschen tricksen, was wie oben beschrieben trotzdem Spuren hinterlaesst. Mit dem Secret-Mount bleibt die auth.json ausschliesslich waehrend des composer install Aufrufs sichtbar und verschwindet danach spurlos.
Im folgenden Beispiel wird die auth.json per Mount an die vom Composer erwartete Stelle im HOME-Verzeichnis gelegt, sodass composer install sie automatisch findet, ohne dass sie im finalen Image-Layer landet. Diese Technik funktioniert analog fuer npm mit einer .npmrc, die einen Auth-Token enthaelt, oder fuer pip mit einer pip.conf fuer private PyPI-Indizes.
# syntax=docker/dockerfile:1
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
# auth.json liegt nur waehrend dieses RUN-Schritts im Container
RUN --mount=type=secret,id=composer_auth,target=/root/.composer/auth.json \
composer install --no-dev --no-scripts --no-interaction --prefer-dist
FROM php:8.4-fpm-alpine AS runtime
COPY --from=vendor /app/vendor /var/www/html/vendor
COPY . /var/www/html
4. SSH Agent Forwarding mit --ssh
Wenn ein Build private Git-Repositories per SSH klonen muss, etwa ein internes Composer-Paket, das direkt aus einem Git-Remote statt aus einer Registry geladen wird, ist das Kopieren eines privaten Schluessels ins Image tabu, selbst temporaer, weil auch hier Layer-Reste bestehen bleiben koennen. BuildKit loest das eleganter durch Forwarding des lokal laufenden SSH-Agenten in den Build-Container, analog zum bekannten ssh -A Prinzip.
Voraussetzung ist ein laufender ssh-agent mit geladenem Schluessel auf dem Host oder Runner, der Aufruf erfolgt dann mit docker build --ssh default=$SSH_AUTH_SOCK oder kurz --ssh default, wenn die Umgebungsvariable SSH_AUTH_SOCK bereits gesetzt ist. Im Dockerfile wird der Agent-Socket dann per RUN --mount=type=ssh in genau diesem RUN-Schritt verfuegbar gemacht, sodass git clone oder ssh-basierte Composer-Requires funktionieren, ohne dass ein privater Schluessel jemals den Host verlaesst oder im Image landet.
5. Praxisbeispiel: privates Git-Repo per SSH klonen
Im folgenden Beispiel wird ein internes Modul-Repository per SSH geklont, waehrend gleichzeitig bekannte Host-Keys eingebunden werden, damit git nicht interaktiv nach einer Bestaetigung fragt. Der StrictHostKeyChecking-Parameter sollte in produktiven Pipelines nicht pauschal deaktiviert, sondern durch einen vorab bekannten known_hosts-Eintrag ersetzt werden, um Man-in-the-Middle-Risiken zu vermeiden.
Wichtig ist zudem, dass der Basis-Image mindestens einen SSH-Client enthaelt, denn Alpine- oder Debian-Slim-Images bringen openssh-client meist nicht standardmaessig mit und muessen ihn vorab per apk oder apt installieren. Nach dem Klonen im Build-Stage wird nur das Ergebnis, nicht aber der SSH-Zugriff, in die naechste Stage kopiert.
# syntax=docker/dockerfile:1
FROM alpine:3.20 AS git-fetch
RUN apk add --no-cache git openssh-client && \
mkdir -p -m 0700 /root/.ssh && \
ssh-keyscan gitlab.mironsoft.internal >> /root/.ssh/known_hosts
RUN --mount=type=ssh \
git clone git@gitlab.mironsoft.internal:core/shared-module.git /src/shared-module
FROM php:8.4-fpm-alpine AS runtime
COPY --from=git-fetch /src/shared-module /var/www/html/vendor/mironsoft/shared-module
6. Secrets in Docker Compose Builds einbinden
Auch docker compose build unterstuetzt BuildKit Secrets ueber den Schluessel secrets innerhalb der build-Sektion eines Services. Damit muss das Secret-Flag nicht bei jedem docker build manuell gesetzt werden, sondern ist deklarativ in der compose.yaml hinterlegt, was besonders bei mehreren Services mit unterschiedlichen Secrets die Wartbarkeit verbessert.
Die Top-Level secrets-Sektion definiert dabei die Quelle, entweder eine Datei oder eine Umgebungsvariable, waehrend die service-lokale Referenz nur den Namen angibt. Fuer den produktiven Einsatz in CI-Systemen wird meist die environment-Variante bevorzugt, weil dort keine temporaeren Dateien auf der Build-Maschine verbleiben, die versehentlich in ein Artefakt oder Cache-Verzeichnis geraten koennten.
services:
app:
build:
context: .
secrets:
- composer_auth
- npm_token
secrets:
composer_auth:
environment: COMPOSER_AUTH
npm_token:
environment: NPM_TOKEN
7. Einsatz in GitHub Actions und GitLab CI
In GitHub Actions lassen sich BuildKit Secrets sauber mit docker/build-push-action kombinieren, indem der secrets-Input verwendet wird, der intern die passenden --secret-Flags an den Build weiterreicht. Repository- oder Organization-Secrets werden dabei ueber ${{ secrets.NAME }} referenziert und niemals im Klartext im Workflow-File hinterlegt, was Audits und Secret-Scanning deutlich vereinfacht.
In GitLab CI funktioniert das analog ueber vordefinierte CI/CD-Variablen, die als maskierte oder protected Variablen im Projekt hinterlegt und dann per --secret id=name,env=VARNAME an den docker build Aufruf im Job-Skript uebergeben werden. Wichtig ist in beiden Systemen, dass das jeweilige Secret nicht versehentlich als Build-ARG statt als BuildKit-Secret deklariert wird, denn ARGs landen wie eingangs beschrieben sichtbar im Image, waehrend Secrets das nicht tun.
# .github/workflows/build.yml (Ausschnitt)
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/mironsoft/app:${{ github.sha }}
secrets: |
composer_auth=${{ secrets.COMPOSER_AUTH }}
npm_token=${{ secrets.NPM_TOKEN }}
8. Sicherheitsaspekte und typische Fallstricke
Ein haeufiger Fehler ist, ein Secret zwar per --mount=type=secret bereitzustellen, es dann aber im selben RUN-Befehl per echo oder set -x auszugeben, wodurch es in den Build-Logs landet, die oft laenger aufbewahrt werden als das Image selbst. Build-Logs sollten daher grundsaetzlich als potenzielle Leckquelle betrachtet und set -x in sicherheitsrelevanten RUN-Schritten vermieden werden. Ebenso riskant ist es, ein per Secret-Mount geladenes Zertifikat oder eine Config-Datei versehentlich per COPY in eine spaetere Stage zu kopieren, denn dann landet der Inhalt doch in einer persistenten Layer.
Zu beachten ist ausserdem, dass Secret-Mounts nur mit dem BuildKit-Backend funktionieren und nicht mit dem klassischen, mittlerweile veralteten Legacy-Builder. Seit Docker Engine 23.0 ist BuildKit Standard, in aelteren Setups muss DOCKER_BUILDKIT=1 explizit gesetzt werden. Wer Multi-Platform-Builds mit buildx nutzt, sollte zudem pruefen, dass der verwendete Buildx-Builder tatsaechlich BuildKit-basiert ist, denn manche minimalen CI-Runner-Images bringen nur einen abgespeckten Docker-Client ohne vollstaendige BuildKit-Unterstuetzung mit.
9. Best Practices und Methodenvergleich
Als Faustregel gilt: Alles, was zur Laufzeit des Containers gebraucht wird, gehoert in ENV oder in einen Runtime-Secret-Mechanismus wie Docker Secrets im Swarm-Modus oder Kubernetes Secrets, waehrend alles, was nur waehrend des Build-Vorgangs gebraucht wird, wie Registry-Zugangsdaten oder SSH-Keys fuer private Dependencies, ausschliesslich per BuildKit --secret oder --ssh eingebunden werden sollte. ARG bleibt weiterhin sinnvoll fuer unkritische Build-Parameter wie Versionsnummern oder Build-Datum, niemals aber fuer Zugangsdaten.
Wer BuildKit Secrets konsequent einsetzt, reduziert nicht nur die Angriffsflaeche eines Images erheblich, sondern vereinfacht auch Security-Audits, weil ein docker history niemals sensible Werte offenlegen kann, die gar nicht erst in eine Layer geschrieben wurden. Die folgende Tabelle fasst die verschiedenen Mechanismen und ihre Eignung fuer unterschiedliche Szenarien zusammen.
| Mechanismus | Sichtbar im Image | Geeignet fuer | Empfehlung |
|---|---|---|---|
| ARG | Ja, dauerhaft in Layer-Historie | Unkritische Build-Parameter | Niemals fuer Secrets |
| ENV | Ja, dauerhaft im Image | Laufzeit-Konfiguration | Nicht fuer Secrets |
| RUN --mount=type=secret | Nein, nur waehrend RUN | Auth-Tokens, Passwoerter, Zertifikate | Empfohlen fuer Build-Secrets |
| RUN --mount=type=ssh | Nein, Agent-Forwarding | Privates Git-Klonen per SSH | Empfohlen fuer SSH-Zugriffe |
Mironsoft
Container-Infrastruktur, CI-Pipelines und Deployment-Automatisierung
Docker-Setups, die im Team und in Produktion tragfähig bleiben?
Wir prüfen bestehende Dockerfiles und Compose-Stacks auf Sicherheitslücken, aufgeblähte Images und fragile Build-Pipelines und bauen daraus eine Container-Infrastruktur, die schnell baut, sicher läuft und im Team nachvollziehbar bleibt.
Dockerfile-Review
Multi-Stage-Builds, Layer-Caching und Image-Größe systematisch optimieren.
Security-Audit
Container-Isolation, Secrets-Handling und Image-Scanning gegen echte Angriffsflächen absichern.
CI/CD-Integration
Build-Pipelines, Registries und Deployment-Strategien für reproduzierbare Releases aufbauen.
10. Zusammenfassung
BuildKit Secrets: Das Wichtigste auf einen Blick
Kernproblem
ARG und ENV schreiben Secrets dauerhaft in die Image-Historie, auch nach scheinbarem Ueberschreiben.
Loesung Secrets
RUN --mount=type=secret stellt Dateien nur waehrend eines RUN-Schritts bereit, ohne Layer-Spuren.
Loesung SSH
RUN --mount=type=ssh forwardet den lokalen SSH-Agenten fuer privates Git-Klonen ohne Schluesselkopie.
CI-Integration
GitHub Actions und GitLab CI reichen Secrets ueber maskierte Variablen an --secret-Flags weiter.