GitOps ~4 Min.

Kaniko und rootless Image Builds

Images bauen ohne Docker Socket auf Shared Nodes.

von PlatPrep

Docker-in-Docker hat lange funktioniert. Du mountest den Docker-Socket in den CI-Container, kannst Images bauen, alles läuft. Bis jemand fragt: Was kann ein Container, der den Docker-Socket gemountet hat, auf dem Host-System machen?

Die Antwort: fast alles. Wer mit dem Socket reden darf, kann einen privilegierten Container mit gemountetem Host-Filesystem starten — effektiv root auf dem Node. Auf einem Shared-CI-Cluster, auf dem auch andere Workloads laufen, heißt das: Jede Pipeline, die ein Image baut, ist nur einen kompromittierten Build-Step von der Übernahme des Nodes entfernt. Das ist das Problem mit DinD. Nicht dass es nie funktioniert hat, sondern dass es ein Privilege-Escalation-Vektor ist, der in Multi-Tenant-Clustern nichts verloren hat.

Wie Kaniko ohne Daemon baut

Kaniko löst das, indem es gar keine Container-Runtime braucht. Der Executor liest das Dockerfile, zieht das Base-Image, entpackt dessen Filesystem in seinen eigenen Container und führt die Build-Anweisungen direkt im Userspace aus. Nach jedem Befehl macht Kaniko einen Filesystem-Snapshot, vergleicht ihn mit dem Stand davor und schreibt die Differenz als neuen Layer. Am Ende wird das fertige Image direkt in die Registry gepusht. Kein Docker-Daemon, kein privilegierter Container, kein Socket-Mount.

Ein minimaler Kubernetes-Job sieht so aus:

containers:
  - name: build
    image: gcr.io/kaniko-project/executor:latest
    args:
      - "--context=git://git.example.com/team/app.git#refs/heads/main"
      - "--destination=harbor.example.com/team/app:1.4.2"
      - "--cache=true"
      - "--cache-repo=harbor.example.com/team/cache"
    volumeMounts:
      - name: registry-creds
        mountPath: /kaniko/.docker

Die Registry-Credentials kommen als Docker-Config-Secret in den Pod — idealerweise von einem Harbor Robot Account mit Push-Rechten nur auf das eigene Projekt.

Caching: der Punkt, an dem es langsam oder schnell wird

Ohne Caching baut Kaniko jedes Mal von Scratch — bei einem Node- oder Java-Projekt mit großem Dependency-Layer heißt das schnell mehrere Minuten pro Build nur für npm ci oder den Maven-Download. Mit --cache=true und einem Cache-Repository in der Registry legt Kaniko für jeden Layer einen Cache-Eintrag ab, der über den Hash der Build-Anweisung und der Eingabedateien adressiert wird. Der erste Lauf wärmt den Cache auf, danach sind unveränderte Layer Sekundensache.

Damit das wirkt, gilt dieselbe Regel wie bei Docker: Dockerfile-Anweisungen nach Änderungshäufigkeit ordnen. Erst COPY package.json, dann RUN npm ci, erst danach den Rest des Quellcodes kopieren — sonst invalidiert jeder Commit den teuren Dependency-Layer. Auch das Snapshotting selbst kostet: Bei Images mit sehr vielen Dateien kann der Vergleich nach jedem Build-Step spürbar Zeit fressen. Das ist Kanikos strukturelle Schwäche gegenüber daemon-basierten Buildern.

Zwei weitere Praxis-Punkte: Der Build-Kontext muss nicht aus Git kommen — Kaniko kann ihn auch aus einem S3-Bucket oder einem gemounteten Volume lesen, was in Pipelines praktisch ist, die den Checkout in einem vorherigen Step erledigen. Und die Reproduzierbarkeit hat Grenzen: Wer in Builds apt-get install ohne Versionspinning aufruft, bekommt heute und in drei Monaten verschiedene Images mit demselben Tag — das ist kein Kaniko-Problem, fällt aber genau dann auf, wenn der Registry-Cache ein altes Layer-Ergebnis ausliefert und niemand versteht, warum.

Multi-Stage-Builds funktionieren in Kaniko problemlos — wichtig, weil sie Standard für produktive Images sind: Compiler und Build-Tools bleiben in der Build-Stage und landen nicht im finalen Image, das macht es kleiner und reduziert die Angriffsfläche gleich mit.

Der Elefant im Raum: Wartungsstatus

Ehrlichkeit gehört dazu: Google hat das Kaniko-Repository 2024 archiviert. Das Tool funktioniert weiter und läuft in unzähligen Pipelines, aber es gibt vom ursprünglichen Maintainer keine neuen Features und keine garantierten Security-Fixes mehr; es existieren Community-Forks. Für ein Werkzeug, das in der Mitte deiner Supply Chain sitzt, ist das ein relevanter Faktor — wer heute neu aufsetzt, sollte Alternativen ernsthaft prüfen:

BuildKit (der Builder hinter modernem docker build) läuft auch rootless als Deployment im Cluster, parallelisiert unabhängige Build-Stages und hat das ausgereiftere Cache-Modell (Registry-Cache, Cache-Mounts für Package-Manager). Die rootless-Konfiguration ist etwas anspruchsvoller — je nach Kernel- und Node-Setup braucht es User-Namespaces und ggf. Anpassungen am seccomp/AppArmor-Profil. Buildah baut ebenfalls daemonlos und rootless und ist im Red-Hat-Umfeld der Standard. Beide werden aktiv gepflegt.

Der Rest des Setups bleibt bei allen drei gleich: ein unprivilegierter Pod, ein Robot-Account-Secret, ein Push in die Registry — und damit kompatibel mit Pod Security Standards im restricted-Profil, was für DinD schlicht unmöglich ist. In Tekton-Pipelines ist so ein Build-Task ein Standardbaustein; wie sich das im Vergleich zu gehosteten Runnern einordnet, steht in Tekton vs. GitHub Actions.

Rootless Image Builds sind kein Nice-to-have mehr in sicherheitsbewussten Clustern. Sie sind Standard — und im Interview ist die Frage “warum ist der Docker-Socket im CI-Container ein Problem?” ein beliebter Einstieg, auf den “effektiv root auf dem Host, deshalb bauen wir daemonlos und unprivilegiert” die Antwort ist, die hängen bleibt. Mehr zum sauberen Build-Setup findest du im Lernpfad CI/CD & Builds.