kubepath
The change path hop 9 of 10 ordinary Kubernetes

BuildKit, rootless and tokenless

A Dockerfile is a program a tenant wrote. The Job is built so that running it grants nothing.

The push landed; something has to turn it into an image. BuildKit does, rootless and daemonless, inside a Kubernetes Job — one Job per build, no long-lived builder, no Docker socket anywhere.

The premise. A Dockerfile is a program, written by a tenant, that this cluster is about to execute. Every decision in the Job spec follows from taking that sentence literally.

No ServiceAccount token. automountServiceAccountToken: false is the highest-value line in the file. Without it, a projected token for the Job’s ServiceAccount is mounted at a well-known path, and a RUN step reads it. Even a token with almost no permissions is an authenticated identity inside the cluster and a starting point; the default token also makes the API server reachable and enumerable. Setting this to false means a build has no cluster identity at all — there is nothing at the path to steal.

backoffLimit: 0. One attempt, one verdict. Retrying a build that failed is either pointless, because the Dockerfile is wrong, or actively harmful, because it hides a Dockerfile that is flaky — one that pulls an unpinned dependency, or races on a network fetch — behind an eventual green. It also bounds cost: a retry loop on a build that fails at minute 25 is expensive on a single-node cluster.

Rootless, and under gVisor. No privileged container and no host mounts; the build runs under the same RuntimeClass as any other user code from hop 6. That combination has sharp edges worth warning a class about in advance rather than during the exercise: rootless BuildKit needs user namespaces available on the node, and a few build patterns — RUN --mount=type=bind in some configurations, anything that shells out to mount, builds that expect /proc/sys to be writable — fail under runsc in ways whose error messages do not say "gVisor". An hour disappears there if nobody has said the word first.

apps/builder/job.yaml — executing a tenant’s program
apiVersion: batch/v1
kind: Job
spec:
  backoffLimit: 0                        # one attempt. a retry hides flake.
  activeDeadlineSeconds: 1800
  template:
    spec:
      runtimeClassName: gvisor           # same sandbox as any user code
      automountServiceAccountToken: false  # <- the line that matters most
      restartPolicy: Never
      containers:
        - name: buildkit
          image: moby/buildkit:v0.27.0-rootless@sha256:9c2b...
          command: [buildctl-daemonless.sh]
          args:
            - build
            - --frontend=dockerfile.v0
            - --local=context=/src
            - --local=dockerfile=/src
            - --output=type=image,name=$(IMAGE),push=true
          securityContext:
            runAsUser: 1000
            allowPrivilegeEscalation: false
            seccompProfile: { type: RuntimeDefault }
            capabilities: { drop: [ALL] }

Check yourself

2 questions. One attempt each is recorded; the explanation is the point, not the score.

Why is automountServiceAccountToken: false the most important line in the build Job?

What is the argument for backoffLimit: 0?