DEV Community

Leo
Leo

Posted on Originally published at cicd.deployment.to

PHP2Shell and the eMudhra certificates have the same fix: rebuild the image

I'll admit something slightly embarrassing. For years my mental model of a compromised certificate was "the browsers will handle it." Somebody revokes it, Chrome and Firefox stop trusting it, and I go back to watching my build. That model has a hole in it, and the hole is shaped exactly like every container I ship.

Docker's write-up on PHP2Shell and the eMudhra certificate compromise is the clearest explanation of that hole I've read in a while. Two unrelated security events landed in the same week. If you run containers, both come down to the same chore of rebuilding the image on a fixed base and redeploying it.

Two incidents, one chore

PHP2Shell is a pair of critical flaws in the PHP engine itself, CVE-2026-109618 and CVE-2026-109619. According to Docker, they let an unauthenticated request turn into code execution on the server, on any PHP application, no matter how careful that application's own code is. Docker also notes that PHP runs about 69% of the websites whose server-side language is known. That's a lot of Dockerfiles starting with a PHP base.

The eMudhra problem is quieter, and honestly it's the one that changed how I think about images. Eight intermediate certificates from the certificate authority eMudhra turned out to have private keys that can be computed from the public certificates. With those keys, anyone could issue a certificate for any website and have it accepted.

Browsers revoke certificates like these on their own. Your containers don't. In Docker's words, an image "carries its own trust store and keeps trusting them until it is rebuilt."

Why restarting the pod does nothing

This is general container plumbing, and it's worth having clearly in your head before the next incident. On Debian and Alpine based images, the bundle of trusted certificate authorities comes from the ca-certificates package and gets written into a layer at build time. Nothing refreshes it at runtime. Every container started from that image gets the same bundle, forever. The PHP binary works the same way: it's a file in a layer, and a restart hands you the same file.

Two common habits make this worse:

  • A warm layer cache. If your build host already has the old base image cached, a plain docker build reuses it. You get a fresh build of your app on a stale base, and the pipeline goes green.
  • Digest pinning without a bump process. Pinning FROM to sha256:<digest> is good supply-chain hygiene. It also means you get the fix only when someone, or some bot, moves the digest.

Neither is wrong. They just mean "the vendor patched it" and "we're patched" are two different events, and only the second one protects you.

What Docker's rebuild looked like

On the Docker Hardened Images (DHI) side, the dhi.io/php image was rebuilt on the patched PHP and published early on October 22, within 24 hours of the upstream release and after DHI's testing and verification steps. In the 48 hours after the PHP release, the DHI build system ran 16,567 package and image builds across the catalog. By 10pm UTC on October 22, Docker says almost all catalog tags were on the fixed CA store, with most of the rest in the build queue.

The mechanism is an event-driven build system: an upstream release or a changed package triggers rebuilds of everything that depends on it. What I appreciated most is that Docker could measure it. The percentage comes from reading the SBOM shipped with every published tag to see which CA store and which PHP version it carries, and Docker points out you can run the same check on anything you pull. A rebuild you can't verify is a rebuild you have to take on faith, and I've been burned by faith before.

Doing the same on your own images

Docker's advice for DHI users is short: pull the current tag for your branch, redeploy, and rebuild anything you built on top of a DHI image so it picks up the new base. Customizations built through the DHI Enterprise workflow are rebuilt automatically when the base layer is patched. For PHP in any other container, Docker says to check the PHP version actually running inside it, because the tag on the outside won't tell you, and to rebuild on a patched base.

Here's roughly what that looks like from a terminal:

# Which PHP is really inside the image you deploy?
docker run --rm --entrypoint php $IMAGE -v

# Rebuild on a freshly pulled base, skipping a stale cached FROM layer
docker build --pull -t $REGISTRY/my-php-app:<new-tag> .

# Which CA bundle package does the image's SBOM record?
syft $IMAGE | grep -i ca-certificates
Enter fullscreen mode Exit fullscreen mode

The rough edge is everything you didn't build yourself: the sidecar image someone vendored two years ago, the internal tool nobody owns, the image in a registry your pipeline never touches. None of those get rebuilt by a pipeline that only runs on your commits.

How popular tools handle "rebuild when the base changes"

Both fixes need a rebuild that something other than your own commit kicks off. Here's how common tools get you there.

Tool What kicks off the rebuild Where it shines
Dependabot PRs that bump a FROM tag or pinned digest (docker ecosystem) Zero extra infra on GitHub, and the change goes through normal review. A tag that is rebuilt in place gives it nothing to bump, so pair it with a schedule.
Renovate PRs that pin and update image digests The most configurable option, and it runs across GitHub, GitLab, Bitbucket and Azure DevOps
GitHub Actions on: schedule cron plus docker/build-push-action with pull: true The schedule lives in the same workflow file as the build
GitLab CI Pipeline schedules plus docker build --pull, with Container Scanning flagging vulnerable packages Scheduling and scan results in one place for GitLab shops
Docker Scout / DHI docker scout recommendations suggests newer base images; DHI Enterprise rebuilds customizations when the base is patched The base gets fixed upstream before you even pull
Buddy A pipeline SCHEDULE event plus the Dockerfile build action with pull: true A forced-fresh nightly rebuild declared in one YAML file, with no separate bot to run

If you pin by digest and your repos are spread over more than one Git host, Renovate is the better fit than any CI-native trigger, Buddy's included. Bumping digests through reviewed PRs fixes the pinning problem at the source.

If you'd prefer a plain timed rebuild, the reason I'd look at Buddy is that the trigger and the forced base pull sit side by side in one pipeline definition:

- pipeline: "Nightly base-image rebuild"
  refs:
  - "refs/heads/main"
  events:
  - type: SCHEDULE
    cron: "0 3 * * *"
    timezone: "UTC"
  actions:
  - action: "Build Docker image"
    type: "DOCKERFILE"
    dockerfile_path: "Dockerfile"
    context_path: "."
    pull: true
Enter fullscreen mode Exit fullscreen mode

What I'm watching next

Docker's numbers show a rebuild of this size can be done and checked within a couple of days. I'm less sure most of our own pipelines could say the same. For your images, the question to ask is simple: if a CA went bad tonight, how long until every container you run stopped trusting it? I'd love to hear honest answers, including "we'd have no idea," because I suspect that's where a lot of us are.

Top comments (0)