Container image builds in 2026: revisit BuildKit, Buildah and kaniko before you change your CI builder

Picture a payments software company in Pune (an invented example, not a real client). Since 2021 its GitLab runners on Kubernetes have built every service image with gcr.io/kaniko-project/executor, pinned to a tag nobody has touched in two years. Last month the security team’s scanner flagged the executor image itself, and someone found that the upstream repository is archived. The platform lead wants to move to BuildKit, a senior developer who uses Podman on his laptop wants Buildah, and the architect asks whether the team can just switch to a kaniko fork and change one line.

All three are fair suggestions, and none of them is only a tool swap. A CI image builder decides what privileges your build pods need, where the cache lives, whether the same commit gives the same digest twice, what evidence travels with the image, and whose security advisories you must follow. This post walks through those decisions for BuildKit (with Docker Buildx), Buildah (and Podman, which uses it), and the two maintained kaniko forks, with short notes on ko, Jib, Cloud Native Buildpacks and apko. Facts come from GitHub release pages, official docs, licence files and CVE records, checked on 11 October 2026. The drill near the end is my own run.

The short version

  • Google’s GoogleContainerTools/kaniko repository is archived; its last release was v1.24.0 on 23 May 2025. Two forks carry it on. Chainguard’s fork (now at chainguard-forks/kaniko, v1.25.19 on 27 August 2026) does maintenance only and publishes source tags, not images. The osscontainertools/kaniko fork (v1.28.5 on 20 September 2026) publishes free images and is still adding features.
  • BuildKit 0.34.0 and Buildx 0.38.0 came out on 8 October 2026. BuildKit 0.33.1 (30 September) fixed ten advisories, including CVE-2026-93318, a cache poisoning bug that matters most on shared, long-lived build daemons.
  • Buildah 1.45.1 and Podman 6.1.3 are current. The projects moved from the containers GitHub organisation to podman-container-tools in June 2026 as part of their CNCF move, so old URLs and import paths now redirect.
  • “Rootless” is not a single setting. On my VM, without newuidmap and subordinate IDs, rootless BuildKit started but could not unpack an Alpine or BusyBox base image or start a RUN step. In Kubernetes, rootless BuildKit still needs seccomp and AppArmor set to Unconfined.
  • Reproducible digests need two things in BuildKit: SOURCE_DATE_EPOCH and the rewrite-timestamp=true exporter option. In my drill, SOURCE_DATE_EPOCH alone fixed the config dates but still gave a new digest on every run; with both, three builds and a touch of every input gave the same digest.
  • Provenance makes the image index differ between identical builds, because the attestation records build times and an invocation ID. Compare platform manifest digests, not index digests, when you check reproducibility.

Where things stand on 11 October 2026

Dates are from GitHub release pages, converted to IST.

Project

Latest release

What it is

Licence

BuildKit

0.34.0 on 8 October 2026 (0.33.1 on 30 September); Dockerfile frontend 1.28.0 on 8 October

Build engine (buildkitd and buildctl) behind docker build

Apache 2.0

Docker Buildx

0.38.0 on 8 October 2026 (0.37.2 on 30 September)

Docker CLI plugin that drives BuildKit, including Bake

Apache 2.0

Buildah

1.45.1 on 15 September 2026; 1.43.4 on the same day

Daemonless OCI image builder from the Podman family

Apache 2.0; CNCF Sandbox as part of Podman Container Tools

Podman

6.1.3 and 5.8.8 on 29 September 2026

Daemonless container engine; podman build uses Buildah

Apache 2.0

kaniko (Google)

1.24.0 on 23 May 2025; repository archived in June 2025

Builds Dockerfiles inside an unprivileged container

Apache 2.0

kaniko (Chainguard fork)

1.25.19 on 27 August 2026

Maintenance fork; source tags only, images for Chainguard customers

Apache 2.0

kaniko (osscontainertools fork)

1.28.5 on 20 September 2026

Community fork with public images on GHCR and Docker Hub

Apache 2.0

ko

0.19.1 on 29 June 2026

Builds Go programs into images without a Dockerfile

Apache 2.0; CNCF Sandbox

Jib

Maven plugin 3.5.2, Gradle plugin 3.5.4 and core 0.28.2 on 15 July 2026

Builds Java images from Maven or Gradle without a daemon

Apache 2.0

Cloud Native Buildpacks (pack)

0.40.9 on 9 August 2026

Builds images from source using buildpacks

Apache 2.0; CNCF Graduated

apko

1.4.10 on 10 October 2026

Builds images from APK packages with a YAML file

Apache 2.0

Date

Event

26 January 2017

Buildah repository created at Red Hat

31 May 2017

BuildKit repository created at Docker

April 2018

Google announces kaniko for building images in Kubernetes without privileges

3 October 2018

Cloud Native Buildpacks accepted into CNCF

14 December 2022

ko accepted into CNCF at Sandbox level

1 February 2023

Docker Engine 23.0 makes Buildx and BuildKit the default builder on Linux

1 February 2024

BuildKit 0.12.5 fixes CVE-2024-23651, CVE-2024-23652 and CVE-2024-23653

21 January 2025

Podman Container Tools (Podman, Buildah, Skopeo) accepted into CNCF at Sandbox level

23 May 2025

kaniko 1.24.0, Google’s last release

June 2025

Google archives kaniko; Chainguard announces its fork on 5 June

7 November 2025

runc container escape fixes, including CVE-2025-31133, CVE-2025-52565 and CVE-2025-52881

June 2026

Podman, Buildah and Skopeo move to the podman-container-tools GitHub organisation

24 June 2026

Podman 6.0.0, with a new Go import path and many removals

17 July 2026

Cloud Native Buildpacks reaches CNCF Graduated level

29 July 2026

BuildKit 0.32.0: attestations default to OCI artifact descriptors

2 September 2026

BuildKit 0.33.0: rootless output made compatible with rootful output for reproducibility

30 September 2026

BuildKit 0.33.1 fixes ten advisories; Buildx 0.37.2 fixes two Bake advisories

8 October 2026

BuildKit 0.34.0 and Buildx 0.38.0

What actually happens when CI builds an image

Every builder in this post does the same five jobs, and most surprises come from one of them.

  1. Gets the inputs. The build context, the Dockerfile and the base images. This is where secrets, .dockerignore mistakes and untrusted Git or tar sources come in.
  2. Runs build steps. Each RUN needs some kind of container: a full runc sandbox, a chroot, or, in kaniko’s case, the builder’s own container filesystem. This decides your privilege needs.
  3. Snapshots layers. The builder turns filesystem changes into tar layers. File times, owners and the order of entries all end up in the layer digest.
  4. Uses and stores cache. Local state, a registry, S3 or a CI cache. A cache that many teams share is also a place where one build can affect another.
  5. Exports the result. Manifests, an index for multi-arch, and attestations such as an SBOM and SLSA provenance, pushed to a registry.

A builder swap changes all five.

Design: daemon, daemonless and rootless

BuildKit: a build daemon with a client

BuildKit is a long-running daemon, buildkitd, that turns a build definition (LLB) into a content-addressed graph and runs it with high parallelism. Dockerfiles reach it through a frontend; the Dockerfile frontend is versioned separately, which is why you see both v0.34.0 and dockerfile/1.28.0 on the release page. You talk to it with buildctl, with docker buildx, or through tools that embed its client. Docker Engine has used it as the default builder on Linux since 23.0 in February 2023.

A daemon brings real benefits for CI: a warm local cache, shared work between parallel builds, and remote drivers, so a CI job can send work to a BuildKit service in the cluster instead of building in its own pod. It also brings the usual daemon questions. Who can reach its socket? Whose builds share its cache? How do you restart it when a fix lands? BuildKit 0.33.1’s advisories were mostly about malicious frontends, malformed requests and untrusted images crashing or confusing a shared daemon. That matters much more for a central service than for a daemon that lives for one CI job.

Buildah and Podman: no daemon, one process per build

Buildah builds images as an ordinary process with no daemon. buildah build reads a Containerfile or Dockerfile, and podman build calls the same code. Each RUN uses an OCI runtime by default, or a chroot (BUILDAH_ISOLATION=chroot) when the runtime cannot run inside your build container. Images go into local containers-storage and are pushed from there.

