feat(chart): add optional readOnlyRootFilesystem hardening (empirically verified) - #177
Open
danielqb wants to merge 1 commit into
Open
feat(chart): add optional readOnlyRootFilesystem hardening (empirically verified)#177danielqb wants to merge 1 commit into
danielqb wants to merge 1 commit into
Conversation
…ly verified) ## Problem None of the containers (php-fpm, nginx, mariadb, redis) could run with a read-only root filesystem, a standard hardening control (reduces the blast radius of a container compromise: an attacker can't write a webshell or modify binaries even with code execution). ## Approach Rather than guessing which paths need to stay writable, each image was tested directly with `docker run --read-only` (and, for the final pass, combined with the exact runAsUser/capabilities this chart already sets) to get ground truth instead of assumptions: - eftechcombr/glpi:php-fpm-11.0.8 - starts clean with ZERO extra writable paths. - eftechcombr/glpi:nginx-11.0.8 - fails without /tmp writable (`mkdir() "/tmp/proxy_temp" failed (Read-only file system)`); works with only /tmp added. - mariadb:11.4 - fails without /tmp (InnoDB temp files) and /run/mysqld (unix socket bind); works with those two plus the existing /var/lib/mysql PVC. Verified with an actual `SELECT 1` after startup. - redis:7.0-alpine - runs and SAVE succeeds even fully read-only when the image's own VOLUME-declared /data gets an implicit writable mount from the Docker daemon, but Kubernetes/CRI runtimes don't reliably replicate that behavior, so an explicit emptyDir is safer than relying on it. Final combined validation (uid matching this chart's securityContext, --cap-drop ALL, --read-only, plus only the specific tmpfs paths listed above): all four images started/ran cleanly, and mariadb/redis were exercised with real queries (SELECT 1, SET/GET) to confirm they weren't just booting but functioning. ## Fix - Added `readOnlyRootFilesystem: false` (opt-in, not default-on) to glpi.securityContext, mariadb.securityContext, and redis.securityContext in values.yaml, each documented with exactly which paths the chart mounts when enabled and why. - When glpi.securityContext.readOnlyRootFilesystem is true: mounts an emptyDir at /tmp on php-fpm, nginx, all 5 init Jobs, and the cronjob (they all share this securityContext). - When mariadb.securityContext.readOnlyRootFilesystem is true: mounts emptyDirs at /tmp and /run/mysqld on the MariaDB StatefulSet, and /tmp on the mariadb-timezone Job. - When redis.securityContext.readOnlyRootFilesystem is true: mounts an emptyDir at /data (Redis has no PVC in this chart either way - it's used purely as an ephemeral object cache, so this changes nothing about data durability). - All additions are conditional and add zero volumes/mounts when the corresponding flag stays false (the default), so this is a strictly additive, opt-in change. ## Testing - helm lint: 0 failures - helm template (default): 0 occurrences of the new tmp/data volumes/mounts anywhere in the rendered output - helm template with all three readOnlyRootFilesystem flags set to true: correct emptyDir mounts appear on exactly the expected 11 workloads (2 Deployments=php-fpm+nginx, 1 Deployment=redis, 6 Jobs=5 glpi jobs + mariadb-timezone, 1 CronJob, 1 StatefulSet=mariadb); mariadb gets both /tmp and /run/mysqld, redis gets /data - docker run --read-only --cap-drop ALL --user <matching-uid> against all four images individually: all start/run correctly with only the documented paths writable; mariadb and redis verified with live queries, not just process liveness
eduardofraga
requested changes
Jul 28, 2026
eduardofraga
left a comment
Contributor
There was a problem hiding this comment.
Hi, please remove redis and mariadb because was changed by sub-chart from helmforge. Next, move files from kubernetes/glpi to helm/
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.
Problem
None of the containers (php-fpm, nginx, mariadb, redis) could run with a
read-only root filesystem, a standard hardening control (reduces the
blast radius of a container compromise: an attacker can't write a
webshell or modify binaries even with code execution).
Approach
Rather than guessing which paths need to stay writable, each image was
tested directly with
docker run --read-only(and, for the final pass,combined with the exact runAsUser/capabilities this chart already sets)
to get ground truth instead of assumptions:
paths.
(
mkdir() "/tmp/proxy_temp" failed (Read-only file system)); workswith only /tmp added.
(unix socket bind); works with those two plus the existing
/var/lib/mysql PVC. Verified with an actual
SELECT 1after startup.image's own VOLUME-declared /data gets an implicit writable mount from
the Docker daemon, but Kubernetes/CRI runtimes don't reliably replicate
that behavior, so an explicit emptyDir is safer than relying on it.
Final combined validation (uid matching this chart's securityContext,
--cap-drop ALL, --read-only, plus only the specific tmpfs paths listed
above): all four images started/ran cleanly, and mariadb/redis were
exercised with real queries (SELECT 1, SET/GET) to confirm they weren't
just booting but functioning.
Fix
readOnlyRootFilesystem: false(opt-in, not default-on) toglpi.securityContext, mariadb.securityContext, and redis.securityContext
in values.yaml, each documented with exactly which paths the chart
mounts when enabled and why.
emptyDir at /tmp on php-fpm, nginx, all 5 init Jobs, and the cronjob
(they all share this securityContext).
emptyDirs at /tmp and /run/mysqld on the MariaDB StatefulSet, and /tmp
on the mariadb-timezone Job.
emptyDir at /data (Redis has no PVC in this chart either way - it's
used purely as an ephemeral object cache, so this changes nothing
about data durability).
corresponding flag stays false (the default), so this is a strictly
additive, opt-in change.
Testing
volumes/mounts anywhere in the rendered output
correct emptyDir mounts appear on exactly the expected 11 workloads
(2 Deployments=php-fpm+nginx, 1 Deployment=redis, 6 Jobs=5 glpi jobs +
mariadb-timezone, 1 CronJob, 1 StatefulSet=mariadb); mariadb gets both
/tmp and /run/mysqld, redis gets /data
four images individually: all start/run correctly with only the
documented paths writable; mariadb and redis verified with live
queries, not just process liveness