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/kanikorepository is archived; its last release was v1.24.0 on 23 May 2025. Two forks carry it on. Chainguard’s fork (now atchainguard-forks/kaniko, v1.25.19 on 27 August 2026) does maintenance only and publishes source tags, not images. Theosscontainertools/kanikofork (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
containersGitHub organisation topodman-container-toolsin 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
newuidmapand subordinate IDs, rootless BuildKit started but could not unpack an Alpine or BusyBox base image or start aRUNstep. In Kubernetes, rootless BuildKit still needs seccomp and AppArmor set toUnconfined. - Reproducible digests need two things in BuildKit:
SOURCE_DATE_EPOCHand therewrite-timestamp=trueexporter option. In my drill,SOURCE_DATE_EPOCHalone fixed the config dates but still gave a new digest on every run; with both, three builds and atouchof 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.
- Gets the inputs. The build context, the Dockerfile and the base images. This is where secrets,
.dockerignoremistakes and untrusted Git or tar sources come in. - Runs build steps. Each
RUNneeds 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. - 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.
- 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.
- 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-toin Buildx).mode=min, the default, keeps only layers of the final image;mode=maxalso 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-toand--cache-fromrepositories, used with--layers. - kaniko caches
RUNlayers (and optionallyCOPYlayers) in a repository given by--cache-repo, inferred from--destinationif 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.22changes when upstream pushes. Pin by digest and let a bot raise the update. - Package installs.
apt-get installwithout pinned versions and a snapshot mirror gives different files on different days. - Builder versions. BuildKit’s reproducibility doc now has a
compatibility-versionexporter option:20matches the output of v0.15 through v0.31.x, and30is 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:
- 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. - Native builders for each platform. Buildx can join several nodes into one builder, and CI can run an arm64 runner pool.
- Cross-compilation, where the build stage runs on
$BUILDPLATFORMand 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 buildon the host and putting the binary on a small base image. It needs no Docker daemon and noRUNsandbox, 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
- List every place images are built: CI runners, laptops, release jobs and the odd cron job.
- Record the builder, its version and its image digest for each, and who updates it.
- Replace any
gcr.io/kaniko-projectexecutor or warmer image. - Pin base images and the Dockerfile frontend by digest, with automated update requests.
- Decide the pod security model: privileged, rootless with
Unconfinedprofiles, user namespaces, or kaniko-style. - Set up registry cache with access control and a retention rule, separate from release images.
- Keep untrusted builds, such as pull requests from forks, away from shared caches and shared daemons.
- Set
SOURCE_DATE_EPOCHfrom the commit and turn on timestamp rewriting; build twice and compare platform manifest digests. - Decide which attestations you need, then verify them at deploy time.
- 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: truebecause rootless failed once. Find which requirement was missing first. - Sharing one build cache across every team and every pull request.
- Setting
SOURCE_DATE_EPOCHand 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.22failed while unpacking the base image: BuildKit could notlchown/etc/shadowto group 42. BusyBox 1.37 failed the same way on/home, owned by user 65534. The error itself suggested more subordinate IDs. - A
RUNstep on ascratchbase failed because runc could not mount/dev/ptswith 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
- BuildKit: repository, 0.34.0 release, 0.33.1 release, 0.33.0 release, 0.32.0 release, Dockerfile frontend 1.28.0, rootless mode, build reproducibility, Kubernetes examples, SLSA definitions, security advisories, LICENSE
- Docker: Buildx repository, Buildx 0.38.0 release, Buildx 0.37.2 release, BuildKit overview, registry cache backend, Docker Engine 23.0 release notes, Docker Desktop licence agreement
- Buildah and Podman: Buildah repository, Buildah 1.45.1 release, Buildah 1.43.4 release, buildah-build manual, buildah-run manual, rootless OpenShift tutorial, Buildah security advisories, Buildah LICENSE, Podman repository, Podman 6.1.3 release, Podman 6.1.2 release, Podman 6.0.0 release, CNCF organisation move discussion, container-libs advisories, CNCF: Podman Container Tools
- kaniko: archived Google repository, Google 1.24.0 release, Google Cloud blog: introducing kaniko, Chainguard fork, Chainguard fork 1.25.19 release, Chainguard fork advisories, Chainguard: Fork yeah, we’re bringing kaniko back, osscontainertools fork, osscontainertools 1.28.5 release, osscontainertools changelog overview
- Other builders: ko repository, ko 0.19.1 release, CNCF: ko, Jib repository, Jib Gradle 3.5.4 release, pack repository, pack 0.40.9 release, CNCF: Buildpacks, apko repository, apko 1.4.10 release
- Kubernetes and supply chain: Kubernetes user namespaces, SLSA build levels, SOURCE_DATE_EPOCH specification, runc security advisories
- CVE records (cveawg.mitre.org API): CVE-2026-93318, CVE-2026-93317, CVE-2026-93316, CVE-2026-93315, CVE-2026-15793, CVE-2026-61711, CVE-2026-33747, CVE-2024-23651, CVE-2024-23652, CVE-2024-23653, CVE-2026-103433, CVE-2026-44517, CVE-2026-79705, CVE-2024-11218, CVE-2025-11395, CVE-2026-79699, CVE-2026-28406, CVE-2025-31133, CVE-2025-52565, CVE-2025-52881
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 moreFluent 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 moreService 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