The Podman family went through a big year. Podman Container Tools joined the CNCF Sandbox on 21 January 2025. In June 2026 Podman, Buildah and Skopeo moved to the podman-container-tools GitHub organisation, pushed by CNCF requirements and the shutdown of the Cirrus CI service the projects used. Podman 6.0.0 (24 June 2026) changed its Go import path to go.podman.io/podman/v6, dropped cgroups v1, CNI, iptables and slirp4netns, and must be paired with Buildah 1.44 or later. If your CI images install Buildah from a distribution, check which line you actually get.

kaniko: build inside your own container

kaniko works differently. The executor runs as root inside an ordinary, unprivileged container, unpacks the base image straight into its own root filesystem, runs each RUN command there, and snapshots the changes. It does not need a daemon, a socket or privileged: true, which is why it became the default answer for “build images in Kubernetes” in 2018. The flip side is in its README: it “unpacks directly into its own container root and may overwrite anything already there”, and the projects do not recommend running the executor binary inside some other image. One kaniko pod should run one build and then go away.

Rootless is a set of requirements

Rootless builds map root inside the build to an ordinary user outside it, using user namespaces. For multi-user base images that means a range of subordinate IDs (/etc/subuid and /etc/subgid) and the setuid helpers newuidmap and newgidmap. My drill hit this directly: in a user namespace with a single mapped ID, BuildKit failed to unpack Alpine because /etc/shadow belongs to group 42, and failed on BusyBox because /home belongs to user 65534.

In Kubernetes, BuildKit’s rootless docs ask for seccomp and AppArmor set to Unconfined, and its Kubernetes examples use --oci-worker-no-process-sandbox, because Kubernetes has no equivalent of Docker’s systempaths=unconfined. The same docs warn that this flag lets build containers kill, and possibly ptrace, other processes in the BuildKit container. Buildah’s rootless OpenShift tutorial uses chroot isolation and the anyuid security context constraint, which adds CAP_SETUID, CAP_SETGID and CAP_KILL.

Kubernetes user namespaces (hostUsers: false) became stable in Kubernetes 1.36. They need Linux 6.3 or later, filesystems with idmap mount support, containerd 2.0 or CRI-O 1.25, and runc 1.2 or crun 1.9. With them, root in a build pod is not root on the node, which makes “rootful inside the pod” builders far less risky. If your clusters run 1.36 with a recent runtime, test this before you settle for wide Unconfined profiles.

Caching

Layer cache in a registry

On ephemeral CI runners, the local cache is empty every time, so all three builders can keep cache in a registry:

  • BuildKit exports cache with --export-cache type=registry,ref=... (or --cache-to in Buildx). mode=min, the default, keeps only layers of the final image; mode=max also keeps intermediate stages, which helps multi-stage builds. BuildKit also has backends for S3, Azure Blob, GitHub Actions and a local directory. If you run an S3-compatible store yourself, my MinIO alternatives post covers the options.
  • Buildah has --cache-to and --cache-from repositories, used with --layers.
  • kaniko caches RUN layers (and optionally COPY layers) in a repository given by --cache-repo, inferred from --destination if you do not set it, and can warm base images into a local directory.

A cache repository is not a scratch area. It holds full layers from your builds, so give it the same access control and retention you give the images. In my drill, a local cache export of the tiny test image was 1.5 MB; after buildctl prune --all, a build that imported it showed both COPY steps as CACHED and produced the same digest.

Cache mounts

A cache mount keeps a directory, such as a package manager’s download cache, between builds without putting it in a layer:

# syntax=docker/dockerfile:1
FROM golang:1.25 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -trimpath -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

BuildKit and Buildah both support RUN --mount=type=cache, and Buildah documents sharing=shared, locked and private. The osscontainertools kaniko fork supports cache and secret mounts too; its README even prints a hint when a large cache directory ends up in a snapshot. Remember that cache mounts live on the builder. On a fresh CI pod they start empty unless the daemon or the volume survives between jobs, and the registry layer cache does not carry them.

When a shared cache becomes a risk

CVE-2026-93318 is the case to remember. Before BuildKit 0.33.1, a malicious image could advertise layer DiffIDs that belong to another image, and BuildKit trusted them for its cache identity. On a daemon with a shared or persistent cache, a later build of a different, trusted image could then get the attacker’s layer as its base. CVE-2026-93317 was a related issue where the blob cache could accept unverified content through the low-level API. If several teams share one BuildKit service, update it to 0.33.1 or later first, and think about separate daemons for untrusted code such as pull requests from forks.

Reproducibility

SOURCE_DATE_EPOCH is not enough on its own

Two builds of the same commit usually give different digests, because timestamps are everywhere: the created field in the config, the history entries, and the modification time of every file and directory in each layer. The convention for fixing this is SOURCE_DATE_EPOCH, a Unix time usually taken from the last commit.

BuildKit treats SOURCE_DATE_EPOCH as a special build argument (since 0.11), and buildctl 0.13+ and Buildx 0.10+ pass it on from the environment. It sets the config and history created times and the index annotation. It does not change the times inside the layers unless you add rewrite-timestamp=true to the image exporter (since BuildKit 0.13). My drill showed exactly this. With only the build argument, the config said 2026-01-01T00:00:00Z, but each run still had a new digest, because the /app directory that COPY created carried the build time. With both settings, three builds gave the same digest even after I touched every input file.

export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --cache-from type=registry,ref=registry.example.in/payments/api:buildcache \
  --cache-to type=registry,ref=registry.example.in/payments/api:buildcache,mode=max \
  --provenance=mode=max --sbom=true \
  --output type=image,name=registry.example.in/payments/api:"$CI_COMMIT_SHA",push=true,rewrite-timestamp=true \
  .

Buildah has the same pair: --source-date-epoch (which also reads the SOURCE_DATE_EPOCH environment variable) sets the created time, and --rewrite-timestamp clamps newly added content to that time. kaniko has --reproducible, which strips timestamps from the image; the osscontainertools fork has flags that change how that mode treats base layers and compression.

buildah build --layers \
  --cache-from registry.example.in/payments/api-cache \
  --cache-to registry.example.in/payments/api-cache \
  --source-date-epoch "$(git log -1 --pretty=%ct)" --rewrite-timestamp \
  -t registry.example.in/payments/api:"$CI_COMMIT_SHA" .

I did not run the Buildah command; it is assembled from the current buildah-build manual.

Other things that change digests

Timestamps are only the first layer of the problem:

  • Base images by tag. FROM alpine:3.22 changes when upstream pushes. Pin by digest and let a bot raise the update.
  • Package installs. apt-get install without pinned versions and a snapshot mirror gives different files on different days.
  • Builder versions. BuildKit’s reproducibility doc now has a compatibility-version exporter option: 20 matches the output of v0.15 through v0.31.x, and 30 is the current behaviour, where image exports default to OCI media types. BuildKit 0.33.0 also changed rootless output to match rootful output. Upgrading the builder can change digests even when nothing else did.
  • Compression settings. gzip level, zstd and estargz all change layer bytes.

A practical goal for most teams is “we can rebuild last month’s image and explain any difference”.

Multi-arch builds

Many teams now run some arm64, on cloud instances or on developer laptops. There are three ways to build for it:

  1. Emulation with QEMU through binfmt_misc. BuildKit ships QEMU helpers, but registering them needs a privileged step on the node, and emulated compiles are slow.
  2. Native builders for each platform. Buildx can join several nodes into one builder, and CI can run an arm64 runner pool.
  3. Cross-compilation, where the build stage runs on $BUILDPLATFORM and only the final stage is per target. Go, Rust and Java make this easy.

My VM had no binfmt_misc handlers registered, so I used the third pattern in its simplest form: a FROM scratch image with a binary compiled on the host for each architecture. BuildKit produced an index with an amd64 and an arm64 manifest, and the amd64 manifest digest was identical to the single-platform build. Buildah accepts a list in --platform and collects the results in a manifest list with --manifest; kaniko builds one platform per run, so you join the results with a separate manifest tool.

Attestations: SBOM and SLSA provenance

BuildKit can attach two kinds of attestation to an image: an SBOM, produced by running a scanner image (by default docker/buildkit-syft-scanner), and SLSA provenance describing how the image was built. They are stored as attestation manifests inside the image index. BuildKit 0.32.0 made attestations default to OCI artifact descriptors; use oci-artifacts=false if your registry cannot handle them. In my run, the provenance used the https://slsa.dev/provenance/v1 predicate, and in mode=max it included the Dockerfile source and the build steps. Check what mode=max records before you push it to a public registry.

Two cautions from the drill. First, the SBOM step runs a container, so it needs the same sandbox as a RUN step; in my restricted user namespace it failed in exactly the same way. Second, provenance includes an invocation ID and start and finish times. Two identical builds with provenance gave the same platform manifests but different index digests. That is expected, but it means “compare the index digest” is the wrong reproducibility check once attestations are on.

Buildah can produce SBOMs with --sbom presets that run a scanner image and store the result in the image or next to it. The kaniko READMEs do not describe built-in SBOM or provenance attestations, so kaniko users usually run a separate scanner and sign with cosign after the push.

Provenance only helps if someone checks it. SLSA’s build track asks who produced the provenance and whether the build could tamper with it. Provenance written by a build daemon that the build itself can influence is useful evidence, but it is not the same as signed provenance from a hardened, hosted build platform. Decide what your deploy side will verify, then generate exactly that. If your clusters deploy through GitOps, that check belongs next to the controller; my Argo CD and Flux post covers the deploy side.

kaniko after Google: the two forks

The GoogleContainerTools/kaniko README now begins: “This project is archived and no longer developed or maintained.” The last push was on 3 June 2025, and the old gcr.io/kaniko-project/executor images are frozen. Chainguard, whose founders include kaniko’s original authors, announced its fork on 5 June 2025, saying Google had “quietly announced” the archive that week.

Chainguard fork

osscontainertools fork

Repository

chainguard-forks/kaniko (the old chainguard-dev/kaniko URL redirects)

osscontainertools/kaniko, created in October 2024; early changes came from the mzihlmann/kaniko fork

Latest release

1.25.19 on 27 August 2026

1.28.5 on 20 September 2026

Scope

Security patches and maintenance; “No major feature work is planned”

Dependency updates, bug fixes, performance work and new features behind FF_KANIKO_* flags

Images

Not published; build your own from source tags, or use Chainguard’s commercial images

Published at ghcr.io/osscontainertools/kaniko and martizih/kaniko on Docker Hub

Funding

Chainguard

Corporate sponsors L3montree and Siemens, plus individuals

Advisory to know

CVE-2026-28406: build context tar path traversal, from 1.25.4 until 1.25.10

Its changelog lists many dependency CVEs fixed since Google’s 1.24.0

Neither fork is a drop-in “change one line” answer. With Chainguard’s fork you also take on building, scanning and hosting the executor image yourself, unless you buy theirs. With the osscontainertools fork you get public images and active development, but it is a younger project, and new feature flags mean behaviour changes you should read before upgrading. It also offers optional OpenTelemetry traces of each build. They are off by default, but once on, its README warns that they carry the text of every instruction, so treat the collector as part of your secret boundary.

For the Pune team, a fork is a reasonable short-term step: it removes the archived executor today. Pin it by digest and test the --reproducible and cache flags you use. Whether to stay on kaniko is a separate decision for the next quarter.

A minimal kaniko job, for reference (not run in my drill):

apiVersion: v1
kind: Pod
metadata:
  name: build-payments-api
spec:
  restartPolicy: Never
  containers:
    - name: kaniko
      image: ghcr.io/osscontainertools/kaniko@sha256:<pinned-digest>
      args:
        - --context=git://git.example.in/payments/api.git#refs/heads/main
        - --dockerfile=Dockerfile
        - --destination=registry.example.in/payments/api:main
        - --cache=true
        - --cache-repo=registry.example.in/payments/api-cache
        - --reproducible
      volumeMounts:
        - name: registry-auth
          mountPath: /kaniko/.docker
  volumes:
    - name: registry-auth
      secret:
        secretName: registry-push
        items:
          - key: .dockerconfigjson
            path: config.json

Push credentials like this should be short-lived and scoped to one repository. If you are moving them into a secrets manager at the same time, see my Vault and OpenBao post.

Security advisories

Every CVE ID below was checked against the CVE.org API (cveawg.mitre.org) on 11 October 2026 and returned a published record.

CVE

Project

What it is

Fixed in

CVE-2026-93318

BuildKit

Cache poisoning through unvalidated layer DiffIDs (high)

