Add OpenShift compatibility: rootless, arbitrary-UID support - #432
Add OpenShift compatibility: rootless, arbitrary-UID support#432jfstenuit wants to merge 20 commits into
Conversation
…ID support for OpenShift MISP's containers relied on running as root and dropping privilege at runtime (supervisord as root, ~85 `sudo -u www-data` calls, nginx binding 80/443, php-fpm's -R flag, chown-based permission enforcement on every start). None of this works under OpenShift's restricted/restricted-v2 Security Context Constraint, which always runs containers as an arbitrary, unpredictable UID, never root, and forbids privilege escalation. Rather than special-case OpenShift, all three images now run as their final identity from PID 1 everywhere (Docker Compose, Kubernetes, Helm): - misp-core: removed the sudo package and every sudo invocation in configure_misp.sh/utilities.sh; dropped php-fpm's -R flag and all supervisor `user=` directives; nginx moved to unprivileged ports 8080/8443; every system path the entrypoint chain writes to (nginx, php-fpm, supervisord, rsyslog config dirs) is now baked at build time with group-0 ownership, group=owner permissions, and setgid on directories, so files stay accessible regardless of which UID the container actually runs as. Config file rewriting (bootstrap.php, database.php, email.php, app/files seeding) switched from chmod-then-edit-in-place to copy/recreate-then-edit, since chmod and in-place writes require file ownership, not just group-write access. rsyslog's imuxsock module (needs a writable /dev/log) and update-ca-certificates (needs a writable system trust store) are now root-only, skipped otherwise, since neither is achievable without root or a writable image layer. - misp-modules/misp-guard: their service users are now supplementary members of group 0, and their writable directories (/custom, /home/misp, /srv, /home/guard, the shared mitmproxy CA volume) are group-0-owned with setgid, matching the same arbitrary-UID convention. - docker-compose.yml/Makefile: container-internal ports updated to 8080/8443; the sudo-only AUDIT_WRITE capability dropped; added `make init-dirs` since misp-core can no longer chown its bind-mounted host directories itself on first start. - Helm chart: misp.securityContext no longer pins runAsUser/runAsGroup, letting OpenShift assign its own; mariadb/valkey's global.compatibility.openshift.adaptSecurityContext made explicit; documented the bitnamilegacy registry as an accepted short-term tradeoff for OpenShift compatibility on the DB/cache layer. - Raw kubernetes/manifests: replaced fixed fsGroup/runAsUser with restricted-compliant securityContext (no fixed UID, capabilities dropped, no privilege escalation); documented that the stock mariadb/redis images here aren't arbitrary-UID-safe on a fresh PVC and that the Helm chart is the recommended OpenShift path.
- default admin account back to an e-mail address - properly start supervisord and background workers - use a dynamically generated random password for supervisord
|
Thanks for this. We might need to give priority to this though at this stage #430 Would a rebase possible (some of the concerns should already be addressed by that PR) once we merge? |
Clearly, this goes in the right direction. The new nginx container is natively Openshift-compatible and the move to unprivileged ports aligns with the security requirements. After that change, I'll still have to adapt the init scripts and remove any leftover "sudo" or "chown" along the way. There is also the "-R" flag of php-fpm that needs to be dropped. Also, the whole helm chart needs to be reviewed and aligned with the new Dockerfile. I suggest I start a new branch once #430 is merged, with two change sets :
|
|
@jfstenuit Nice! I had the In addition to the permission and UID changes I planned to switch the FPM / PHP config to a template based syntax as the nginx config in #430 instead of all the sed commands. Maybe you're interested in integrating that in your PR too? PHP templates# docker-compose.yml
services:
misp-core:
tmpfs:
- /opt/php:mode=0755,uid=33,gid=33# core/Dockerfile
ENV PHP_INI_SCAN_DIR="/etc/php/${PHP_PACKAGE_VERSION}/conf.d:/opt/php/conf.d"
...
# overwrite global php-fpm config to load dynamic pool configs
COPY <<-EOF /etc/php/${PHP_PACKAGE_VERSION}/fpm/php-fpm.conf
[global]
pid = /run/php/php${PHP_PACKAGE_VERSION}-fpm.pid
error_log = /var/log/php${PHP_PACKAGE_VERSION}-fpm.log
include=/etc/php/${PHP_PACKAGE_VERSION}/fpm/pool.d/*.conf
include=/opt/php/pool.d/*.conf
EOF# core/files/entrypoint_fpm.sh
generate_php_config() {
local REDIS_HOST_WITH_SCHEME
local REDIS_PASSWORD_ESCAPED
local STATUS_LISTEN
mkdir -p /opt/php/{pool.d,conf.d}
REDIS_HOST_WITH_SCHEME=$(echo "$REDIS_HOST" | grep -E '^\w+://' || echo "tcp://$REDIS_HOST")
REDIS_PASSWORD_ESCAPED=$(printf '%s\n' "$REDIS_PASSWORD" | sed -e 's/[\/&]/\\&/g')
if [[ "$ENABLE_REDIS_EMPTY_PASSWORD" == "true" ]]; then
REDIS_SAVE_PATH="${REDIS_HOST_WITH_SCHEME}:${REDIS_PORT}"
elif [[ -n "$REDIS_PASSWORD" ]]; then
REDIS_SAVE_PATH="${REDIS_HOST_WITH_SCHEME}:${REDIS_PORT}?auth=${REDIS_PASSWORD_ESCAPED}"
else
echo "ERROR: REDIS_PASSWORD is not set but ENABLE_REDIS_EMPTY_PASSWORD is false. Please set REDIS_PASSWORD or enable ENABLE_REDIS_EMPTY_PASSWORD=true for passwordless Redis."
exit 1
fi
# FPM listen address
if [[ -n "$PHP_LISTEN_FPM" ]]; then
FPM_LISTEN=$([[ "$DISABLE_IPV6" == "true" ]] && echo "0.0.0.0:9002" || echo "[::]:9002")
fi
# FPM status block
if [[ -n "$FASTCGI_LISTEN_STATUS" ]]; then
STATUS_LISTEN=$([[ "$DISABLE_IPV6" == "true" ]] && echo "0.0.0.0:9003" || echo "[::]:9003")
FPM_STATUS_BLOCK="pm.status_listen=${STATUS_LISTEN}"$'\n'"pm.status_path=/fpm-status"
else
FPM_STATUS_BLOCK=""
fi
# pm.max_spare_servers: must be >= pm.start_servers
if [[ "$PHP_FCGI_START_SERVERS" -gt "$PHP_FCGI_SPARE_SERVERS" ]]; then
PHP_FCGI_MAX_SPARE_SERVERS="$PHP_FCGI_START_SERVERS"
else
PHP_FCGI_MAX_SPARE_SERVERS="$PHP_FCGI_SPARE_SERVERS"
fi
# --- render templates ---
export REDIS_SAVE_PATH FPM_LISTEN FPM_STATUS_BLOCK PHP_FCGI_MAX_SPARE_SERVERS
envsubst < /opt/php/templates/99-runtime.ini > /opt/php/conf.d/99-runtime.ini
envsubst < /opt/php/templates/99-www.conf > /opt/php/pool.d/99-www.conf
}# core/files/opt/php/templates/99-runtime.ini
memory_limit=${PHP_MEMORY_LIMIT}
max_execution_time=${PHP_MAX_EXECUTION_TIME}
upload_max_filesize=${PHP_UPLOAD_MAX_FILESIZE}
max_file_uploads=${PHP_MAX_FILE_UPLOADS}
post_max_size=${PHP_POST_MAX_SIZE}
max_input_time=${PHP_MAX_INPUT_TIME}
date.timezone=${TZ}
session.save_handler=redis
session.save_path='${REDIS_SAVE_PATH}'
session.sid_length=64
session.use_strict_mode=1# core/files/opt/php/templates/99-www.conf
listen=${FPM_LISTEN}
pm.max_children=${PHP_FCGI_CHILDREN}
pm.start_servers=${PHP_FCGI_START_SERVERS}
pm.min_spare_servers=${PHP_FCGI_SPARE_SERVERS}
pm.max_spare_servers=${PHP_FCGI_MAX_SPARE_SERVERS}
pm.max_requests=${PHP_FCGI_MAX_REQUESTS}
${FPM_STATUS_BLOCK} |
…er than from secret store
…swd file, as expected) - now runs via libnsswrapper to ensure the name 'www-data' is mapped to the OpenShift random UID
readinessProbe was permanently broken - it grepped /misp/readiness/ready.log, a file nothing ever wrote. Fixed by doing a simple httpGet on port 8080
customize_misp.sh hung forever polling ${BASE_URL} - now polling localhost instead.
|
Really appreciate all the work that's gone into this! I tried deploying the chart myself and ran into a couple of issues. I managed to fix most of them, but after that the pod still ends up in a crash loop because:
At this point, I think I'll just sit quietly and wait for the fix to be merged. 😅 Thanks again for all the work you're putting into this! |
…ainer Every other container in this chart already omits runAsUser so OpenShift restricted/restricted-v2 SCC can assign an in-range UID. This init container was missed, and its hardcoded 1337 falls outside the per-namespace UID range OpenShift enforces, causing new rollouts to fail SCC validation entirely.
entrypoint.sh only ever read the plain MYSQL_PASSWORD/REDIS_PASSWORD
env vars, silently falling back to the upstream defaults ("example",
"redispassword") whenever a deployment supplies credentials via the
Docker/Kubernetes secret-file convention (*_PASSWORD_FILE) instead -
which is exactly what the openshift-ready Helm chart does. This made
every DB/Redis auth attempt in init_mysql()/MYSQL_CMD use the literal
default password regardless of what was actually mounted, no matter
how correct the Secret, MariaDB, or chart configuration were.
whoami succeeding is not sufficient - CRI-O (and similar runtimes) auto- register an arbitrary UID in /etc/passwd itself once the file is group- writable, naming the entry after the raw UID rather than any name MISP recognises. That made the prior check always false on OpenShift, silently skipping the nss_wrapper www-data alias it was meant to set up. Without it, every cake command (including the Admin runUpdates that applies pending DB schema migrations on every boot) refuses to run under an unrecognised OS user, leaving the schema permanently incomplete no matter how many times the app restarts.
phpredis defaults redis.session.locking_enabled to off. Without it, concurrent requests from the same browser session (MISP fires plenty of them in parallel) race to write the same session key - the last writer silently wins, which can clobber a CSRF token a previous response already sent to the browser and trips CSRF protection on the next form submit. lock_expire caps how long a held lock can block other requests if the worker holding it dies mid-request.
phpredis defaults redis.session.locking_enabled to off. Without it, concurrent requests from the same browser session (MISP fires plenty of them in parallel) race to write the same session key - the last writer silently wins, which can clobber a CSRF token a previous response already sent to the browser and trips CSRF protection on the next form submit. lock_expire caps how long a held lock can block other requests if the worker holding it dies mid-request. Written into mods-available/ and enabled via phpenmod, same mechanism already used for the other optional modules above it - /etc/php/*/ only matches the shared version dir (e.g. /etc/php/8.4/), not the per-SAPI conf.d/ directories directly.
MISP's images assumed a fixed UID and root-then-drop-privilege startup (
sudo,chown, binding privileged ports, php-fpm'suser/group/listen.owner/listen.groupdirectives), which conflicts with OpenShift'srestrictedSCC - pods run under a randomly assigned, non-predictable UID with primary GID0, never root, and never withCAP_CHOWN/CAP_SETUID.This PR reworks
misp-core,misp-modules, andmisp-guardto start as their final identity instead of dropping into it, following the standard "support arbitrary user IDs" container pattern (owner + group0,g=upermissions, setgid on directories so new files stay group-writable regardless of the runtime UID). Docker Compose, the raw Kubernetes manifests, and the Helm chart are all updated to match - including moving nginx off privileged ports 80/443, removing hardcodedrunAsUser/fsGroupfrom pod security contexts, and generating all credentials (admin, API key, supervisord, MariaDB) as strong random secrets instead of static defaults. Non-OpenShift usage (plain Docker/Compose/vanilla k8s with a fixed UID) is unaffected - the default UID (33/www-data) is kept and simply added to supplementary group0.