fix(hardening): remove the shell ubi-micro ships + detect shells at real paths - #2
Merged
Conversation
…al paths
A local + adversarial build review caught that the "no shell" claim was false:
ubi9/ubi-micro itself ships /usr/bin/bash (+ /usr/bin/sh), and `COPY --from=builder
/rootfs/ /` overlays onto it without deleting it, so every derived image shipped a
working shell. The hardening test missed it because it only checked /bin/bash and
on UBI /bin is a symlink to /usr/bin (the export tar stores the real file under
usr/bin), so the literal /bin/bash entry never exists.
Root-cause fix:
- containers/Dockerfile builder (rpm-rootfs): the dependency closure of any RPM
drags bash into the empty installroot; remove it with `rpm -e --nodeps
--noscripts bash` (rpmdb stays consistent so scanners still enumerate packages)
and rm any other shell, fail closed if one survives. (NOTE: no '#' comments
inside the RUN -- a comment line silently breaks the '\' continuation.)
- containers/Dockerfile runtime (FROM ubi-micro): delete the shell ubi-micro
itself carries (COPY cannot remove a base file), fail closed if any survives.
Detection fix (catch a shell at its real path, not just /bin/*):
- tests/runtime-hardening.sh: forbid /usr/bin/{sh,bash,dash} in addition to
/bin/*, plus a generic scan for any shell binary under any bin/sbin path.
- tools/verify.py check_runtime_script: require the /usr/bin shell asserts + the
generic-scan marker.
- tools/check_image_manifest.py FORBIDDEN_BASELINE + examples manifest
forbidden_executables: add /usr/bin/{sh,bash}.
Validated empirically: --no-cache build -> zero shells in the image (export+grep),
rpmdb preserved, CA bundle populated, runtime-hardening.sh passes, verify.py ci green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap (caught by local + adversarial build review)
The "distroless / no shell" guarantee was false:
ubi9/ubi-microitself ships/usr/bin/bash(+/usr/bin/sh), andCOPY --from=builder /rootfs/ /overlays onto it without deleting it. The hardening test missed it because it only checked/bin/bash— and on UBI/binis a symlink to/usr/bin, so the real file lives atusr/bin/bashand the literal/bin/bashtar entry never exists.Root-cause fix
bashinto the empty installroot → remove it withrpm -e --nodeps --noscripts bash(rpmdb stays consistent → scanners still enumerate packages), fail closed if a shell survives. (No#comments inside the RUN — a comment line silently breaks the\continuation; that was a second bug found en route.)FROM ubi-micro): delete the shell ubi-micro itself carries (COPY can't remove a base file), fail closed if any survives.Detection fix (real-path aware)
runtime-hardening.sh: forbid/usr/bin/{sh,bash,dash}+ a generic scan for any shell under anybin/sbinpath.verify.pycheck_runtime_script+check_image_manifest.pyFORBIDDEN_BASELINE+ example manifestforbidden_executables: add the real/usr/binshell paths.Validation (empirical, local)
--no-cachebuild → zero shells in the image (docker export | tar -tf | grepfinds none), rpmdb preserved (scanners enumerate 36 pkgs), CA bundle populated,runtime-hardening.shpasses,verify.py cigreen. The CIimage build + hardeningjob re-proves it on a fresh runner.🤖 Generated with Claude Code