Skip to content

Add OpenShift compatibility: rootless, arbitrary-UID support#432

Open
jfstenuit wants to merge 10 commits into
MISP:masterfrom
jfstenuit:openshift-ready
Open

Add OpenShift compatibility: rootless, arbitrary-UID support#432
jfstenuit wants to merge 10 commits into
MISP:masterfrom
jfstenuit:openshift-ready

Conversation

@jfstenuit

Copy link
Copy Markdown

MISP's images assumed a fixed UID and root-then-drop-privilege startup (sudo, chown, binding privileged ports, php-fpm's user/group/listen.owner/listen.group directives), which conflicts with OpenShift's restricted SCC - pods run under a randomly assigned, non-predictable UID with primary GID 0, never root, and never with CAP_CHOWN/CAP_SETUID.

This PR reworks misp-core, misp-modules, and misp-guard to start as their final identity instead of dropping into it, following the standard "support arbitrary user IDs" container pattern (owner + group 0, g=u permissions, 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 hardcoded runAsUser/fsGroup from 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 group 0.

jfstenuit added 10 commits July 8, 2026 13:29
…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
@ostefano

Copy link
Copy Markdown
Collaborator

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?

@jfstenuit

Copy link
Copy Markdown
Author

There is ~40% overlap with the work of PR #430 . Probably better of create a new PR once #430 has been merged.

@ostefano

Copy link
Copy Markdown
Collaborator

There is ~40% overlap with the work of PR #430 . Probably better of create a new PR once #430 has been merged.

Could you do a review of that PR and check whether the direction is compatible with yours?

@jfstenuit

Copy link
Copy Markdown
Author

There is ~40% overlap with the work of PR #430 . Probably better of create a new PR once #430 has been merged.

Could you do a review of that PR and check whether the direction is compatible with yours?

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 :

  • one to fix the remaining privilege issues in the common code
  • one for the helm chart
@pascaliske

pascaliske commented Jul 16, 2026

Copy link
Copy Markdown

@jfstenuit Nice! I had the core container on my todo list as well after #430 is merged. Thanks for taking over. 😁

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}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants