A build-arg nem tűnik el: így marad benne a titkos kulcs egy Docker image rétegeiben

Az ARG-ba vagy ENV-be tett API-token, npm-jelszó vagy AWS-kulcs nem tűnik el a buildel együtt: ott marad a docker history-ban, és publikus GitHub Actions build esetén a provenance attesztációban is. A BuildKit secret mount ezt oldja meg – hands-on kipróbálva, docker history-val és exportált réteggel is ellenőrizve.

Előbb-utóbb minden Dockerfile-ba bekerül egy npm token, egy privát registry jelszó vagy egy AWS kulcs, mert a build közben kell letölteni valamit, ami nincs publikusan. A leggyorsabb megoldásnak az ARG vagy az ENV tűnik: beteszed build argumentumként, a konténerből meg amúgy sem fut ki élesben semmi extra. Csak hogy nem a futó konténerről van szó, hanem az image-ről, amit push-olsz – és az image simán megőrzi, amit beleraktál.

Diagram: egy kulcs a felső, ARG/ENV-alapú soron benn marad az image rétegeiben és kiszivárog a nyilvános registrybe, míg az alsó, secret mountos soron a kulcs csak a build közben létezik, az image lezárt lakattal érkezik meg

A docker history kiadja, amit elrejtettél

Kipróbáltam egy minimális esettel. A Dockerfile ennyi:

FROM alpine:3.20
ARG API_TOKEN
RUN echo "hívom az apit..." && test -n "$API_TOKEN"

A build simán lefut: docker build --build-arg API_TOKEN=supersecrettoken123 -t leaktest . Ezután viszont a docker history --no-trunc ezt írja ki:

RUN |1 API_TOKEN=supersecrettoken123 /bin/sh -c echo "hívom az apit..." && test -n "$API_TOKEN"
ARG API_TOKEN=supersecrettoken123

A token ott van, olvashatóan, mindenkinek, akinek hozzáférése van az image-hez – akár egy docker pull és egy docker history is elég. Ez nem új felismerés, a Dockerfile-referencia is külön figyelmeztet rá: „build argumentumokat nem érdemes titkok – felhasználói adatok, API tokenek – átadására használni, mert láthatók a docker history parancsban”.

A CI-pipeline még ennél is tovább adja

A dokumentáció ugyanitt egy kevésbé ismert részletet is megemlít: a build argumentumok „max módú provenance attesztációkban” is megjelennek, amiket a Buildx GitHub Action alapértelmezés szerint csatol az image-hez, ha a repository publikus. Ez azt jelenti, hogy nem is kell senkinek docker history-t futtatnia – a titok ott lóg a registry metaadatában, kereshetően, még akkor is, ha az image-et magát soha nem is húzza le senki.

A BuildKit ezt már saját maga is észreveszi. Amikor lefuttattam a fenti buildet, a kimenet végén ott volt egy figyelmeztetés:

1 warning found (use docker --debug to expand):
- SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "API_TOKEN") (line 2)

Ez a SecretsUsedInArgOrEnv build check, a BuildKit beépített lint szabályai közül – nem külön eszköz, nem CI plugin, hanem alapból bekapcsolva figyeli a nevekből ítélve gyanús ARG/ENV deklarációkat. Ha ezt eddig nem vetted észre, valószínűleg csak azért, mert a build sikeresen lefutott, és a figyelmeztetés belesüllyedt a többi sor közé.

A helyes eszköz: RUN --mount=type=secret

A BuildKit régóta kínál erre pontosan illő megoldást: a RUN --mount=type=secret mountot. A titkot nem build argumentumként adod át, hanem a build kliens tölti fel egy ideiglenes, csak az adott RUN lépés alatt létező fájlként vagy környezeti változóként – image rétegbe és build cache-be sem kerül bele.

Egy tipikus, valós eset: privát npm registry token kell az npm ci-hez, de a .npmrc-t nem akarod belesütni az image-be.

# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --omit=dev
COPY . .

A buildet a --secret kapcsolóval indítod, a titok forrása maradhat egy sima fájl a gépeden:

docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

Ezt magam is leteszteltem egy minimális image-en: a docker history --no-trunc a build után semmilyen formában nem tartalmazta a titkot, és az image exportált rétegeiben (docker save + kicsomagolás) sem volt megtalálható a string – pontosan azt kaptam, amit a dokumentáció ígér: a mount „lehetővé teszi, hogy a build konténer hozzáférjen titkos értékekhez anélkül, hogy azok bekerülnének az image-be vagy a build cache-be”.

Az opciók egy táblázatban:

  • id – a titok azonosítója, alapértelmezetten a target elérési út utolsó darabja
  • target – hova kerüljön fájlként; alapértelmezés /run/secrets/<id>
  • env – környezeti változóként is (vagy helyette) elérhetővé teszi, a Dockerfile 1.10.0 szintaxis óta (2024 szeptembere)
  • required – ha true, a build hibával áll le, ha a titok nincs átadva; alapértelmezés false
  • mode, uid, gid – a titokfájl jogosultságai, alapértelmezetten 0400, 0, 0

Az env opcióval ugyanazt a titkot környezeti változóként is megkapod, ami akkor jön jól, ha egy build lépés natívan env változóból várja a hitelesítést:

RUN --mount=type=secret,id=API_TOKEN,env=API_TOKEN \
    curl -sf -H "Authorization: Bearer $API_TOKEN" https://api.example.com/asset -o asset.json

Két dolog, amin könnyű elcsúszni

A required alapértéke csendben harap. Kipróbáltam: ha egy --mount=type=secret,id=hiányzó mountot úgy futtatsz le, hogy a --secret kapcsolóval nem adtad meg az adott azonosítót, a build nem hibázik – egyszerűen nem lesz ott a fájl a megadott target útvonalon, mintha sosem létezett volna. Ha a build lépésed emiatt csendben kihagy egy hitelesítést vagy egy telepítést, ezt semmi nem jelzi, csak az, hogy valami furán viselkedik a végeredményben. Ha a titok nélkül nem szabadna lefutnia a lépésnek, tedd oda a required=true-t.

A másik: a mount csak az adott RUN utasítás életciklusára létezik. Ha három különböző RUN-nak kell ugyanaz a titok, mindháromra ki kell írni a --mount-ot – nincs Dockerfile-szintű „állítsd be egyszer, használd mindenhol” mechanizmus, és ez direkt van így: pont ez korlátozza a titok életidejét a lehető legszűkebb ablakra.

Külön jó hír, hogy ehhez a legtöbb esetben semmi extra telepítést nem kell csinálni. A Docker Engine 23.0 (2023 februárja) óta a sima docker build Linuxon alapból a Buildx/BuildKit motort használja, tehát a --mount és a --secret a legtöbb, 2023 utáni Docker-telepítésen írás nélkül is működik – nem kell külön docker buildx build-ot hívnod. Amit viszont érdemes mindig kiírni, az a Dockerfile elejére kerülő # syntax=docker/dockerfile:1 sor: ez rögzíti, hogy a Dockerfile-frontendnek a mindenkori legfrissebb, karbantartott verzióját kapod, nem azt, ami épp a telepített Docker-verzióba van beégetve.

A tanulság nem az, hogy az ARG rossz eszköz – build-time konfigurációra (verziószám, célarchitektúra, feature flag) pontosan arra való. Csak arra nem, amire a leggyakrabban mégis használják: hitelesítési adatra. Ott a RUN --mount=type=secret nem egy plusz lépés a folyamatban, hanem az egyetlen mód, ami a titkot tényleg csak addig tartja életben, amíg szükség van rá.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük