Skip to main content

Buildpack Rebasing: Patching a Base-Image CVE Across Every Running Container in Seconds

9 min readDora NodaDora Noda
Share
On this page

In November 2025, the Go project disclosed two CVEs in golang.org/x/crypto/ssh. Docker's own security team calls its response a best case: Docker Scout picked up the CVE feed within seconds, and every affected Docker Hardened Image was rebuilt and republished inside a day — against a public SLA that only promises seven. That's the fastest a base-image publisher can plausibly move.

Now the harder question: how fast does that fix reach a container that's already running in production? On a Dockerfile-first platform, the answer is "however long it takes each tenant to notice, rebuild, push, and redeploy" — a rebuild-and-redeploy cycle repeated once per app, gated on that app's own CI. On a platform built on Cloud Native Buildpacks (CNB), the answer is one command — pack rebase — that finishes in seconds and touches every image built from that base, no rebuild required. That gap, not the publisher's SLA, is what actually determines how long a fleet stays exposed.

Two Ways a Container Image Ships an OS

A docker build from a typical Dockerfile flattens everything into one image: base OS packages, language runtime, and application code all get baked into a single set of layers referenced by one manifest. Nothing in that manifest distinguishes "the part that came from apt-get" from "the part that came from COPY . ." — to the registry, it's just layers. Patch a CVE in the base OS and the only lever you have is rebuilding the whole thing from FROM down.

Cloud Native Buildpacks split that same image into two OCI artifacts that stay logically separate even after the image is built:

  • A build image used only at build time (compilers, build-time OS packages) — thrown away once the app is exported.
  • A run image — the actual runtime OS layer (glibc, OpenSSL, the C library your language runtime links against) — that ships underneath the app's own layers in the final image.

The app's layers (compiled binaries, dependencies, source) sit on top of the run image but reference it only as a base layer digest in the OCI manifest — the same relationship a Dockerfile's FROM line has to its base, except the buildpacks lifecycle records that reference explicitly and keeps the two halves independently swappable after the fact. That's the property a flattened Dockerfile build never had: an app image whose OS layer can be replaced without touching a single byte of app code.

The Command

When a CVE lands in the run image — glibc, OpenSSL, whatever ships in the base OS layer — the fix is one command per app image:

bash
pack rebase my-app:latest --run-image paketobuildpacks/run-jammy-base:patched

Under the hood, pack rebase does three things, and none of them re-runs a build:

  1. It resolves the new run image's layer digests from the registry.
  2. It rewrites the app image's manifest to point at those digests instead of the old, vulnerable ones — the app layers (your compiled code, dependencies) are untouched.
  3. If the old and new run images live in the same registry, it uses the registry's cross-repository blob mount API to link the new layers in place, rather than re-uploading them — so the data actually moved over the wire is close to zero.

No buildpack detect, build, or export phase runs again. The command completes in seconds regardless of how large the app's own dependency tree is, because it never touches that dependency tree — it's a manifest edit plus a registry-side layer link, not a build.

One honest caveat belongs here: rebase only works if the run image genuinely stayed isolated. A tenant whose own build step shells out to apt-get install and bakes an OS package into their app layer instead of the run layer has broken that isolation — that image is no longer cleanly rebasable, and falls back to a real rebuild like everyone else. This is why a git-push PaaS built on buildpacks enforces the app-layer/run-layer boundary as a platform invariant rather than a suggestion: it's the thing that makes the seconds-not-minutes number true at all.

That command patches one image. What a platform operator actually cares about the day a CVE lands is what it costs to patch every image built from the vulnerable base — which is where the comparison to a Dockerfile-first platform gets concrete.

What It Costs the Other Way

Picture a platform running 200 tenant apps, all built from the same vulnerable Ubuntu Jammy base. The upstream base image gets patched. Now what?

On the buildpack side: one pack rebase invocation per image, or a loop over all 200 — a script the platform operator runs once, finishing in well under a minute of wall-clock time for the whole fleet, because each rebase is a manifest edit, not a rebuild. The platform can then roll the patched images out to the running fleet on its own schedule, without asking a single tenant to do anything.

On a Dockerfile-first platform, the patch has to reach each app individually, because each app's Dockerfile owns its own FROM line:

  • A cold rebuild of a typical ~2GB application image takes roughly 5–8 minutes; a full build-push-pull-redeploy cycle for one app runs closer to 12 minutes end to end once registry push/pull and container restart are counted.
  • Assume, generously, that all 200 tenants are perfectly automated — a Renovate or Dependabot job watching the base digest and triggering a rebuild the moment it changes. That's still roughly 200 × 12 minutes = 2,400 minutes, or 40 hours, of CI time spread across 200 different teams' pipelines, every one of which has to actually fire correctly.
  • That's the optimistic case. Wiz's own container-patching guidance — written for exactly this scenario — recommends a 24–72 hour rebuild window for high-severity fixes and weekly batches for medium severity, precisely because most engineering orgs aren't running that kind of automation on every repo. In practice, a meaningful share of those 200 apps don't rebuild on digest-change at all; they rebuild whenever someone notices a scan flag, files a ticket, and gets to it — which is where the real tail of a Dockerfile-fleet's exposure window lives: not 40 hours, but however long it takes the least-attentive tenant to notice.

The buildpack side's cost doesn't scale with tenant count at all — it's one command whether the fleet has 20 apps or 2,000. The Dockerfile side's cost is N independent rebuild cycles gated on N independent teams' CI hygiene, and the slowest one sets the fleet's real exposure window, not the average.

The Precedent: Heroku Already Ran This Experiment

This isn't hypothetical — Heroku's classic buildpack stacks already demonstrated the platform-absorbs-the-patch model in production, years before "rebasing" was a term anyone outside the CNB spec used. When CVE-2024-2961 (a buffer overflow in glibc's iconv) landed, Heroku shipped the fix into its Heroku-20/22/24 stack images, and every app running on a classic buildpack stack picked it up automatically: the platform layers the app's slug on top of the current stack image every time a dyno starts, so a routine dyno restart was enough to pick up the patched glibc — no customer rebuild, no redeploy, no ticket.

Heroku's own container stack — apps deployed from a customer-owned Dockerfile — got no such thing. Those customers had to rebuild their own image against the patched base manually, on their own schedule, exactly the per-app cycle described above. Same platform, same CVE, two completely different exposure windows depending on which build model the app happened to use. That split is the entire argument for buildpack rebasing in one real-world data point: even Docker's own 24-hour publisher-side SLA for Hardened Images is a best case for how fast the base image gets fixed — it says nothing about how fast a Dockerfile-first consumer's running containers actually get patched, and that consumer-side gap is exactly what rebase collapses to zero.

Why This Is a Platform Property, Not a Buildpack Trick

The reason this matters for a self-hosted, git-push PaaS specifically — not just for buildpacks as a build tool — is that rebase turns "patch a base-image CVE" from a fleet-wide coordination problem into a platform operation. A team self-hosting on Coolify, Dokploy, or a raw Dockerfile-per-app setup on Railway or Fly.io inherits the coordination problem: the platform can patch its own hosted base images, but it has no lever over a tenant's Dockerfile, because the tenant's Dockerfile is the tenant's own artifact from FROM on down. Offering "we patch it for you" would mean literally owning every tenant's Dockerfile, which none of those platforms do or plausibly could.

A platform built on Cloud Native Buildpacks doesn't have that problem, because it never handed tenants a Dockerfile to own in the first place — the buildpack lifecycle builds the run-image/app-layer split into every image by construction. Patching a CVE stops being "email every tenant and hope their CI is wired up" and becomes "run one command against the builder's own run-image registry, then roll the fleet."

bex builds on that same Cloud Native Buildpacks foundation — push a git repo, get a running HTTPS service on machines you own, with the same run-image/app-layer separation underneath every app the platform builds. When a base-image CVE lands, that's the difference between one pack rebase and hoping two hundred tenants each remember to rebuild.

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.


Sources:

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