0.33.1

CVE-2026-93317

BuildKit

Blob cache can accept unverified content

0.33.1

CVE-2026-93316

BuildKit

Daemon panic on some builds when started with --cdi-disabled (high)

0.33.1

CVE-2026-93315

BuildKit

Build steps can disrupt proxy CA clean-up

0.33.1

CVE-2026-15793

BuildKit

Git checkout from a bundle can lead to command injection on the host (0.30.0 to 0.31.1)

0.31.2

CVE-2026-61711

BuildKit

A custom frontend can disable seccomp and AppArmor for build containers

0.31.1

CVE-2026-33747

BuildKit

A malicious frontend can write files outside the state directory (CVSS 8.4)

0.28.1

CVE-2024-23651, CVE-2024-23652, CVE-2024-23653

BuildKit

Cache mount race, mount stub cleaner host access, interactive containers entitlement check

0.12.5

CVE-2026-103433

Docker Buildx

Bake skips filesystem entitlement consent for some secret and OCI layout inputs

0.37.2

CVE-2026-44517

Buildah

Build breakout through a malicious Git server or tar archive (1.38.1 onwards)

1.43.2 and 1.44.0

CVE-2026-79705

Buildah

copier package symlink escape when used outside Buildah by non-root callers

1.43.4 and 1.45.1

CVE-2024-11218

Buildah

Container breakout with --jobs=2 and a malicious Containerfile

1.33.12, 1.35.5, 1.37.6 and 1.38.1

CVE-2025-11395, CVE-2026-79699

container-libs (Podman, Buildah, Skopeo)

Crafted tar layers or whiteouts can overwrite or replace files

Podman 6.1.2

CVE-2026-28406

kaniko (Chainguard fork)

Build context tar path traversal (CVSS 8.2)

1.25.10

CVE-2025-31133, CVE-2025-52565, CVE-2025-52881

runc

Container escapes through mount races and procfs tricks

runc 1.2.8 and 1.3.3

Two more items carry IDs that the CVE.org API did not return on 11 October 2026, so I list them by GitHub advisory only: Podman 6.1.3 fixed GHSA-2cvf-wqm6-wr9g (running a checkpoint image could switch off sandboxing, so checkpoint images can no longer be used with podman run), and Buildx 0.37.2 fixed GHSA-gwr2-q96m-6682 (Bake skipped filesystem consent under --progress=rawjson).

The pattern is clear. Most builder bugs come through inputs you do not fully control: custom frontends set with # syntax=, Git and tar sources, base images and shared caches. Three habits cover much of it: pin the Dockerfile frontend to the official docker/dockerfile image by digest, do not let untrusted pull requests use a shared cache, and patch the runc copy inside your builder images, not only the one on your nodes. BuildKit ships its own buildkit-runc in the release tarball, and kaniko and Buildah images carry their own dependencies.

Licences and terms

All the builders here, plus ko, Jib, pack and apko, are Apache 2.0, so the licence itself is rarely the problem. The terms around them are where teams get caught:

  • Docker Desktop is under the Docker Subscription Service Agreement. It is free for small businesses (under 250 employees and under USD 10 million in revenue), personal use, education and non-commercial open source, and paid for professional use in larger organisations. BuildKit, Buildx and Docker Engine are open source and not covered by that agreement, so CI on Linux runners does not need Desktop.
  • Image sources have their own terms. Chainguard’s kaniko images are a commercial product; the osscontainertools images are public. Base images from any vendor can have terms different from the code inside them.
  • Registry limits. Anonymous pulls from public registries are rate limited, which breaks CI at the worst moment. A pull-through cache or mirror is cheaper than the outage.

The other builders worth a look

Not every image needs a Dockerfile. If most of your services are in one language, a language-aware builder may remove the privilege question altogether.

  • ko builds Go programs by running go build on the host and putting the binary on a small base image. It needs no Docker daemon and no RUN sandbox, supports multi-arch by cross-compiling, and creates SBOMs. It is a CNCF Sandbox project; 0.19.1 came out on 29 June 2026.
  • Jib does the same for Java through Maven and Gradle plugins, building layers from dependencies, resources and classes without a daemon.
  • Cloud Native Buildpacks build from source with buildpacks, producing images with an SBOM and the option to rebase onto a new base image without a rebuild. The project became CNCF Graduated on 17 July 2026.
  • apko, from Chainguard, builds images from APK packages described in YAML. Its README calls it “fully reproducible by default”, and it is often paired with melange for building the packages.

For a Go or Java team, these can move most images out of the privileged-build problem and leave BuildKit or Buildah for the few that need it.

A decision guide

Your situation

A sensible starting point

Watch out for

Docker-based CI on VMs or dedicated runners

BuildKit through Buildx, registry cache, rewrite-timestamp

Daemon socket access; keep 0.33.1 or later

Kubernetes CI on 1.36 with containerd 2.x and recent runc

BuildKit or Buildah in pods with hostUsers: false

Idmap support on node filesystems; test with real base images

Kubernetes CI where pods must stay restricted

kaniko fork for now, or language builders where possible

One build per pod; fork maturity; no built-in attestations

Red Hat or Podman-based platform, OpenShift

Buildah with chroot isolation

SCC needs; Podman 6 changes; the GitHub organisation move

Many teams sharing one central build service

BuildKit with separate daemons per trust level

Shared cache poisoning; frontend pinning; daemon patching

Mostly Go or Java services

ko or Jib, with a Dockerfile builder for the rest

Base image choice and SBOM coverage

For the Pune team, my recommendation is in two steps. This month, move the executor to the osscontainertools fork, pinned by digest, or build Chainguard’s fork into your own image if you want the smallest change in behaviour, and turn on --reproducible. Then run a small proof of concept with BuildKit in pods with user namespaces on one runner pool, using registry cache, rewrite-timestamp and provenance. Choose by what the security team will accept for pod settings and what the deploy side will verify, not by build speed.

A practical checklist

  1. List every place images are built: CI runners, laptops, release jobs and the odd cron job.
  2. Record the builder, its version and its image digest for each, and who updates it.
  3. Replace any gcr.io/kaniko-project executor or warmer image.
  4. Pin base images and the Dockerfile frontend by digest, with automated update requests.
  5. Decide the pod security model: privileged, rootless with Unconfined profiles, user namespaces, or kaniko-style.
  6. Set up registry cache with access control and a retention rule, separate from release images.
  7. Keep untrusted builds, such as pull requests from forks, away from shared caches and shared daemons.
  8. Set SOURCE_DATE_EPOCH from the commit and turn on timestamp rewriting; build twice and compare platform manifest digests.
  9. Decide which attestations you need, then verify them at deploy time.
  10. Follow advisories for the builder, its embedded runc and the libraries it vendors, and patch on a calendar.

Common mistakes

  • Treating the archived kaniko image as “stable”. It no longer gets fixes.
  • Running BuildKit privileged: true because rootless failed once. Find which requirement was missing first.
  • Sharing one build cache across every team and every pull request.
  • Setting SOURCE_DATE_EPOCH and assuming the image is reproducible. Layer timestamps need the rewrite option.
  • Comparing index digests with provenance turned on. They change on every build by design.
  • Upgrading the builder and the base image in the same change, then not knowing which one changed the digest.

Drill: two builds, one digest on one VM

On 11 October 2026, from about 6:21 PM to 6:24 PM IST, on a shared Linux VM with 8 vCPUs (Intel Xeon), 15 GiB of RAM, Debian 13 and kernel 6.12, without root, I ran BuildKit 0.34.0 from the official release tarball. Its SHA-256 matched the digest GitHub lists for the release asset. There was no container engine and no newuidmap on the VM, so I started buildkitd with --rootless inside unshare -Urm, a user namespace with only my own user mapped to root, using the native snapshotter and host networking. Afterwards I stopped the daemon and removed its state directory.

What did not work, and why it matters:

  • Building on alpine:3.22 failed while unpacking the base image: BuildKit could not lchown /etc/shadow to group 42. BusyBox 1.37 failed the same way on /home, owned by user 65534. The error itself suggested more subordinate IDs.
  • A RUN step on a scratch base failed because runc could not mount /dev/pts with group 5.
  • The SBOM attestation failed for the same reason, because the scanner runs as a container.

So the test image was a FROM scratch image with two COPY steps: a small static Go program (2,364,442 bytes for amd64), cross-compiled on the host for amd64 and arm64, and an 8-byte text file. Output went to OCI tar files, not a registry. SOURCE_DATE_EPOCH was 1767225600 (1 January 2026, 00:00 UTC).

Run

Settings

Result

1 and 2

--no-cache, no epoch

Different digests (ca9ce602… and 99a72950…); config times and both layers differ

3

Cache allowed

0.09 s, same digest as run 2

4, 5 and 6

Epoch only, --no-cache, an input touched before run 5

Three different digests; config says 2026-01-01, but /app entries carry the build time

7 and 8

Epoch and rewrite-timestamp=true

Same digest, 7982a96e…; every tar entry dated 2026-01-01

9

As 7, after touch on every input

Same digest, 7982a96e…

10 and 11

amd64 and arm64, epoch, rewrite, no provenance

Same index digest (31a95107…); amd64 manifest equals run 7

12 and 13

As 10, with attest:provenance=mode=max

Same platform manifests, different index digests (8390fe6e… and be558a54…)

14

Local cache export, prune --all, then import

Both COPY steps CACHED; same digest 7982a96e…; cache directory 1.5 MB

Build times were all under half a second for this tiny image, so they say nothing about real builds. The useful results are the digests. Without the epoch, every build was new. With the epoch alone, the config was stable but the layers were not. With both settings, the digest stayed the same through rebuilds, touched files and a cache round trip. The provenance runs show why the index is the wrong thing to compare.

Honest limits: one VM, one builder, one tiny COPY-only image, and each scenario run once or twice. I could not test RUN steps, cache mounts, SBOMs, real base images, QEMU emulation or a registry push, because the VM has no subordinate ID helpers, no container runtime and no root. I did not run Buildah, Podman or kaniko: Buildah publishes source releases, not static binaries, and kaniko is meant to run only as its own container image. Nothing here is a speed comparison, and I used no vendor benchmark. The numbers describe this setup, not your CI.

What to unlearn and re-learn

  • Unlearn “kaniko is the safe default for Kubernetes builds”. Re-learn that the original is archived, the forks differ in scope and images, and newer Kubernetes user namespaces change the trade-offs.
  • Unlearn “rootless means no special settings”. Re-learn that rootless needs subordinate IDs, helper binaries and, in Kubernetes, relaxed seccomp and AppArmor or user namespaces.
  • Unlearn “the same commit gives the same image”. Re-learn that you need a fixed epoch, rewritten layer timestamps, pinned inputs and the same builder version.
  • Unlearn “attestations make the image trustworthy”. Re-learn that provenance is only useful when something verifies it, and that it changes the index digest.

Revisit your image builder before you change your CI

The Pune team does not need to pick a winner this week; it needs to stop running an archived executor and know what each option asks of its clusters. Learn how your builder runs RUN steps, where its cache lives and what it records about each build. Unlearn the comfort of a pinned executor tag that has not changed in two years. Re-learn rootless requirements, cache trust and reproducibility from the current docs and release notes. Practise by building the same commit twice and comparing digests, as I did on one VM, and by running one build in a restricted pod with a real base image. Then apply what you find: a builder you can patch, a cache you can trust, digests you can explain and attestations someone actually checks. Revisit it when your Kubernetes version, your base images or your builder’s maintainers change.

Sources

comments powered by Disqus

Releted Posts

Vault and OpenBao: revisit your secrets manager before you renew or migrate

A platform team runs a three-node Vault Community cluster for about sixty services. In one week, a product group asks for its own isolated tenant, which in Vault means namespaces, an Enterprise feature, and finance asks whether the Enterprise quote is really needed.

Read more

Fluent Bit, Vector and the OpenTelemetry Collector: revisit your log pipeline before you ship more logs

A logistics company in Pune runs about 600 pods across three Kubernetes clusters. A Fluentd DaemonSet, set up in 2019 with a dozen Ruby plugins, ships about 25 crore log lines a day to Loki.

Read more

Service mesh in 2026: revisit Istio ambient, Linkerd and Cilium before you add sidecars

A logistics company in Bengaluru runs a production Kubernetes cluster with 14 nodes and about 420 pods. Three requests landed in one sprint.

Read more