Skip to content

fix(security): scale-zero-pg compute pods meet Pod Security restricted on every container (#2564) - #2585

Merged
AhmedElBanna80 merged 3 commits into
integration/v2from
security/2564-szpg-compute-restricted
Oct 22, 2026
Merged

AhmedElBanna80 merged 3 commits into
integration/v2from
security/2564-szpg-compute-restricted

Conversation

@AhmedElBanna80

@AhmedElBanna80 AhmedElBanna80 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

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-glass deploy/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… — config User: postgres, passwd postgres:1000:1000
  • neon:8464@sha256:7a4f1249… (init containers) — config User: neon, passwd neon:1000:1000

Both images name their user rather than a number, so runAsNonRoot alone 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. fsGroup is 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-admission restricted evaluator (latest) over the REAL RenderDeployment / RenderRODeployment output 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=1 green in the gateway module (go 1.26.9); VALIDATE_STATIC=1 deploy/_validate.sh green.
  • Mutation proof (exit code AND --- FAIL present 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, and runAsNonRoot removed 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):

  • writer and read-replica pods were ADMITTED and RUNNING: init uid=1000(neon), compute uid=1000(postgres), Cap*: 0, NoNewPrivs: 1, Seccomp: 2; compute_ctl executes.
  • same writer pod with runAsUser removed: Init:CreateContainerConfigError — container has runAsNonRoot and image has non-numeric user (neon). This is why the uid is explicit.
  • Not proven: a full Postgres boot against a live storage plane. There is no scale-zero-pg kind lane in CI (the workflow runs go/turbo + static manifest contracts); the OKE drill battery (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.md gains 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.yaml are 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)

  • RenderConfig now carries ComputeUID/ComputeGID and InitUID/InitGID (default 1000, DefaultImageUID). The compute container uses the compute pair, the init containers (writer and read replica) the init pair.
  • Operator env, set next to the image overrides in applyImageEnv: APPDB_COMPUTE_UID/GID and APPDB_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.
  • Default tied to the pinned digests: image_uid_table_test.go holds a hand-maintained digest -> uid:gid table (no network). One test requires the DefaultRenderConfig images to be in the table with matching defaults; another scans every file under deploy/ for pinned neondatabase/{compute-node-v*,neon} digests and requires each to be in the table. A digest bump without a uid entry fails.
  • Docs: docs/operations.md documents 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, since main() is not unit-testable), TestRenderHonoursImageIdentityOverrides, TestDefaultImagesHaveARecordedUID, TestEveryPinnedNeonImageInDeployHasARecordedUID. go build, go vet, go test ./... -count=1 green; VALIDATE_STATIC=1 deploy/_validate.sh green.

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 the APPDB_INIT_UID or APPDB_COMPUTE_GID row, loosening n < 1 to n < 0, dropping the upper bound, bypassing applyImageEnv in main(), re-reading an image env raw. Render: swapping the compute and init pair on the compute container and on resolve-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 in render.go and in compute-app.template.yaml. The round-1 field mutants (six fields, container-level removal, template runAsNonRoot, 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 includes 2147483648 and kills it.

🤖 Generated with Claude Code

https://claude.ai/code/session_0198oVxWkEEDJJnVhES6ULnx

…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
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
knext-docs Ready Ready Preview Oct 22, 2026 4:15pm UTC

AhmedElBanna80 and others added 2 commits October 22, 2026 18:11
…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
@AhmedElBanna80
AhmedElBanna80 merged commit 29c6caf into integration/v2 Oct 22, 2026
60 checks passed

This branch was successfully deployed

1 active deployment
Preview — 13a32aec Deployed Oct 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant