Skip to main content

Buildpacks RFC 331: Getting the Extend Phase Off Vendored Kaniko

11 min readDora NodaDora Noda
Share
On this page

Every Dockerfile-free build on your platform has a secret Dockerfile inside it. A tenant pushes a Python app, asks for libpq or an ImageMagick binary the base image doesn't ship, and the buildpack workflow quietly generates a Dockerfile, applies it to the base image, and carries on as if nothing happened. That quiet step is the extend phase — and the tool applying the Dockerfile is a vendored copy of kaniko baked into the buildpack lifecycle image, undiscoverable, unpinnable, and running as root against tenant-influenced instructions.

Here is the 30-second version: today the extender ships inside the lifecycle image and finds its kaniko the way a sibling finds a sibling — implicitly. RFC 331 proposes pulling the extender out of the lifecycle image entirely so the platform references it by an explicit, configurable URI. The table below is the whole argument; the rest of this post is the evidence.

DimensionToday (extender in lifecycle)Under RFC 331 (extender by URI)
Where the extender livesCompiled into the lifecycle imageSeparate image, referenced by platform config
Which kaniko runsWhatever the lifecycle vendoredWhatever URI the platform pins and mirrors
DiscoveryImplicit sibling artifactExplicit extender-uri the operator sets
Patching kaniko CVEsWait for a lifecycle releaseRepoint the URI, rebuild nothing else
Air-gapped buildsLifecycle image drags its own extender alongMirror one more image digest, done
PriceOne image to track, zero choiceOne more reference to manage, full choice

The short version: RFC 331 turns a hidden dependency into a declared one. It was motivated by kaniko looking abandoned, explicitly de-urgentized when Chainguard adopted kaniko in June 2025 — and it is still the right change, because maintenance is not the same as modularity. This post walks through how the extend phase actually works, why kaniko got welded in, what the RFC changes mechanically, and what your platform should do while the RFC sits approved-but-unmerged.

How image extension actually works

Image extensions are the buildpacks answer to "I need an OS package." A normal buildpack emits layers during the build phase. An image extension is different: it emits Dockerfiles. During detect, extensions participate in detection like buildpacks. During generate, each selected extension's bin/generate writes either build.Dockerfile (extend the build-time base image), run.Dockerfile (extend the run-time base image), or both, into the output directory signaled by CNB_OUTPUT_DIR.

Then the extender lifecycle binary takes over. It applies those generated Dockerfiles to the base images, and only afterwards does the normal build phase run against the extended build image. A concrete example — a tenant needs Postgres client libraries in an otherwise standard Python build:

dockerfile
ARG base_image
FROM ${base_image}
RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*

Two things worth noticing. First, only a limited subset of Dockerfile instructions is supported — this is base-image customization, not a general build. Second, extension can target the run image too, and when it does with io.buildpacks.rebasable=true, the extend phase records the pre-extension top layer so export can still rebase later. The mechanism is small, well-specified, and load-bearing: it is the only sanctioned way to get libpq, ffmpeg, or a pinned JDK into a Dockerfile-free build.

For a git-push PaaS, that makes extension a tenant-facing contract even though most tenants never learn its name. Every "can I apt-get install something" support ticket is really a question about this phase.

Why kaniko got welded in

The lifecycle needs something that applies a Dockerfile to an image without a Docker daemon. Platform build nodes — especially Kubernetes-based fleets — cannot assume privileged Docker access, and handing every tenant build a daemon is a non-starter. Kaniko was the obvious answer: it builds images from Dockerfiles daemonlessly, in userspace, and it was already the standard for unprivileged in-cluster builds when the extend phase was designed.

So the lifecycle vendored it. The extender binary shells out to an embedded kaniko implementation, applying each generated Dockerfile and snapshotting the result. The Platform API 0.9 → 0.10 migration guide makes the operational shape explicit: the platform invokes extender instead of builder, mounts the shared volume at /kaniko, and — critically — the extender user "should have sufficient permissions to execute all RUN instructions in each Dockerfile — typically, it should run as root."

That last line is the cost, itemized:

  • Root against attacker-adjacent input. The RUN lines originate with extension authors and, in app-specified extension flows, with tenant configuration. The process executing them runs as root on your build nodes.
  • CVE coupling. A kaniko vulnerability can only be fixed by a lifecycle release that re-vendors it. Your patch cadence for a Dockerfile-applier is yoked to the release train of the entire build orchestrator.
  • Version lock. You cannot test a new kaniko against your fleet without adopting a new lifecycle, and you cannot hold kaniko back while adopting one. The two move as a unit whether you want them to or not.
  • No air-gap story. An operator mirroring images into a disconnected registry has no first-class artifact to mirror for the extender — it arrives stowed away inside the lifecycle image.

None of this was laziness. Vendoring was the fastest way to ship daemonless extension, and it worked. But "works" and "operable at fleet scale" are different bars, and the gap between them is exactly what RFC 331 addresses.

What RFC 331 proposes, mechanically

The proposal is one sentence with large consequences: take the extender out of the lifecycle image and let the platform supply it by configurable URI.

Concretely, instead of the lifecycle image containing the extender binary plus its vendored kaniko, the platform configuration names an extender image reference — a URI the operator sets, pins by digest, mirrors, and audits like any other supply-chain input. The lifecycle discovers nothing implicitly; the "sibling artifact" convention (the extender is whatever shipped next to the lifecycle) is replaced by an explicit pointer.

That single indirection buys four things platforms actually feel:

  1. Independent patching. Kaniko CVE? Repoint the extender URI at a patched image. No lifecycle upgrade, no builder rebuild, no fleet-wide orchestrator rollout for what is really a one-component fix.
  2. Pinned digests. The extender becomes something you can pin with sha256:, attest, and admit through policy — the same treatment your builder and run images already get.
  3. Air-gap support. A disconnected registry mirror list gains one more entry instead of one more undocumented stowaway. Auditors can point at it.
  4. A door for alternatives. The lifecycle's own Go interface already anticipates this — phase.Extender takes a DockerfileApplier described as "uses kaniko, BuildKit, or other." The RFC turns that code-level seam into an operational one. The "explore alternatives (BuildKit?)" discussion the RFC deferred becomes something a platform could trial per-fleet rather than waiting for upstream to bless a swap.

Status matters here, so state it plainly: the RFC is approved-but-unmerged. The design is accepted; the implementation is not. Platforms cannot set an extender URI today. What they can do is stop building as if the current coupling is permanent — which is the subject of the "what to do today" checklist below.

How Chainguard's fork changed the urgency (and what it didn't change)

In June 2025, Google archived kaniko. Days later, Chainguard — led by Priya Wadhwa, who had originally created kaniko on Google's Container Tools team — forked it to chainguard-dev/kaniko, promising continued maintenance, triage of long-standing issues, and hardened zero-CVE images. The original panic ("the tool inside our build path is abandoned") deflated. RFC reviewers said so explicitly: the proposal was de-urgentized once kaniko had a maintainer again.

That was the right call on urgency and would be the wrong call as a verdict. Maintenance fixed one column of the ledger and left the rest untouched:

ConcernBefore the forkAfter the fork
Abandonment riskReal — archived upstreamFixed — maintained fork, CVE patches flowing
New features (e.g. BuildKit parity)StalledStill stalled — "no major feature work planned"
Vendored into lifecycleYesStill yes
Root execution of RUN linesYesStill yes
Independently pinnable/mirrorableNoStill no
Air-gap storyNoneStill none

Read the fork announcement carefully: "we aren't planning to add new features. We'll do our best to merge minor bug fixes." That is honest, sustainable maintenance — and it is a ceiling. Every structural complaint in the previous section survives a change of maintainer, because none of them was ever about who patches kaniko. They are about where kaniko sits. A maintained vendored dependency is still a vendored dependency: you still cannot pin it, mirror it, swap it, or patch it without moving the lifecycle it hides inside.

If anything, the fork sharpens the RFC's case. Kaniko now has a stable, security-conscious home producing images worth referencing — which is precisely what a URI-configurable extender wants to point at. The fork solved the "what would we even point to" half of the problem. RFC 331 is the other half.


What a git-push PaaS should do today

The RFC is open, the fork is maintained, and tenants keep filing "can I install libpq" tickets. Six items, in priority order:

  1. Pin your lifecycle and know which kaniko you run. You cannot choose the extender today, but you can know exactly which one you have. Record the lifecycle version per builder and keep a mapping to its vendored kaniko. When the next kaniko CVE lands, you want "which builders are exposed" to be a lookup, not an investigation.
  2. Track chainguard-dev/kaniko, not the archived repo. If any part of your build path references kaniko images or docs, repoint at the maintained fork. Evaluate Chainguard's hardened kaniko images the way you would any base-image swap: build a canary builder, run your extension-heavy tenants through it, watch for behavioral drift.
  3. Isolate the extend step on build nodes. The migration guide's root guidance is not optional hardening — it is the documented shape. Run extension with the least privilege the RUN lines require, on nodes or sandboxes where a malicious or merely reckless RUN (cryptominers, exfiltration, fork bombs in apt-get clothing) has somewhere safe to fail. gVisor, Kata, or plain separate node pools all count; shared build-and-serve nodes do not.
  4. Treat generated Dockerfiles as untrusted input in review. Audit the extensions you ship and the app-specified extension paths you allow. The limited instruction set helps, but RUN is RUN. Log which extension produced which Dockerfile per build so incidents are traceable.
  5. Design your config surface for the URI now. You cannot wire an extender URI into the lifecycle yet, but you can leave the room: a single extender: key in your builder/platform config, defaulting to "lifecycle-bundled," costs almost nothing and turns RFC-day adoption into a value change instead of a schema migration. Note where the mirrored digest will go for air-gapped customers.
  6. Watch the RFC, not just the CVE feed. The signal that matters is implementation movement on the extender-URI work or the deferred BuildKit exploration — either one changes items 1–5 from defensive posture to migration plan. A quarterly five-minute check of the RFC thread beats discovering the feature in someone else's changelog.

None of this is wasted motion if the RFC ships slowly. Items 1–4 are correct operations for the coupled world; item 5 is cheap optionality; item 6 is how you avoid being surprised.

The dependency you can't see is the one that pages you

Buildpacks earned their adoption by removing Dockerfiles from the developer's view. Image extension keeps that promise for the awkward middle — tenants who need one system library and nothing else — by hiding exactly one Dockerfile one layer deeper. RFC 331's insight is that hidden-from-developer must not mean hidden-from-operator. The extender runs root-adjacent code on your build fleet, gates your CVE response time, and decides whether your air-gapped customers can build at all. That deserves a URI you can read, pin, and repoint — not a sibling artifact you discover by unpacking someone else's image.

Chainguard's fork bought the ecosystem time. Spend it on the checklist above, and on keeping the pressure — polite, upstream, implementation-shaped — for the RFC to land. Maintenance keeps the lights on; modularity is what lets you change the bulbs without rewiring the house.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex