Skip to content

infra: harden the dashboard pod like the API (read-only root, no token, namespace) - #35

Merged
devinder-dev merged 1 commit into
mainfrom
infra/dashboard-hardening
Nov 4, 2026
Merged

devinder-dev merged 1 commit into
mainfrom
infra/dashboard-hardening

Conversation

@devinder-dev

Copy link
Copy Markdown
Owner

What

The four follow-ups from my review of #24, so you only have to review them. One file,
k8s/app/dashboard.yaml; the dashboard then matches the API's hardening.

Change Why
namespace: relay on the Deployment, Service and Ingress as in the API's files; a hand-run kubectl apply with the admin kubeconfig would otherwise land in default
automountServiceAccountToken: false nginx never talks to the Kubernetes API, so the pod carries no token
readOnlyRootFilesystem: true + an emptyDir on /tmp (16Mi) nothing in the image may change at runtime, as for the API. nginx-unprivileged writes only under /tmp (pid /tmp/nginx.pid, client_body_temp_path /tmp/client_temp, …), not /var as the old comment said
runAsUser: 101 written out, so it doesn't depend on the image's USER line

Checked

With the real image (relay-dashboard:c145056, pulled anonymously), run the way the pod will run:
--user 101 --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges:

/index.html                → 200
/assets/index-BimGaWWm.js  → 200
/deliveries (fallback)     → 200

The only non-access log line is the IPv6 entrypoint script saying it can't edit the config
(info:, then "Configuration complete; ready for start up"). The manifest parses, and all three
objects carry namespace: relay.

Server dry run for Pod Security (from the laptop, any commit's image tag works):

sed "s/__GIT_SHA__/$(git rev-parse origin/main)/" k8s/app/dashboard.yaml | ssh relay 'kubectl apply --dry-run=server -f -'

If the pods ever failed at runtime anyway, relay-deploy rolls the commit back, which #34 now
tests for the dashboard.

@AbdikhaniAbdirashid AbdikhaniAbdirashid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. This is my file, so I checked each of the four changes against the API's manifests and
against what nginx actually writes, instead of only reading the diff.

It matches the API now, field for field. k8s/app/deployment.yaml has namespace: relay (line
9), automountServiceAccountToken: false (32), runAsUser (37) and readOnlyRootFilesystem: true
(108); service.yaml and ingress.yaml carry the namespace too. The dashboard was the only thing
in k8s/app/ without any of it.

/tmp is genuinely the only writable place nginx needs here. I went through my own
dashboard/nginx.conf looking for anything that writes: no proxy_pass, so no proxy_temp; no
proxy_cache, so nothing under /var/cache/nginx; gzip compresses in memory and needs no temp
file; both locations are try_files on static files. That leaves the pid file and the body temp
dir, which nginx-unprivileged already points at /tmp. 16Mi is plenty for a server that only
answers GETs.

Server dry run, from the manifest on this branch:

deployment.apps/relay-dashboard configured (server dry run)
service/relay-dashboard unchanged (server dry run)
ingress.networking.k8s.io/relay-dashboard unchanged (server dry run)

So Pod Security restricted accepts it, and the two unchanged lines also confirm the namespace
field only writes down where those objects already live - it doesn't move anything.

One thing I want to state because I'll be asked about it. With a read-only root the image's
10-listen-on-ipv6-by-default.sh can no longer rewrite default.conf, which is the info: line
you saw. The consequence is that nginx listens on IPv4 only - fine for us on purpose, not by luck:
the cluster is IPv4-only and we deliberately publish no AAAA records, so Traefik reaches the pod
over IPv4. Worth remembering as the reason that log line is harmless rather than something to
silence.

runAsUser: 101 written out is right for the same reason the API writes 1000: Kubernetes never
looks inside the image, so a base-image bump that changed USER would otherwise change who we run
as without any file in git changing.

Thanks for taking the follow-ups from your own #24 review rather than handing them back to me.

@devinder-dev
devinder-dev merged commit 8d06a1f into main Nov 4, 2026
6 checks passed
@devinder-dev
devinder-dev deleted the infra/dashboard-hardening branch November 4, 2026 10:11
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.

2 participants