Day 150 — Image hardening & distroless¶
Month 6 · Week 2 · ⬅ Day 149 · Day 151 ➡ · Journal index
🎯 Learning Objective¶
Shrink the attack surface of a Go container: choose scratch vs distroless, run as non-root, mount a read-only root filesystem, and drop Linux capabilities.
📚 Topics¶
scratchvsgcr.io/distroless/static— what each gives you- Non-root
USER, read-only rootfs, dropped capabilities, no shell - TLS roots, timezone data, and
/etc/passwdin minimal images
📖 Reading / Sources¶
- GoogleContainerTools/distroless
- Docker — running containers as non-root
-
crypto/x509.SystemCertPool - Kubernetes — SecurityContext
📝 Notes¶
- Smaller image = smaller attack surface. No shell, no package manager, no libc means far fewer CVEs and no
shfor an attacker to pivot into → [[image-hardening]]. scratchis empty: no CA certs, no/etc/passwd, no tzdata. distrolessstaticadds exactly those few essentials (ca-certificates,/etc/passwdwith anonrootuser, tzdata) but still no shell → [[distroless]]. UsestaticforCGO_ENABLED=0Go binaries;baseif you need glibc.- A Go program calling HTTPS needs CA roots; on
scratchyou mustCOPY/etc/ssl/certs/ca-certificates.crtfrom the builder, orx509.SystemCertPoolreturns nothing and TLS fails. distroless includes them. - Run as non-root. Set
USER nonroot:nonroot(distroless ships UID 65532) or a numeric UID. A root process that escapes the container is root on issues; a UID 65532 process is far less dangerous. - Harden at runtime too:
--read-onlyroot filesystem (+ atmpfsfor/tmpif needed),--cap-drop=ALL, and--security-opt=no-new-privileges. In Kubernetes these map tosecurityContextfields (runAsNonRoot,readOnlyRootFilesystem,capabilities.drop). - If you embed timezone data, import
_ "time/tzdata"so the zoneinfo is compiled in and you don't depend on the OS tzdata at all → [[tzdata]]. - Pin image tags by digest (
@sha256:...) for reproducibility;:latestdrifts.
💻 Code Examples¶
# syntax=docker/dockerfile:1
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd/app
# distroless: CA certs + tzdata + a nonroot user, but NO shell or libc.
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER nonroot:nonroot # UID 65532, set by the base image
ENTRYPOINT ["/app"]
# run with: docker run --read-only --cap-drop=ALL \
# --security-opt=no-new-privileges --tmpfs /tmp myimg
The Go side can verify it actually has TLS roots at startup:
// Fail fast if the image shipped without CA certificates.
pool, err := x509.SystemCertPool()
if err != nil || pool == nil {
log.Fatal("no system CA roots: TLS to external services will fail")
}
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Resolve secrets from mounted files (non-root friendly) | ✅ | exercises/month-06/week-2/secretsource |
🐛 Mistakes Made¶
- HTTPS calls failed with "x509: certificate signed by unknown authority" on a
scratchimage — no CA bundle. Switched to distrolessstatic, which bundlesca-certificates. - Logged the time in UTC only on a
scratchimage because there was no tzdata. Addedimport _ "time/tzdata"to embed it.
❓ Open Questions¶
- distroless
:debugships a busybox shell forkubectl execdebugging — is it safe to use only in staging? (Yes; keep it out of prod images.)
🧠 Active Recall (answer without looking)¶
- Q: Your
scratch-based Go service can't make HTTPS calls. Most likely cause?A
No CA root certificates in the image. scratch is empty, so x509.SystemCertPool finds nothing. Copy /etc/ssl/certs/ca-certificates.crt from the builder, or use distroless static, which bundles them.
2. Q: Name three runtime hardening flags and what they prevent. A
--read-only (no writes to the rootfs → no dropping tools/persistence), --cap-drop=ALL (remove Linux capabilities like NET_RAW), --security-opt=no-new-privileges (block setuid privilege escalation). Plus running as non-root USER.
🪶 Feynman Reflection¶
A hardened image is a sealed room with one door (the binary) and nothing else inside — no shell to run, no tools to grab, no root keys lying around. If an attacker gets in through the app, there's nowhere to go and nothing to use.
🕳️ Knowledge Gaps¶
- Image signing/provenance (cosign, SLSA) — comes up with the CI pipeline on [[day-151]].
✅ Summary¶
I can pick the right minimal base, run non-root with a read-only rootfs and dropped capabilities, and make sure the image still carries CA roots and tzdata my Go code needs.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 151: automate build → test → vulnerability scan in GitHub Actions.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): image hardening & distroless (day 150)