--- # buildkitd, ROOTLESS. The builder behind .gitea/workflows/images.yaml; see README.md. # # THE SECURITY CONTEXT IS THE BOUNDARY, AND EVERY LINE OF IT WAS MEASURED (2026-10-29, the pinned # image under podman with the same restrictions; decisions/2026-10-29-image-pipeline.md): # * privileged: false — never. This is the whole point of the operator's decision on PR #81: # the alternative was handing CI the privileged DinD daemon, i.e. root on the node. # * runAsUser/Group 1000 — the image's `user`; rootlesskit maps it to uid 0 INSIDE a user # namespace only. The nodes allow that (measured: kernel.unprivileged_userns_clone = 1, # user.max_user_namespaces = 127206 on all three Debian 13 nodes). # * capabilities: drop ALL, add SETUID + SETGID — the image's newuidmap/newgidmap are NOT # setuid; they carry FILE capabilities, which need SETUID/SETGID in the bounding set. # Measured: with drop ALL and nothing added, rootlesskit fails "newuidmap ... operation not # permitted" and the daemon never starts. # * allowPrivilegeEscalation: true — required for the same reason: no_new_privs (what `false` # sets) stops a file capability from ever taking effect. It does NOT grant privileges the # bounding set above does not contain. # * seccompProfile: Unconfined — measured: with the default seccomp profile the daemon starts # but every RUN step fails (the runtime cannot create the build step's namespaces/mounts). # * appArmorProfile: Unconfined — upstream's documented requirement (buildkit # examples/kubernetes, *.rootless.yaml); the nodes run AppArmor (enabled=Y) and containerd's # default profile denies mount. NOT measured here: the test host runs SELinux, not AppArmor. # * readOnlyRootFilesystem: true — measured to work with the five emptyDirs below. # # --oci-worker-no-process-sandbox: build steps share the daemon's PID namespace instead of getting # their own (creating one needs an unmasked /proc, i.e. more privilege). Consequence, stated by # buildkitd itself at startup: a build step can signal or ptrace the daemon and read what the # daemon can read — including this pod's TLS key and the build cache. So a build step is trusted # at the level of "may impersonate this builder", which is the level anyone holding the client # certificate already has. It can NOT reach the node: everything still runs as uid 1000 in a user # namespace, unprivileged, with no host mounts. apiVersion: apps/v1 kind: Deployment metadata: name: buildkitd namespace: buildkit labels: app.kubernetes.io/name: buildkitd spec: replicas: 1 strategy: type: Recreate # one builder, one cache; never two daemons fighting over a node's disk selector: matchLabels: app.kubernetes.io/name: buildkitd template: metadata: labels: app.kubernetes.io/name: buildkitd spec: automountServiceAccountToken: false # the builder needs no Kubernetes API at all enableServiceLinks: false securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 # makes the 0440 TLS Secret below readable by uid 1000, and nobody else containers: - name: buildkitd # Pinned by digest (the multi-arch index). The kubernetes manager in renovate.json5 # tracks platform/**, so Renovate proposes bumps; the CI client (buildx, pinned in # versions.env) talks to any recent daemon. image: docker.io/moby/buildkit:v0.34.2-rootless@sha256:c8252268760e1baba80780d63d738ef9f260fb7c7c0973c78494cb9ddd99eda1 args: - --addr - unix:///run/user/1000/buildkit/buildkitd.sock # local only: the probes below - --addr - tcp://0.0.0.0:1234 # CI, mTLS only - --tlscacert - /certs/ca.crt # client certs are VERIFIED against this CA: no cert, no build - --tlscert - /certs/tls.crt - --tlskey - /certs/tls.key - --config - /etc/buildkit/buildkitd.toml - --oci-worker-no-process-sandbox ports: - name: buildkit containerPort: 1234 protocol: TCP readinessProbe: exec: command: ["buildctl", "debug", "workers"] initialDelaySeconds: 5 periodSeconds: 30 livenessProbe: exec: command: ["buildctl", "debug", "workers"] initialDelaySeconds: 30 periodSeconds: 60 timeoutSeconds: 10 securityContext: privileged: false runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 allowPrivilegeEscalation: true readOnlyRootFilesystem: true capabilities: drop: ["ALL"] add: ["SETUID", "SETGID"] seccompProfile: type: Unconfined appArmorProfile: type: Unconfined resources: # Build steps (dnf, npm, pip) run INSIDE this container's cgroup, so the limit covers # the daemon and the build together. requests: cpu: 250m memory: 512Mi ephemeral-storage: 2Gi limits: memory: 4Gi volumeMounts: - { name: certs, mountPath: /certs, readOnly: true } - { name: config, mountPath: /etc/buildkit, readOnly: true } - { name: cache, mountPath: /home/user/.local/share/buildkit } - { name: run, mountPath: /run/user } - { name: tmpdir, mountPath: /home/user/.local/tmp } # the image's TMPDIR - { name: tmp, mountPath: /tmp } - { name: clicfg, mountPath: /home/user/.docker } # buildctl (the probes) writes here volumes: - name: certs secret: secretName: buildkitd-tls # cert-manager: ca.crt + tls.crt + tls.key defaultMode: 0440 - name: config configMap: name: buildkitd-config - name: cache emptyDir: sizeLimit: 6Gi # GC caps use at 4GB (configmap.yaml); this is the backstop - name: run emptyDir: {} - name: tmpdir emptyDir: {} - name: tmp emptyDir: {} - name: clicfg emptyDir: {}