# DevOps

# 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_userns` greift hier also nicht.
- Kernel aktuell halten: Weil userns offen ist, gehört ein gepatchter Kernel auf den Build-Host (`unattended-upgrades` mit 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.

```bash
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-runner

```

## 2. 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:

```bash
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.service

```

Kontrolle: `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):

```bash
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":

```bash
sudo sysctl kernel.unprivileged_userns_clone

```

Auf 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:

```bash
echo "kernel.unprivileged_userns_clone=1" | sudo tee /etc/sysctl.d/99-userns.conf
sudo sysctl --system

```

Capabilities von newuidmap/newgidmap prüfen (erwartet: `cap_setuid=ep` bzw. `cap_setgid=ep`; falls leer: `sudo apt install --reinstall uidmap`):

```bash
sudo getcap /usr/bin/newuidmap /usr/bin/newgidmap

```

Linger aktivieren, damit `/run/user/<uid>` und der User-systemd-Manager dauerhaft und ohne Login existieren.

```bash
sudo loginctl enable-linger gitlab-runner

```

## 4. Storage auf fuse-overlayfs festnageln

fuse-overlayfs explizit setzen, damit der Storage-Treiber unabhängig von Kernel- und Distro-Defaults reproduzierbar bleibt.

Verzeichnis anlegen:

```bash
sudo -u gitlab-runner mkdir -p /home/gitlab-runner/.config/containers

```

Datei `/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:

```ini
[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:

```bash
sudo -u gitlab-runner mkdir -p /home/gitlab-runner/smoketest

```

Datei `/home/gitlab-runner/smoketest/Containerfile` als `gitlab-runner` anlegen (`sudo -u gitlab-runner vim /home/gitlab-runner/smoketest/Containerfile`) mit folgendem Inhalt:

```dockerfile
FROM registry.access.redhat.com/ubi9/ubi-minimal
RUN echo hello-rootless

```

Build ausführen:

```bash
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 images

```

Verifizieren:

```bash
sudo -u gitlab-runner env HOME=/home/gitlab-runner buildah info

```

Erwartet: `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.

```bash
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 &gt; CI/CD &gt; Runners &gt; Runner öffnen &gt; 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:

```bash
sudo -iu gitlab-runner sed -i 's/^concurrent = .*/concurrent = 1/' ~/.gitlab-runner/config.toml
sudo -iu gitlab-runner gitlab-runner verify

```

## 7. Runner als User-systemd-Service

Verzeichnis anlegen:

```bash
sudo -u gitlab-runner mkdir -p /home/gitlab-runner/.config/systemd/user

```

Datei `/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:

```ini
[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.target

```

Aktivieren und starten:

```bash
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-runner

```

## 8. 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.

```yaml
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" || true

```

Es 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:

```ini
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:

```bash
sudo -iu gitlab-runner buildah rmi --prune

```

Automatisiert 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:

```ini
[Unit]
Description=Prune dangling buildah images

[Service]
Type=oneshot
Environment=HOME=%h
ExecStart=/usr/bin/buildah rmi --prune

```

Datei `/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:

```ini
[Unit]
Description=Weekly buildah image prune

[Timer]
OnCalendar=weekly
Persistent=true

[Install]
WantedBy=timers.target

```

Timer aktivieren:

```bash
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.timer

```

Bei 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:

```bash
RUNNER_UID=$(id -u gitlab-runner)
sudo -u gitlab-runner env XDG_RUNTIME_DIR=/run/user/$RUNNER_UID systemctl --user status gitlab-runner

```