Repository navigation
Conversation
|
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} |
|
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! |
|
@jfstenuit this should be now rebased, otherwise I will close it |
|
@jfstenuit we merged the other big change (nginx container) so this might be a good time to revisit this. Let me know if you are interested, otherwise I will close this. |
|
Hi @ostefano . Pending time re-allocation, but rebase and adaptation is definitely foreseen within the month. |
…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
…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.
f014251 to
a0713ca
Compare
|
PR is now re-based, but still require some tests :
|
|
|
||
| 1. Copy the `template.env` file to `.env` in the project root. | ||
| 2. Customize `.env` according to your requirements (optional but recommended). | ||
| 3. `make init-dirs` (or manually `mkdir -p configs logs files ssl gnupg && chown -R 33:0 configs logs files ssl gnupg`) - `misp-core` runs as a non-root user and can no longer `chown` these bind-mounted host directories itself on first start. |
There was a problem hiding this comment.
Does the execution of misp-core fails if those dirs are not created / chowned corrrectly? We should fail early if possible
There was a problem hiding this comment.
I would assume that misp-core fails because the permissions of those dirs will be wrong on first start. But this could probably be automated using init containers (for both docker and k8s):
Deployments behind a TLS-terminating reverse proxy/ingress (e.g. Traefik, an OpenShift Route with edge termination) legitimately have BASE_URL set to https while this container itself only ever sees plain HTTP and holds no certificate. The existing check couldn't tell that apart from a real misconfiguration and would exit 1. Replaces the old DISABLE_SSL_REDIRECT variable removed upstream, which covered the same case.
/etc/supervisor/conf.d is mounted from an emptyDir that survives container restarts within the same pod (unlike the pod's init containers, which only re-run on pod recreation). If the script reaches PHP-FPM once and later crashes for an unrelated reason, the next container restart would find the .kubernetes source files already consumed by the earlier mv and fail right here every time - masking the actual, still-unknown crash reason behind an unrelated "No such file or directory".
The refresh curl had no timeout at all. On a host with no outbound internet access this hangs until the OS's own TCP connect timeout (well over a minute) rather than failing fast, which alone can exceed the pod's startup probe budget and put it in a restart loop before the container ever gets a chance to become ready. Bound both the connection and overall request, and fall back to the local (build-time) CA bundle if the remote isn't reachable instead of leaving cacert.pem untouched.
PHP-FPM's default clear_env=yes strips the environment - including
LD_PRELOAD and NSS_WRAPPER_PASSWD/GROUP - from request-handling worker
processes. entrypoint.sh's nss_wrapper shim needs those to make our
arbitrary OpenShift UID resolve to "www-data" (see its own comments).
Without this, any `cake` subprocess PHP shells out to from within a web
request (e.g. loading /servers/serverSettings) inherits a stripped
environment and fails MISP's own OS-user check ("This OS user is not
allowed to run this command"), even though the identical check passes
for supervisord-managed background workers and anything the entrypoint
script invokes directly - neither of which goes through PHP-FPM.
MISP core's AdminShell::__assertOsUserMayWrite() checks a plain string
equality: $whoami === Configure::read('MISP.osuser'), where $whoami comes
from ProcessTool::whoami() (posix_getpwuid(posix_geteuid())['name'], with
a whoami-shell-out fallback) - and prints that exact value back in its
error ("You tried to run this command as: X"). On at least one code path
under OpenShift's arbitrary per-namespace UID assignment, that resolves
to the raw numeric UID rather than the nss_wrapper-mapped "www-data",
even though the identical check passes for supervisord-managed workers
and anything the entrypoint script invokes directly. Rather than keep
chasing why nss_wrapper doesn't reach that specific call, let MISP.osuser
be pinned to whatever value it actually needs to match.
MISP.osuser previously only had a default (applied if unset, no env var
override). Moved it to the "enforced" mechanism (minimum_config.envars.json,
always applied from an env var via envsubst) instead, matching how
SimpleBackgroundJobs.supervisor_host etc. already work. envsubst has no
${VAR:-default} fallback of its own, so the "www-data" default is now
resolved at the bash level (matching the existing PYTHON_BIN/GPG_BINARY/
SETTING_CONTACT pattern) before envsubst ever sees it, keeping the
previous default behavior for anyone not setting MISP_OSUSER.
Module_send_report_to_CTIInfoExtractor.php and Module_ai_summarize_report.php
both declare $id = 'send-report-to-cti-info-extractor', which makes
Workflow::loadAllWorkflowModules() throw WorkflowDuplicatedModuleIDException
on every load - a 500 on /servers/serverSettings, among anything else that
reads server settings. Confirmed against MISP/MISP: the current 2.5 branch
no longer has this file at all, superseded by Module_ai_summarize_report.php
in 1072b37f ("Rework the CTIInfoExtractor workflow action into Summarise
report with AI") - that commit landed after v2.5.47 was tagged. Remove the
stale duplicate ourselves until a later CORE_TAG picks up the real fix;
rm -f is a no-op once it does.
Addresses pascaliske's review comment on MISP#432: the manual `make init-dirs` step (misp-core can no longer chown its own bind-mounted host directories on first start, now that it runs as a fixed non-root user from process 1) was a regression in onboarding UX that init containers are the standard fix for. - core/files/init.sh: fixes ownership of misp-core's own bind mounts (configs, logs, files, gnupg) to 33:0, skipping any already correct - ssl is deliberately not included, see below. - docker-compose.yml: new misp-core-init service runs this as root, once, before misp-core starts (depends_on: condition: service_completed_ successfully). Reuses the misp-core image purely as "a Linux userland with chown available" rather than building/pulling a separate image for it. Both services now reference the same volume list via a shared YAML anchor (x-misp-core-volumes), so the mount list is never duplicated. - Makefile, README.md: drop `make init-dirs` and the manual step it documented (introduced by this same unreleased PR, so no deprecation period needed) - ownership is now handled automatically. ssl is deliberately excluded from this: it's mounted read-only into misp-nginx (a different, non-root user, 101) and nginx never writes to it, so there is no ownership to fix - only a *readability* constraint, documented as a README note instead (nginx-unprivileged's fixed UID needs read access to cert.pem/key.pem, which default permissions from the documented openssl command already satisfy). misp-modules' and misp-guard's own bind mounts were also reviewed: misp-modules' ./custom/*_mod/ dirs are read-only from the app's perspective (custom modules are imported, not written to) and typically fine under default host directory permissions; misp-guard's only host bind mount (./guard/config.json) is already :ro in compose. Neither needed the same treatment.
|
@pascaliske @jfstenuit what is the status of this review / PR ? |
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.