DUNDER-227: harden the opslevel app workloads with a read-only root filesystem - #92
Conversation
| # OpenShift: set `opslevel.securityContext: {}`. The restricted SCC assigns fsGroup | ||
| # from the namespace's range and rejects a hardcoded value at admission. | ||
| securityContext: | ||
| fsGroup: 1000 |
There was a problem hiding this comment.
might be worth a comment to explain the lack of runAsGroup means that the user default runs with group 0 which is how we can 'read' the app code and such in /home/opslevel
|
Pushed The container The fix moves the guard below the unconditional blocks, so the hardening always renders and the certificate mounts stay conditional. My original verification only ever passed |
…ilesystem The app image now ships its code, vendored gems, and bootsnap cache root-owned and unwritable at runtime, which makes a read-only root filesystem viable for the app containers. readOnlyRootFilesystem is a container-level field and the chart only templated pod-level securityContext, so add a container-level block to web, the four workers, and the scheduler, along with dropping all capabilities and disallowing privilege escalation. Three emptyDir mounts cover everything the app still writes: /home/opslevel/tmp (Rails' default file cache lives at tmp/cache), /home/opslevel/log, and /tmp -- Tempfile backs ActiveStorage uploads and app/models/document.rb, and a read-only rootfs takes /tmp with it. fsGroup: 1000 is what makes those volumes writable, since emptyDir is created root:root 0755. OpenShift installs must clear opslevel.securityContext: the restricted SCC assigns fsGroup from the namespace range and rejects a hardcoded value at admission. Noted in values.yaml. The scheduler had no pod-level securityContext block at all, so it was rendering without fsGroup and would not have been able to write to its own volumes. The init-certs init containers are deliberately left unhardened: they run update-ca-certificates, which writes /etc/ssl/certs on their own root filesystem.
The container securityContext and the tmp//log//tmp mounts were inserted
inside the `{{- if .Values.certificate.enabled }}` guard that wraps the
container's volumeMounts and the pod's volumes keys in the worker and
scheduler templates. With certificate.enabled at its default of false --
which is every install that does not use a custom CA -- none of it
rendered: no readOnlyRootFilesystem, no dropped capabilities, no writable
volumes.
Move the guard below the unconditional blocks so the app hardening always
renders and the certificate mounts stay conditional. web.yaml was already
correct; its volumeMounts and volumes keys are unconditional.
Verified across certificate.enabled and opslevel.tls.enabled in both
states: all six app workloads render readOnlyRootFilesystem with the three
emptyDir mounts, and the certificate/ca/ssl mounts still appear when
enabled. The earlier verification only exercised certificate.enabled=true,
which is why this was missed.
c138910 to
6153f9a
Compare
Companion to the app-image change in
OpsLevel!19844 (DUNDER-227).
That MR makes the app image ship its code, vendored gems, and bootsnap cache
root-owned and unwritable at runtime, leaving
tmp/,log/, and/tmpas theonly paths the app writes. That is what makes a read-only root filesystem viable
here.
What this does
readOnlyRootFilesystemis a container-level field, and the chart onlytemplated pod-level
securityContext, so this adds a container-level block tothe six app workloads —
web, the four workers, andscheduler:Three
emptyDirmounts, via a shared helper, cover everything the app stillwrites:
/home/opslevel/tmpconfig.cache_storeis unset, so Rails 7.2 defaults to a file store attmp/cache/home/opslevel/logCOPY/tmpTempfilebacks ActiveStorage uploads andapp/models/document.rb, and a read-only rootfs takes/tmpwith itAction required for OpenShift installs
Set
opslevel.securityContext: {}.emptyDirvolumes are createdroot:root 0755, so something has to grant the appgroup write — hence the new
fsGroup: 1000default. OpenShift'srestricted-v2SCC assigns
fsGroupfrom the namespace's range and rejects a hardcoded valueat admission, so OpenShift users must clear the pod-level block and let the SCC
inject it. This is called out in
values.yamland in the changie entry.Notes
schedulerhad no pod-levelsecurityContextblock at all, so it was renderingwithout
fsGroupand could not have written to its own volumes. Added.init-certsinit containers are deliberately left unhardened: they runupdate-ca-certificates, which writes/etc/ssl/certson their own rootfilesystem. Same for the scheduler's
migrationsinit container.Verification
helm lintis clean, and all six workloads render as expected:Rendered output was also parsed to confirm every hardened container mounts all
three writable paths, with
certificate.enabled=trueandopslevel.tls.enabled=true.Not cluster-validated. The permission model is verified at the image level and
the templates are verified by rendering, but "does the app actually boot with a
read-only rootfs" needs a real deploy. Highest-risk unknowns are a write path not
found by grepping, and the
fsGroup/emptyDirinteraction.Follow-ups (not in this PR)
opslevel-kubernetesneeds the same container hardening for its rawDeployments (
clusters/new-prod-runners/opssight/*, theetl/*fleet). TheHelmRelease paths inherit these defaults; the raw manifests won't.
config.cache_storebeing unset means each replica keeps its own file cache inits own
emptyDir, with no shared invalidation across web pods. Pointing it atthe Redis already deployed here would fix that and drop
/home/opslevel/tmpfrom the writable set.