Run the pod as non-root with a read-only filesystem
Part of the same security-hardening pass as the last three commits. Neither the Dockerfile nor the chart's Deployment set any securityContext at all, so the container ran as root by default — scratch has no /etc/passwd for a USER directive to resolve against, so nobody had set one. Dockerfile now ends with USER 65532:65532 (numeric, since scratch has no user database; 65532 is the common "nonroot" convention, distroless's own uid). The chart's Deployment adds a matching pod-level securityContext (runAsNonRoot, runAsUser/runAsGroup: 65532, seccompProfile: RuntimeDefault) plus per-container hardening (allowPrivilegeEscalation: false, capabilities dropped, readOnlyRootFilesystem: true) on both the app container and the wait-for-postgres init container — neither writes anything to disk, so the root filesystem can stay read-only. Verified with helm-lint and a manual `helm template` render of both the terdut-server and terdut-demo charts. Not yet verified: an actual pod starting with these in place — readOnlyRootFilesystem is exactly where a non-obvious write (a temp file, a cache dir) would surface as a crash rather than a lint error, so that needs a real rollout to confirm. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,4 +24,10 @@ FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /terdut /terdut
|
||||
EXPOSE 8080
|
||||
# Numeric, not a name: scratch has no /etc/passwd for one to resolve against,
|
||||
# and Docker's USER accepts a bare UID:GID without it. 65532 is the common
|
||||
# "nonroot" convention (distroless's own uid), chosen so the chart's pod
|
||||
# securityContext (runAsNonRoot, runAsUser: 65532) matches what the image
|
||||
# already runs as rather than fighting it.
|
||||
USER 65532:65532
|
||||
ENTRYPOINT ["/terdut"]
|
||||
|
||||
@@ -25,11 +25,30 @@ spec:
|
||||
{{- include "terdut-server.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
enableServiceLinks: false
|
||||
# Pod-wide default; both containers below run as this UID regardless of
|
||||
# what their own image would otherwise pick (postgres:17-alpine's
|
||||
# pg_isready needs no particular user, and 65532 is what the app image
|
||||
# itself runs as now — see the Dockerfile's USER). seccompProfile here
|
||||
# rather than per-container: there is no reason it would ever differ
|
||||
# between them.
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
{{- if .Values.database.waitForPostgres.enabled }}
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: "{{ .Values.database.waitForPostgres.image.repository }}:{{ .Values.database.waitForPostgres.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.database.waitForPostgres.image.pullPolicy }}
|
||||
# No capability this loop needs, and nothing in it writes to disk:
|
||||
# sh, pg_isready, echo and sleep all run read-only.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: TERDUT_DB_DSN
|
||||
value: {{ required "database.dsn is required" .Values.database.dsn | quote }}
|
||||
@@ -46,6 +65,13 @@ spec:
|
||||
- name: terdut-server
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
# scratch, nothing to write: the binary keeps no local state and
|
||||
# writes nothing to disk, so the root filesystem can stay read-only.
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
|
||||
Reference in New Issue
Block a user