You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The operator container image is built on mcr.microsoft.com/devcontainers/base:alpine — the devcontainer base image used by VS Code for development environments. It ships with a full shell, apk, git, vim, sudo, ssh, curl, wget, and a few hundred packages the operator binary does not need at runtime.
For a static Go binary (CGO_ENABLED=0 GOEXPERIMENT=nosystemcrypto, already produced by the existing build stage) the appropriate runtime base is a distroless image such as mcr.microsoft.com/cbl-mariner/distroless/base:2.0 (or the newer mcr.microsoft.com/azurelinux/distroless/base:3.0, staying in the same Microsoft image lineage as the builder).
Current state
operator/src/Dockerfile:
FROM mcr.microsoft.com/oss/go/microsoft/golang:1.25-azurelinux3.0 AS builder
...
RUN GOEXPERIMENT=nosystemcrypto CGO_ENABLED=0 GOOS=... GOARCH=... go build -a -o manager cmd/main.go
# Use Microsoft official image from MCR as minimal base image to package the manager binaryFROM mcr.microsoft.com/devcontainers/base:alpine
WORKDIR /
COPY --from=builder /workspace/manager .
RUN addgroup -S manager && adduser -S manager -G manager
USER manager
The "minimal base" comment is misleading — devcontainers/base:alpine is a development environment image, not a minimal production runtime.
Why this matters for GA
Concern
Today
After distroless switch
Runtime base size
~300–500 MB
~5–10 MB
Pre-installed shell
bash, sh, dash
none
Package manager present
yes (apk)
none
Network tools
curl, wget, ssh
none
CVE surface from base packages
hundreds
~3 (ca-certificates, tzdata, libc)
Trivy / Defender scan noise
constant Alpine CVE churn
near-zero
Container-escape primitives at runtime
many
none
kubectl exec -it ... -- sh post-RCE
works (attacker dream)
no shell to exec
runAsNonRoot: true semantics
hacky — Alpine adduser -S manager produces an unverifiable UID, requiring an explicit runAsUser: 100 workaround in PR #382 to even pass kubelet admission
clean — distroless images declare USER 65532:65532 (nonroot) by default and kubelet can verify it
Real-world impact
Customer security reviews will flag this. Enterprise procurement / SOC2 / FedRAMP / customer security questionnaires routinely ask for the runtime base image and how the attack surface is minimized. Shipping vim, git, and apk in the operator runtime is hard to defend.
CVE noise becomes a support treadmill at GA. Customers running Trivy / Microsoft Defender for Containers / Snyk against AKS clusters with this operator will file CVE tickets weekly. Most will not be exploitable; the support cost of triaging "Alpine CVE-X is in your operator image" repeatedly is large.
It is a fixed-cost, one-shot migration. Done before GA, it is a few-line Dockerfile change. Done after GA, it becomes a coordinated rollout that some customers will resist on the grounds of "you changed our image."
CNPG already does this. The operator's most important upstream dependency ships on a distroless base, with readOnlyRootFilesystem: true, no shell, UID 10001. Customers comparing the two side-by-side notice.
Suggested fix
Replace the runtime stage in operator/src/Dockerfile with something like:
FROM mcr.microsoft.com/cbl-mariner/distroless/base:2.0
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
(or mcr.microsoft.com/azurelinux/distroless/base:3.0 to match the Azure Linux lineage used by the builder stage).
Companion changes:
Remove the adduser / addgroup lines — distroless ships a nonroot user (UID 65532) already.
Update securityContext.runAsGroup to 65532 for symmetry.
Apply the same base-image swap to other Dockerfiles in the repo where applicable:
operator/cnpg-plugins/sidecar-injector/Dockerfile
any future wal-replica Dockerfile when it lands
the gateway image is built downstream from documentdb-local so is out of scope here
Update the security-context documentation under docs/operator-public-documentation/ to reflect the new UID and to advertise the distroless story (helps with the customer-security-questionnaire pain).
Test plan
Build the new image locally and docker run --rm <image> --help to confirm the binary starts under the new base.
Verify TLS connections to the API server still work (needs ca-certificates; both cbl-mariner/distroless/base:2.0 and azurelinux/distroless/base:3.0 include them).
Smoke test in kind: install the chart with the new image, confirm operator pod becomes Ready, create a sample DocumentDB CR, confirm reconciliation succeeds.
Re-run the existing E2E suite.
Re-run the existing image scan in CI (if any) and confirm CVE count drops.
Out of scope
Switching the base for the database / gateway images (they are produced from a different upstream pipeline — see Dockerfile_gateway_deb).
Summary
The operator container image is built on
mcr.microsoft.com/devcontainers/base:alpine— the devcontainer base image used by VS Code for development environments. It ships with a full shell,apk,git,vim,sudo,ssh,curl,wget, and a few hundred packages the operator binary does not need at runtime.For a static Go binary (
CGO_ENABLED=0 GOEXPERIMENT=nosystemcrypto, already produced by the existing build stage) the appropriate runtime base is a distroless image such asmcr.microsoft.com/cbl-mariner/distroless/base:2.0(or the newermcr.microsoft.com/azurelinux/distroless/base:3.0, staying in the same Microsoft image lineage as the builder).Current state
operator/src/Dockerfile:The "minimal base" comment is misleading —
devcontainers/base:alpineis a development environment image, not a minimal production runtime.Why this matters for GA
apk)curl,wget,sshkubectl exec -it ... -- shpost-RCErunAsNonRoot: truesemanticsadduser -S managerproduces an unverifiable UID, requiring an explicitrunAsUser: 100workaround in PR #382 to even pass kubelet admissionUSER 65532:65532(nonroot) by default and kubelet can verify itReal-world impact
vim,git, andapkin the operator runtime is hard to defend.readOnlyRootFilesystem: true, no shell, UID 10001. Customers comparing the two side-by-side notice.Suggested fix
Replace the runtime stage in
operator/src/Dockerfilewith something like:(or
mcr.microsoft.com/azurelinux/distroless/base:3.0to match the Azure Linux lineage used by the builder stage).Companion changes:
adduser/addgrouplines — distroless ships anonrootuser (UID 65532) already.securityContext.runAsUserfrom100(the Alpineadduser -Sworkaround introduced in feat(helm): add pod security, resources, and scheduling knobs to chart #382) to65532.securityContext.runAsGroupto65532for symmetry.operator/cnpg-plugins/sidecar-injector/Dockerfilewal-replicaDockerfile when it landsdocumentdb-localso is out of scope heredocs/operator-public-documentation/to reflect the new UID and to advertise the distroless story (helps with the customer-security-questionnaire pain).Test plan
docker run --rm <image> --helpto confirm the binary starts under the new base.ca-certificates; bothcbl-mariner/distroless/base:2.0andazurelinux/distroless/base:3.0include them).DocumentDBCR, confirm reconciliation succeeds.Out of scope
Dockerfile_gateway_deb).References
securityContexthardening — became necessary partly because the Alpine base lacked a verifiable nonroot user)restrictedgap on injected sidecars)