Rootless buildah als GitLab CI Build-Host - Debian 13 (POC)#
Ziel#
Container-Images aus der GitLab CI/CD Pipeline bauen und in die GitLab Container Registry pushen, ohne Docker-Daemon und ohne root im Build-Pfad. Runner-Prozess und alle Builds laufen als dedizierter, unprivilegierter gitlab-runner User (Shell-Executor + rootless buildah).
Architektur#
Shell-Executor + buildah rootless direkt auf dem Host.
- Kein Docker-Daemon, kein
docker.sock, kein privileged Container. - buildah läuft daemonless und rootless, per User-Namespace (subuid/subgid) gemappt.
- AppArmor bleibt aktiv.
Bewusst nicht gewählt: buildah-in-container über den Docker-Executor. Das hätte für rootless-in-container in der Praxis einen privileged Container oder aufgeweichtes seccomp/AppArmor erfordert. Preis des Shell-Executors: er isoliert Jobs nicht voneinander, das fängt eine dedizierte Build-VM plus concurrent = 1 ab.
Voraussetzungen#
- Debian 13 (Trixie) VM, dediziert als Build-Host.
- Unprivilegierte User-Namespaces sind auf Debian 13 per default offen, kein Freischalten nötig (Kontrolle in Schritt 3). Debian übernimmt bewusst nicht das restriktive AppArmor-userns-Modell von Ubuntu, der Ubuntu-Schalter
kernel.apparmor_restrict_unprivileged_usernsgreift hier also nicht. - Kernel aktuell halten: Weil userns offen ist, gehört ein gepatchter Kernel auf den Build-Host (
unattended-upgradesmit Security-Updates aktiv lassen). Trixie ist gegen die userns-LPE CVE-2026-46331 über den Security-Channel gefixt, Debian 11/12 waren zum Berichtszeitpunkt noch verwundbar. - GitLab-URL und ein Runner-Authentication-Token (
glrt-...), erstellen.
Platzhalter in diesem Dokument: https://gitlab.example.com und glrt-XXXX durch die echten Werte ersetzen.
Hinweis zu den Datei-Schritten: Alle Dateien unter /home/gitlab-runner/... werden als der User gitlab-runner angelegt, damit die Rechte für rootless stimmen. Also jeweils per sudo -u gitlab-runner vim <pfad> editieren, nicht als root.
1. Pakete installieren#
uidmap liefert newuidmap/newgidmap und ist auf Debian ein eigenes Paket, das nicht zuverlässig als Dependency gezogen wird. Ohne das kein rootless. passt liefert pasta, das rootless-Netzwerk-Backend, das buildah/podman unter Trixie (podman 5.x) per default nutzen; fehlt es, bricht schon der erste RUN-Schritt im Build mit “could not find pasta” ab. libcap2-bin liefert getcap für den Capability-Check in Schritt 3.
sudo apt update
sudo apt install -y buildah podman fuse-overlayfs passt uidmap libcap2-bin git
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner2. System-Unit des Runners deaktivieren#
Wir fahren einen rootless User-Service, nicht den mitgelieferten System-Service. Das deb-Paket ruft im postinst gitlab-runner install auf und legt eine echte Unit-Datei an, daher scheitert mask zunächst mit “File already exists”. Deshalb erst entfernen, dann maskieren:
sudo systemctl disable --now gitlab-runner.service
sudo rm -f /etc/systemd/system/gitlab-runner.service
sudo systemctl daemon-reload
sudo systemctl mask gitlab-runner.serviceKontrolle: systemctl is-enabled gitlab-runner.service muss masked sagen. Nach einem apt upgrade gitlab-runner kurz erneut prüfen, da das Post-Install-Script die Unit theoretisch wieder anlegen kann.
3. Rootless-Grundlagen für den Runner-User#
Subuid/subgid setzen (der gitlab-runner wird als System-User angelegt und bekommt auf Debian keine Ranges automatisch):
grep gitlab-runner /etc/subuid /etc/subgid || {
echo "gitlab-runner:100000:65536" | sudo tee -a /etc/subuid
echo "gitlab-runner:100000:65536" | sudo tee -a /etc/subgid
}Unprivilegierte User-Namespaces prüfen. sysctl und getcap liegen unter /usr/sbin und sind im PATH eines normalen Users nicht enthalten, daher mit sudo (oder vollem Pfad) aufrufen, sonst kommt “command not found”:
sudo sysctl kernel.unprivileged_userns_cloneAuf Standard-Trixie sind userns per default offen, erwartet wird 1. Meldet der Kernel stattdessen “No such file or directory”, ist der Key in diesem Kernel schlicht nicht exponiert; das ist unkritisch, da userns ohnehin offen sind, dann diesen Check überspringen. Nur falls der Wert explizit 0 ist (gehärtetes Image), dauerhaft aktivieren:
echo "kernel.unprivileged_userns_clone=1" | sudo tee /etc/sysctl.d/99-userns.conf
sudo sysctl --systemCapabilities von newuidmap/newgidmap prüfen (erwartet: cap_setuid=ep bzw. cap_setgid=ep; falls leer: sudo apt install --reinstall uidmap):
sudo getcap /usr/bin/newuidmap /usr/bin/newgidmapLinger aktivieren, damit /run/user/<uid> und der User-systemd-Manager dauerhaft und ohne Login existieren.
sudo loginctl enable-linger gitlab-runner4. Storage auf fuse-overlayfs festnageln#
fuse-overlayfs explizit setzen, damit der Storage-Treiber unabhängig von Kernel- und Distro-Defaults reproduzierbar bleibt.
Verzeichnis anlegen:
sudo -u gitlab-runner mkdir -p /home/gitlab-runner/.config/containersDatei /home/gitlab-runner/.config/containers/storage.conf als gitlab-runner anlegen (sudo -u gitlab-runner vim /home/gitlab-runner/.config/containers/storage.conf) mit folgendem Inhalt:
[storage]
driver = "overlay"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"5. Smoke-Test#
buildah rootless braucht ein korrektes HOME (findet sonst die storage.conf nicht) und XDG_RUNTIME_DIR.
Test-Verzeichnis anlegen:
sudo -u gitlab-runner mkdir -p /home/gitlab-runner/smoketestDatei /home/gitlab-runner/smoketest/Containerfile als gitlab-runner anlegen (sudo -u gitlab-runner vim /home/gitlab-runner/smoketest/Containerfile) mit folgendem Inhalt:
FROM registry.access.redhat.com/ubi9/ubi-minimal
RUN echo hello-rootlessBuild ausführen:
RUNNER_UID=$(id -u gitlab-runner)
sudo -u gitlab-runner env HOME=/home/gitlab-runner XDG_RUNTIME_DIR=/run/user/$RUNNER_UID \
buildah build -t smoketest:local /home/gitlab-runner/smoketest
sudo -u gitlab-runner env HOME=/home/gitlab-runner buildah imagesVerifizieren:
sudo -u gitlab-runner env HOME=/home/gitlab-runner buildah infoErwartet: store.graphDriverName: overlay und host.security.rootless: true. Danach kann das Test-Verzeichnis wieder weg: sudo -u gitlab-runner rm -rf /home/gitlab-runner/smoketest.
6. Runner registrieren#
Wichtig beim neuen Authentication-Token-Workflow: --tag-list, --locked, --run-untagged usw. sind serverseitig und dürfen beim register nicht mehr angegeben werden (sonst FATAL). Tags werden in der GitLab-UI am Runner gesetzt.
sudo -iu gitlab-runner gitlab-runner register \
--non-interactive \
--url "https://gitlab.example.com" \
--token "glrt-XXXX" \
--executor "shell" \
--description "rootless-buildah-debian13"Tag am Runner in der UI setzen (Settings > CI/CD > Runners > Runner öffnen > Tags), passend zum tags:-Block der Pipeline, hier rootless-buildah. Ohne passendes Tag bleibt der Job auf pending.
concurrent = 1 erzwingen, damit parallele Jobs sich nicht Cache/Workspace teilen, und verifizieren:
sudo -iu gitlab-runner sed -i 's/^concurrent = .*/concurrent = 1/' ~/.gitlab-runner/config.toml
sudo -iu gitlab-runner gitlab-runner verify7. Runner als User-systemd-Service#
Verzeichnis anlegen:
sudo -u gitlab-runner mkdir -p /home/gitlab-runner/.config/systemd/userDatei /home/gitlab-runner/.config/systemd/user/gitlab-runner.service als gitlab-runner anlegen (sudo -u gitlab-runner vim /home/gitlab-runner/.config/systemd/user/gitlab-runner.service) mit folgendem Inhalt:
[Unit]
Description=GitLab Runner (rootless)
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/gitlab-runner run --working-directory %h --config %h/.gitlab-runner/config.toml
Restart=always
RestartSec=5
[Install]
WantedBy=default.targetAktivieren und starten:
RUNNER_UID=$(id -u gitlab-runner)
sudo -u gitlab-runner env XDG_RUNTIME_DIR=/run/user/$RUNNER_UID systemctl --user daemon-reload
sudo -u gitlab-runner env XDG_RUNTIME_DIR=/run/user/$RUNNER_UID systemctl --user enable --now gitlab-runner
sudo -u gitlab-runner env XDG_RUNTIME_DIR=/run/user/$RUNNER_UID systemctl --user status gitlab-runner8. Pipeline (Push in die GitLab Container Registry)#
Datei .gitlab-ci.yml im Repo anlegen. CI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD (= CI_JOB_TOKEN) und CI_REGISTRY_IMAGE liefert GitLab automatisch.
stages:
- build
build-image:
stage: build
tags:
- rootless-buildah
variables:
STORAGE_DRIVER: overlay
BUILDAH_FORMAT: docker
before_script:
- export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
- buildah login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
script:
- buildah build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- buildah push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
buildah tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:latest"
buildah push "$CI_REGISTRY_IMAGE:latest"
fi
after_script:
- buildah logout "$CI_REGISTRY" || trueEs muss eine Build-Datei (Containerfile oder Dockerfile) im Kontext liegen, sonst bricht der Build mit “cannot find Containerfile or Dockerfile” ab. Liegt die Datei woanders, per -f und Kontextpfad angeben, z. B. buildah build -f docker/Containerfile -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" docker/.
Debian-Besonderheit bei Kurznamen: Anders als Rocky liefert Debian keine unqualified-search-registries in /etc/containers/registries.conf. Ein FROM alpine bricht daher mit “short-name resolution” ab. Entweder in den Containerfiles voll qualifizierte Namen verwenden (FROM docker.io/library/alpine), oder systemweit ergänzen. Datei /etc/containers/registries.conf als root editieren (sudo vim /etc/containers/registries.conf) und folgende Zeile ergänzen:
unqualified-search-registries = ["docker.io"]9. Wartung: Image-Store aufräumen#
Der Store unter /home/gitlab-runner/.local/share/containers wächst mit jedem Build. Manuell:
sudo -iu gitlab-runner buildah rmi --pruneAutomatisiert als User-Timer. Datei /home/gitlab-runner/.config/systemd/user/buildah-prune.service als gitlab-runner anlegen (sudo -u gitlab-runner vim /home/gitlab-runner/.config/systemd/user/buildah-prune.service) mit folgendem Inhalt:
[Unit]
Description=Prune dangling buildah images
[Service]
Type=oneshot
Environment=HOME=%h
ExecStart=/usr/bin/buildah rmi --pruneDatei /home/gitlab-runner/.config/systemd/user/buildah-prune.timer als gitlab-runner anlegen (sudo -u gitlab-runner vim /home/gitlab-runner/.config/systemd/user/buildah-prune.timer) mit folgendem Inhalt:
[Unit]
Description=Weekly buildah image prune
[Timer]
OnCalendar=weekly
Persistent=true
[Install]
WantedBy=timers.targetTimer aktivieren:
RUNNER_UID=$(id -u gitlab-runner)
sudo -u gitlab-runner env XDG_RUNTIME_DIR=/run/user/$RUNNER_UID systemctl --user daemon-reload
sudo -u gitlab-runner env XDG_RUNTIME_DIR=/run/user/$RUNNER_UID systemctl --user enable --now buildah-prune.timerBei knappem /home kann graphroot in der storage.conf auf ein größeres Volume gelegt werden.
10. Reboot-Test#
Nach einem Neustart prüfen, ob der Runner durch Linger von allein hochkommt:
RUNNER_UID=$(id -u gitlab-runner)
sudo -u gitlab-runner env XDG_RUNTIME_DIR=/run/user/$RUNNER_UID systemctl --user status gitlab-runner