Repository navigation
fix(security): scale-zero-pg compute pods meet Pod Security restricted on every container (#2564) - #2585
Merged
AhmedElBanna80 merged 3 commits intoOct 22, 2026
Conversation
…d on every container (#2564) Every container of the rendered compute pods (writer + read replica, init containers included) now carries allowPrivilegeEscalation=false, drop ALL, seccomp RuntimeDefault and runAsNonRoot. The images declare USER by name (postgres / neon, both 1000:1000 in the pinned digests), which the kubelet cannot verify as non-root, so runAsUser/runAsGroup 1000 are set explicitly. Guarded by the pod-security-admission restricted evaluator over the real render and the break-glass template, with a negative control. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…e defaults to pinned digests (#2564) The run-as identity was a hardcoded 1000 although the operator lets the compute and init images be overridden. RenderConfig now carries ComputeUID/GID and InitUID/GID (default 1000), overridable with APPDB_COMPUTE_UID/GID and APPDB_INIT_UID/GID, validated as numeric and > 0 at start-up. A hand-maintained digest -> uid table, enforced against the render defaults and every digest pinned under deploy/, goes red when a digest is bumped without recording its uid. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
…#2564) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx
This branch was successfully deployed
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.
Closes #2564
Escalation trigger acknowledged: security invariant — hardens the container securityContext of the pods the scale-zero-pg gateway/operator renders (pod-admission surface); also adds k8s.io/pod-security-admission as a test-only dependency of the gateway module.
What changed
Every container of the rendered per-app compute pods (writer + read replica, init containers included) now carries
allowPrivilegeEscalation: false,capabilities.drop: [ALL],seccompProfile: RuntimeDefault,runAsNonRoot: true,runAsUser/runAsGroup: 1000. The same fields are on the break-glassdeploy/compute-app.template.yaml(the structural parity guard requires the template to follow the render).Image uids (read from /etc/passwd of the pinned digests via
crane export)compute-node-v17:8464@sha256:13ab146d…— configUser: postgres, passwdpostgres:1000:1000neon:8464@sha256:7a4f1249…(init containers) — configUser: neon, passwdneon:1000:1000Both images name their user rather than a number, so
runAsNonRootalone would fail in the kubelet ("image has non-numeric user … cannot verify user is non-root"; reproduced below). Numeric 1000:1000 is the identity the pods already ran as, so file ownership and the boot are unchanged. No capability is needed (running containers show CapEff/CapPrm/CapBnd all zero, NoNewPrivs 1, Seccomp 2); no exception is required.fsGroupis deliberately not set (it would alter secret-volume modes; the TLS key is already copied to a 0600 path by the entrypoint).Tests
gateway/internal/appdb/render_pod_security_test.go: pod-security-admissionrestrictedevaluator (latest) over the REALRenderDeployment/RenderRODeploymentoutput and over the parsed template, plus per-field assertions per container, plus a negative control (the pre-change shape with no container securityContext is rejected).go build,go vet,go test ./... -count=1green in the gateway module (go 1.26.9);VALIDATE_STATIC=1 deploy/_validate.shgreen.--- FAILpresent AND no build failure, exact-once anchors, file restored each time): 10/10 killed by assertion — each of the six fields removed, securityContext dropped from the resolve-lsn init container and from the writer compute container, uid changed to 999, andrunAsNonRootremoved from the template. (A first run had 4 mutants that merely broke the build via unused locals; the harness now rejects those as invalid and keeps the locals referenced.)Live check (orbstack cluster, throwaway namespace with
pod-security.kubernetes.io/enforce=restricted)Pods built from the real render (volumes/env trimmed, commands overridden to
id//proc/self/status; real pinned images):uid=1000(neon), computeuid=1000(postgres),Cap*: 0,NoNewPrivs: 1,Seccomp: 2;compute_ctlexecutes.runAsUserremoved:Init:CreateContainerConfigError — container has runAsNonRoot and image has non-numeric user (neon). This is why the uid is explicit.deploy/_verify-*.sh) is the standing boot check and was not run here. Image pulls ran amd64 under emulation.Docs
packages/scale-zero-pg/docs/operations.mdgains a "Pod Security: per-app compute pods meet restricted" section (fields, the numeric-uid reason, the check-uid-on-image-bump instruction, and the explicit note that the base-tier static manifests still set only pod-level seccomp).Deferred
Base-tier static manifests
20-compute.yaml,25-compute-warm.yaml,26-compute-ro.yamlare not in this issue's render path and still carry only pod-level seccomp (documented in the section above).Round 2 (code review: the uid was hardcoded although the images are overridable)
RenderConfignow carriesComputeUID/ComputeGIDandInitUID/InitGID(default 1000,DefaultImageUID). The compute container uses the compute pair, the init containers (writer and read replica) the init pair.applyImageEnv:APPDB_COMPUTE_UID/GIDandAPPDB_INIT_UID/GID. Two pairs rather than one because the compute and init images are different images with their own users; overriding one must not silently re-identify the other. Each value must be a decimal number in [1, 2147483647];0, negatives, names, decimals and overflow stop the operator at start-up with a message naming the variable and the value.image_uid_table_test.goholds a hand-maintained digest -> uid:gid table (no network). One test requires theDefaultRenderConfigimages to be in the table with matching defaults; another scans every file underdeploy/for pinnedneondatabase/{compute-node-v*,neon}digests and requires each to be in the table. A digest bump without a uid entry fails.docs/operations.mddocuments the four variables, the numeric-uid requirement, how to read the real uid from an image, and the failure mode of a wrong one.Tests added:
TestApplyImageEnv{Defaults,Overrides,RejectsBadIdentity},TestMainAppliesImageOverridesOnlyThroughApplyImageEnv(source scan, sincemain()is not unit-testable),TestRenderHonoursImageIdentityOverrides,TestDefaultImagesHaveARecordedUID,TestEveryPinnedNeonImageInDeployHasARecordedUID.go build,go vet,go test ./... -count=1green;VALIDATE_STATIC=1 deploy/_validate.shgreen.Mutation results (by exit code AND the named test appearing as
--- FAIL, no build failure, exact-once anchors, file restored each time): 25 mutants, all killed by assertion. Env wiring: dropping theAPPDB_INIT_UIDorAPPDB_COMPUTE_GIDrow, looseningn < 1ton < 0, dropping the upper bound, bypassingapplyImageEnvinmain(), re-reading an image env raw. Render: swapping the compute and init pair on the compute container and onresolve-lsn. Table completeness: removing either table row, a wrong uid in the table, a wrong default uid/gid, and a digest bumped without a uid entry inrender.goand incompute-app.template.yaml. The round-1 field mutants (six fields, container-level removal, templaterunAsNonRoot, default uid) were re-run against the new code, all killed. One mutant survived on the first run (upper bound removed); the bad-value test now includes2147483648and kills it.🤖 Generated with Claude Code
https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx