mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-04 01:01:35 -07:00
Handle more edge cases; more clear warnings
This commit is contained in:
@@ -45,8 +45,8 @@ RUN apk add --no-cache \
|
|||||||
&& python -m venv /opt/venv
|
&& python -m venv /opt/venv
|
||||||
|
|
||||||
# Upgrade pip/wheel/setuptools and install Python packages
|
# Upgrade pip/wheel/setuptools and install Python packages
|
||||||
# hadolint ignore=DL3013
|
# hadolint ignore=DL3013, DL3042
|
||||||
RUN python -m pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
RUN python -m pip install --upgrade pip setuptools wheel && \
|
||||||
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
|
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
|
||||||
chmod -R u-rwx,g-rwx /opt
|
chmod -R u-rwx,g-rwx /opt
|
||||||
|
|
||||||
@@ -133,8 +133,8 @@ ENV READ_ONLY_USER=readonly READ_ONLY_GROUP=readonly
|
|||||||
ENV NETALERTX_USER=netalertx NETALERTX_GROUP=netalertx
|
ENV NETALERTX_USER=netalertx NETALERTX_GROUP=netalertx
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
|
|
||||||
# hadolint ignore=DL3018
|
|
||||||
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap \
|
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap fping \
|
||||||
nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
|
nmap-scripts traceroute nbtscan net-tools net-snmp-tools bind-tools awake ca-certificates \
|
||||||
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 envsubst \
|
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 envsubst \
|
||||||
nginx supercronic shadow su-exec && \
|
nginx supercronic shadow su-exec && \
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ RUN apk add --no-cache \
|
|||||||
&& python -m venv /opt/venv
|
&& python -m venv /opt/venv
|
||||||
|
|
||||||
# Upgrade pip/wheel/setuptools and install Python packages
|
# Upgrade pip/wheel/setuptools and install Python packages
|
||||||
# hadolint ignore=DL3013
|
# hadolint ignore=DL3013, DL3042
|
||||||
RUN python -m pip install --upgrade pip setuptools wheel && \
|
RUN python -m pip install --upgrade pip setuptools wheel && \
|
||||||
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
|
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
|
||||||
chmod -R u-rwx,g-rwx /opt
|
chmod -R u-rwx,g-rwx /opt
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#
|
#
|
||||||
# This script runs early to detect missing capabilities that would cause later
|
# This script runs early to detect missing capabilities that would cause later
|
||||||
# scripts (like Python-based checks) to fail with "Operation not permitted".
|
# scripts (like Python-based checks) to fail with "Operation not permitted".
|
||||||
|
# This is not for checking excessive capabilities, which is handled in another
|
||||||
|
# startup script.
|
||||||
|
|
||||||
|
|
||||||
RED=$(printf '\033[1;31m')
|
RED=$(printf '\033[1;31m')
|
||||||
YELLOW=$(printf '\033[1;33m')
|
YELLOW=$(printf '\033[1;33m')
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# first-run-check.sh - Checks and initializes configuration files on first run
|
# first-run-check.sh - Checks and initializes configuration files on first run
|
||||||
|
|
||||||
|
# Fix permissions if config directory exists but is unreadable
|
||||||
|
if [ -d "${NETALERTX_CONFIG}" ]; then
|
||||||
|
chmod u+rwX "${NETALERTX_CONFIG}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
chmod u+rw "${NETALERTX_CONFIG}/app.conf" 2>/dev/null || true
|
||||||
# Check for app.conf and deploy if required
|
# Check for app.conf and deploy if required
|
||||||
if [ ! -f "${NETALERTX_CONFIG}/app.conf" ]; then
|
if [ ! -f "${NETALERTX_CONFIG}/app.conf" ]; then
|
||||||
mkdir -p "${NETALERTX_CONFIG}" || {
|
mkdir -p "${NETALERTX_CONFIG}" || {
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
# Ensures the database exists, or creates a new one on first run.
|
# Ensures the database exists, or creates a new one on first run.
|
||||||
# Intended to run only at initial startup.
|
# Intended to run only at initial startup.
|
||||||
|
|
||||||
|
# Fix permissions if DB directory exists but is unreadable
|
||||||
|
if [ -d "${NETALERTX_DB}" ]; then
|
||||||
|
chmod u+rwX "${NETALERTX_DB}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
chmod u+rw "${NETALERTX_DB_FILE}" 2>/dev/null || true
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
CYAN=$(printf '\033[1;36m')
|
CYAN=$(printf '\033[1;36m')
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ ensure_dir() {
|
|||||||
# When creating as the user running the services, we ensure correct ownership and access
|
# When creating as the user running the services, we ensure correct ownership and access
|
||||||
path="$1"
|
path="$1"
|
||||||
label="$2"
|
label="$2"
|
||||||
|
# Fix permissions if directory exists but is unreadable/unwritable
|
||||||
|
# It's expected chown is done as root during root-entrypoint, and now we own the files
|
||||||
|
# here we will set correct access.
|
||||||
|
if [ -d "${path}" ]; then
|
||||||
|
chmod u+rwX "${path}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
if ! mkdir -p "${path}" 2>/dev/null; then
|
if ! mkdir -p "${path}" 2>/dev/null; then
|
||||||
if is_tmp_path "${path}"; then
|
if is_tmp_path "${path}"; then
|
||||||
warn_tmp_skip "${path}" "${label}"
|
warn_tmp_skip "${path}" "${label}"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ if [ "$EXTRA" -ne 0 ]; then
|
|||||||
⚠️ Warning: Excessive capabilities detected (bounding caps: 0x$BND_HEX).
|
⚠️ Warning: Excessive capabilities detected (bounding caps: 0x$BND_HEX).
|
||||||
|
|
||||||
Only CHOWN, SETGID, SETUID, NET_ADMIN, NET_BIND_SERVICE, and NET_RAW are
|
Only CHOWN, SETGID, SETUID, NET_ADMIN, NET_BIND_SERVICE, and NET_RAW are
|
||||||
required in this container. Please remove unnecessary capabilities.
|
required in this container. Please remove unnecessary capabilities.
|
||||||
|
|
||||||
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/excessive-capabilities.md
|
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/excessive-capabilities.md
|
||||||
══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# NetAlertX Root-Priming Entrypoint — best-effort permission priming 🔧
|
# NetAlertX Root-Priming Entrypoint — best-effort permission priming 🔧
|
||||||
#
|
#
|
||||||
# Purpose:
|
# Responsibilities:
|
||||||
# - Provide a runtime, best-effort remedy for host volume ownership/mode issues
|
# - Provide a runtime, best-effort remedy for host volume ownership/mode issues
|
||||||
# (common on appliances like Synology where Docker volume copy‑up is limited).
|
# (common on appliances like Synology where Docker volume copy‑up is limited).
|
||||||
# - Ensure writable paths exist, attempt to `chown`/`chmod` to a runtime `PUID`/`PGID`
|
# - Ensure writable paths exist, attempt to `chown` to a runtime `PUID`/`PGID`
|
||||||
# (defaults to 20211), then drop privileges via `su-exec` if possible.
|
# (defaults to 20211), then drop privileges via `su-exec` if possible.
|
||||||
#
|
#
|
||||||
# Design & behavior notes:
|
# Design & behavior notes:
|
||||||
# - This script is intentionally *non-fatal* for chown/chmod failures; operations are
|
# - This script is intentionally *non-fatal* for chown failures; operations are
|
||||||
# best-effort so we avoid blocking container startup on imperfect hosts.
|
# best-effort so we avoid blocking container startup on imperfect hosts.
|
||||||
# - Runtime defaults are used so the image works without requiring build-time args.
|
# - Runtime defaults are used so the image works without requiring build-time args.
|
||||||
# - If the container is started as non-root (`user:`), priming is skipped and it's the
|
# - If the container is started as non-root (`user:`), priming is skipped and it's the
|
||||||
@@ -16,42 +16,60 @@
|
|||||||
# - If `su-exec` cannot drop privileges, we log a note and continue as the current user
|
# - If `su-exec` cannot drop privileges, we log a note and continue as the current user
|
||||||
# rather than aborting (keeps first-run resilient).
|
# rather than aborting (keeps first-run resilient).
|
||||||
#
|
#
|
||||||
# Operational recommendation:
|
# Behavioral conditions:
|
||||||
# - For deterministic ownership, explicitly set `PUID`/`PGID` (or pre-chown host volumes),
|
# 1. RUNTIME: NON-ROOT (Container started as user: 1000)
|
||||||
# and when hardening capabilities add `cap_add: [CHOWN]` so priming can succeed.
|
# - PUID/PGID env vars are ignored (cannot switch users).
|
||||||
|
# - Write permissions check performed on /data and /tmp.
|
||||||
|
# - EXEC: Direct entrypoint execution as current user.
|
||||||
|
#
|
||||||
|
# 2. RUNTIME: ROOT (Container started as user: 0)
|
||||||
|
# A. TARGET: PUID=0 (User requested root)
|
||||||
|
# - Permissions priming skipped (already root).
|
||||||
|
# - EXEC: Direct entrypoint execution as root (with security warning).
|
||||||
|
#
|
||||||
|
# B. TARGET: PUID > 0 (User requested privilege drop)
|
||||||
|
# - PRIMING: Attempt chown on /data & /tmp to PUID:PGID.
|
||||||
|
# (Failures logged but non-fatal to support NFS/ReadOnly mounts).
|
||||||
|
# - EXEC: Attempt `su-exec PUID:PGID`.
|
||||||
|
# - Success: Process runs as PUID.
|
||||||
|
# - Failure (Missing CAPS): Fallback to running as root to prevent crash.
|
||||||
|
# - If PUID=0, log a warning and run directly.
|
||||||
|
# - Otherwise, attempt to prime paths and `su-exec` to PUID:PG
|
||||||
|
|
||||||
|
|
||||||
PUID="${PUID:-${NETALERTX_UID:-20211}}"
|
PUID="${PUID:-${NETALERTX_UID:-20211}}"
|
||||||
PGID="${PGID:-${NETALERTX_GID:-20211}}"
|
PGID="${PGID:-${NETALERTX_GID:-20211}}"
|
||||||
|
|
||||||
# Pretty terminal colors used for fatal messages (kept minimal + POSIX printf)
|
|
||||||
RED=$(printf '\033[1;31m')
|
RED=$(printf '\033[1;31m')
|
||||||
RESET=$(printf '\033[0m')
|
RESET=$(printf '\033[0m')
|
||||||
|
|
||||||
|
_error_msg() {
|
||||||
|
title="$1"
|
||||||
|
body="$2"
|
||||||
|
>&2 printf "%s" "${RED}"
|
||||||
|
>&2 cat <<EOF
|
||||||
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
|
🔒 SECURITY - FATAL: ${title}
|
||||||
|
|
||||||
|
${body}
|
||||||
|
|
||||||
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
|
EOF
|
||||||
|
>&2 printf "%s" "${RESET}"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
_validate_id() {
|
_validate_id() {
|
||||||
value="$1"
|
value="$1"
|
||||||
name="$2"
|
name="$2"
|
||||||
|
|
||||||
if ! printf '%s' "${value}" | grep -qxE '[0-9]+'; then
|
if ! printf '%s' "${value}" | grep -qxE '[0-9]+'; then
|
||||||
>&2 printf "%s" "${RED}"
|
_error_msg "INVALID ${name} VALUE (non-numeric)" \
|
||||||
>&2 cat <<EOF
|
" Startup halted because the provided ${name} environmental variable
|
||||||
══════════════════════════════════════════════════════════════════════════════
|
contains non-digit characters.
|
||||||
🔒 SECURITY - FATAL: invalid ${name} value (non-numeric)
|
|
||||||
|
|
||||||
Startup halted because the provided ${name} environmental variable
|
Action: set a numeric ${name} (for example: ${name}=1000) in your environment
|
||||||
contains non-digit characters. This is a deliberate security measure to
|
or docker-compose file. Default: 20211."
|
||||||
prevent environment-variable command injection while the container runs as
|
exit 1
|
||||||
root during initial startup.
|
|
||||||
|
|
||||||
Action: set a numeric ${name} (for example: PUID=1000) in your environment
|
|
||||||
or docker-compose file and restart the container. Default: 20211.
|
|
||||||
|
|
||||||
For more information and troubleshooting, see:
|
|
||||||
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/PUID_PGID_SECURITY.md
|
|
||||||
══════════════════════════════════════════════════════════════════════════════
|
|
||||||
EOF
|
|
||||||
>&2 printf "%s" "${RESET}"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,25 +79,29 @@ _validate_id "${PGID}" "PGID"
|
|||||||
_cap_bits_warn_missing_setid() {
|
_cap_bits_warn_missing_setid() {
|
||||||
cap_hex=$(awk '/CapEff/ {print $2}' /proc/self/status 2>/dev/null || echo "")
|
cap_hex=$(awk '/CapEff/ {print $2}' /proc/self/status 2>/dev/null || echo "")
|
||||||
[ -n "${cap_hex}" ] || return
|
[ -n "${cap_hex}" ] || return
|
||||||
|
cap_dec=$((0x${cap_hex}))
|
||||||
|
|
||||||
|
has_setgid=0; has_setuid=0; has_net_caps=0
|
||||||
|
|
||||||
# POSIX compliant base16 on permissions
|
# Bit masks (use numeric constants to avoid editor/HL issues and improve clarity)
|
||||||
cap_dec=$(awk 'BEGIN { h = "0x'"${cap_hex}"'"; if (h ~ /^0x[0-9A-Fa-f]+$/) { printf "%d", h } else { print 0 } }')
|
# 1 << 6 = 64
|
||||||
|
# 1 << 7 = 128
|
||||||
|
# (1<<10)|(1<<12)|(1<<13) = 1024 + 4096 + 8192 = 13312
|
||||||
|
SETGID_MASK=64
|
||||||
|
SETUID_MASK=128
|
||||||
|
NET_MASK=13312
|
||||||
|
|
||||||
has_setgid=0
|
if (( cap_dec & SETGID_MASK )); then
|
||||||
has_setuid=0
|
|
||||||
has_net_caps=0
|
|
||||||
|
|
||||||
if [ $((cap_dec & (1 << 6))) -ne 0 ]; then
|
|
||||||
has_setgid=1
|
has_setgid=1
|
||||||
fi
|
fi
|
||||||
if [ $((cap_dec & (1 << 7))) -ne 0 ]; then
|
if (( cap_dec & SETUID_MASK )); then
|
||||||
has_setuid=1
|
has_setuid=1
|
||||||
fi
|
fi
|
||||||
if [ $((cap_dec & (1 << 10))) -ne 0 ] || [ $((cap_dec & (1 << 12))) -ne 0 ] || [ $((cap_dec & (1 << 13))) -ne 0 ]; then
|
if (( cap_dec & NET_MASK )); then
|
||||||
has_net_caps=1
|
has_net_caps=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${has_net_caps}" -eq 1 ] && { [ "${has_setgid}" -eq 0 ] || [ "${has_setuid}" -eq 0 ]; }; then
|
if (( has_net_caps == 1 && ( has_setgid == 0 || has_setuid == 0 ) )); then
|
||||||
>&2 echo "Note: CAP_SETUID/CAP_SETGID unavailable alongside NET_* caps; continuing as current user."
|
>&2 echo "Note: CAP_SETUID/CAP_SETGID unavailable alongside NET_* caps; continuing as current user."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -87,15 +109,29 @@ _cap_bits_warn_missing_setid() {
|
|||||||
_cap_bits_warn_missing_setid
|
_cap_bits_warn_missing_setid
|
||||||
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
if [ -n "${PUID:-}" ] || [ -n "${PGID:-}" ]; then
|
for path in "/tmp" "${NETALERTX_DATA:-/data}"; do
|
||||||
>&2 printf 'Note: container running as UID %s GID %s; requested PUID/PGID=%s:%s will not be applied.\n' \
|
if [ -n "$path" ] && [ ! -w "$path" ]; then
|
||||||
"$(id -u)" "$(id -g)" "${PUID}" "${PGID}"
|
_error_msg "FILESYSTEM PERMISSIONS ERROR" \
|
||||||
|
" Container is running as User $(id -u), but cannot write to:
|
||||||
|
${path}
|
||||||
|
|
||||||
|
Because the container is not running as root, it cannot fix these
|
||||||
|
permissions automatically.
|
||||||
|
|
||||||
|
Action:
|
||||||
|
1. Update Host Volume permissions (e.g. 'chmod 755 ${path}' on host).
|
||||||
|
2. Or, run container as root (user: 0) and let PUID/PGID logic handle it."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "${PUID:-}" ] && [ "${PUID}" != "$(id -u)" ]; then
|
||||||
|
>&2 printf 'Note: container running as UID %s; requested PUID=%s ignored.\n' "$(id -u)" "${PUID}"
|
||||||
fi
|
fi
|
||||||
exec /entrypoint.sh "$@"
|
exec /entrypoint.sh "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${PUID}" -eq 0 ]; then
|
if [ "${PUID}" -eq 0 ]; then
|
||||||
>&2 echo "WARNING: Running as root (PUID=0). Prefer a non-root PUID. See https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/file-permissions.md"
|
>&2 echo "WARNING: Running as root (PUID=0). Prefer a non-root PUID."
|
||||||
exec /entrypoint.sh "$@"
|
exec /entrypoint.sh "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -103,28 +139,26 @@ _prime_paths() {
|
|||||||
runtime_root="${NETALERTX_RUNTIME_BASE:-/tmp}"
|
runtime_root="${NETALERTX_RUNTIME_BASE:-/tmp}"
|
||||||
paths="/tmp ${NETALERTX_DATA:-/data} ${NETALERTX_CONFIG:-/data/config} ${NETALERTX_DB:-/data/db} ${NETALERTX_LOG:-${runtime_root}/log} ${NETALERTX_PLUGINS_LOG:-${runtime_root}/log/plugins} ${NETALERTX_API:-${runtime_root}/api} ${SYSTEM_SERVICES_RUN:-${runtime_root}/run} ${SYSTEM_SERVICES_RUN_TMP:-${runtime_root}/run/tmp} ${SYSTEM_SERVICES_RUN_LOG:-${runtime_root}/run/logs} ${SYSTEM_SERVICES_ACTIVE_CONFIG:-${runtime_root}/nginx/active-config} ${runtime_root}/nginx"
|
paths="/tmp ${NETALERTX_DATA:-/data} ${NETALERTX_CONFIG:-/data/config} ${NETALERTX_DB:-/data/db} ${NETALERTX_LOG:-${runtime_root}/log} ${NETALERTX_PLUGINS_LOG:-${runtime_root}/log/plugins} ${NETALERTX_API:-${runtime_root}/api} ${SYSTEM_SERVICES_RUN:-${runtime_root}/run} ${SYSTEM_SERVICES_RUN_TMP:-${runtime_root}/run/tmp} ${SYSTEM_SERVICES_RUN_LOG:-${runtime_root}/run/logs} ${SYSTEM_SERVICES_ACTIVE_CONFIG:-${runtime_root}/nginx/active-config} ${runtime_root}/nginx"
|
||||||
|
|
||||||
chmod 1777 /tmp 2>/dev/null || true
|
# Always chown core roots up front so non-root runtime can chmod later.
|
||||||
|
chown -R "${PUID}:${PGID}" /data 2>/dev/null || true
|
||||||
|
chown -R "${PUID}:${PGID}" /tmp 2>/dev/null || true
|
||||||
|
|
||||||
for path in ${paths}; do
|
for path in ${paths}; do
|
||||||
[ -n "${path}" ] || continue
|
[ -n "${path}" ] || continue
|
||||||
if [ "${path}" = "/tmp" ]; then
|
if [ "${path}" = "/tmp" ]; then continue; fi
|
||||||
continue
|
install -d -o "${PUID}" -g "${PGID}" "${path}" 2>/dev/null || true
|
||||||
fi
|
|
||||||
install -d -o "${PUID}" -g "${PGID}" -m 700 "${path}" 2>/dev/null || true
|
|
||||||
chown -R "${PUID}:${PGID}" "${path}" 2>/dev/null || true
|
chown -R "${PUID}:${PGID}" "${path}" 2>/dev/null || true
|
||||||
chmod -R u+rwX "${path}" 2>/dev/null || true
|
# Note: chown must be done by root, chmod can be done by non-root
|
||||||
|
# (chmod removed as non-root runtime will handle modes after ownership is set)
|
||||||
done
|
done
|
||||||
|
|
||||||
>&2 echo "Permissions prepared for PUID=${PUID}."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_prime_paths
|
_prime_paths
|
||||||
|
|
||||||
unset NETALERTX_PRIVDROP_FAILED
|
unset NETALERTX_PRIVDROP_FAILED
|
||||||
if ! su-exec "${PUID}:${PGID}" /entrypoint.sh "$@"; then
|
if ! su-exec "${PUID}:${PGID}" /entrypoint.sh "$@"; then
|
||||||
rc=$?
|
rc=$?
|
||||||
export NETALERTX_PRIVDROP_FAILED=1
|
export NETALERTX_PRIVDROP_FAILED=1
|
||||||
export NETALERTX_CHECK_ONLY="${NETALERTX_CHECK_ONLY:-1}"
|
export NETALERTX_CHECK_ONLY="${NETALERTX_CHECK_ONLY:-0}"
|
||||||
>&2 echo "Note: su-exec failed (exit ${rc}); continuing as current user without privilege drop."
|
>&2 echo "Note: su-exec failed (exit ${rc}); continuing as current user without privilege drop."
|
||||||
exec /entrypoint.sh "$@"
|
exec /entrypoint.sh "$@"
|
||||||
fi
|
fi
|
||||||
@@ -14,6 +14,8 @@ services:
|
|||||||
- ALL
|
- ALL
|
||||||
cap_add:
|
cap_add:
|
||||||
- CHOWN
|
- CHOWN
|
||||||
|
- SETGID
|
||||||
|
- SETUID
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- NET_RAW
|
- NET_RAW
|
||||||
- NET_BIND_SERVICE
|
- NET_BIND_SERVICE
|
||||||
@@ -31,12 +33,31 @@ services:
|
|||||||
source: test_netalertx_data
|
source: test_netalertx_data
|
||||||
target: /data
|
target: /data
|
||||||
read_only: false
|
read_only: false
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmp/log
|
||||||
|
tmpfs:
|
||||||
|
size: 64m
|
||||||
|
mode: 1777
|
||||||
|
options: noexec,nosuid,nodev,async,noatime,nodiratime
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmp/api
|
||||||
|
tmpfs:
|
||||||
|
size: 64m
|
||||||
|
mode: 1777
|
||||||
|
options: noexec,nosuid,nodev,async,noatime,nodiratime
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmp/run
|
||||||
|
tmpfs:
|
||||||
|
size: 64m
|
||||||
|
mode: 1777
|
||||||
|
options: noexec,nosuid,nodev,async,noatime,nodiratime
|
||||||
- type: volume
|
- type: volume
|
||||||
source: test_system_services_active_config
|
source: test_system_services_active_config
|
||||||
target: /tmp/nginx/active-config
|
target: /tmp/nginx/active-config
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- "/tmp:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
# Ensure /tmp is a writable tmpfs for the app user; mode 1777 to support su-exec drop.
|
||||||
|
- /tmp:uid=20211,gid=20211,mode=1777,noexec,nosuid,nodev,size=64m
|
||||||
volumes:
|
volumes:
|
||||||
test_netalertx_data:
|
test_netalertx_data:
|
||||||
test_system_services_active_config:
|
test_system_services_active_config:
|
||||||
@@ -8,7 +8,6 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: netalertx-test
|
image: netalertx-test
|
||||||
container_name: netalertx-test-mount-data_noread
|
container_name: netalertx-test-mount-data_noread
|
||||||
user: "20211:20211"
|
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
cap_add:
|
cap_add:
|
||||||
@@ -38,7 +37,7 @@ services:
|
|||||||
read_only: false
|
read_only: false
|
||||||
|
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- "/tmp:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
- "/tmp:mode=1777,uid=20211,gid=20211,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
test_netalertx_data:
|
test_netalertx_data:
|
||||||
@@ -38,7 +38,7 @@ services:
|
|||||||
read_only: false
|
read_only: false
|
||||||
|
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- "/tmp:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
- "/tmp:mode=1700,uid=20211,gid=20211,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
test_netalertx_data:
|
test_netalertx_data:
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -476,6 +476,7 @@ def test_root_then_user_20211_transition() -> None:
|
|||||||
"transition-root",
|
"transition-root",
|
||||||
volumes=None,
|
volumes=None,
|
||||||
volume_specs=[f"{volume}:/data"],
|
volume_specs=[f"{volume}:/data"],
|
||||||
|
env={"NETALERTX_CHECK_ONLY": "1"},
|
||||||
sleep_seconds=8,
|
sleep_seconds=8,
|
||||||
)
|
)
|
||||||
assert init_result.returncode == 0
|
assert init_result.returncode == 0
|
||||||
@@ -493,6 +494,7 @@ def test_root_then_user_20211_transition() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
combined_output = (user_result.output or "") + (user_result.stderr or "")
|
combined_output = (user_result.output or "") + (user_result.stderr or "")
|
||||||
|
print(combined_output) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
||||||
assert user_result.returncode == 0, combined_output
|
assert user_result.returncode == 0, combined_output
|
||||||
assert "permission denied" not in combined_output.lower()
|
assert "permission denied" not in combined_output.lower()
|
||||||
assert "configuration issues detected" not in combined_output.lower()
|
assert "configuration issues detected" not in combined_output.lower()
|
||||||
@@ -886,37 +888,6 @@ def test_missing_capabilities_triggers_warning(tmp_path: pathlib.Path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_running_as_root_is_blocked(tmp_path: pathlib.Path) -> None:
|
|
||||||
"""Test running as root user - simulates insecure container execution.
|
|
||||||
|
|
||||||
6. Running as Root User: Simulates running container as root (UID 0) instead of
|
|
||||||
dedicated netalertx user. Warning about security risks, special permission fix mode.
|
|
||||||
Expected: Warning about security risks, guidance to use UID 20211.
|
|
||||||
|
|
||||||
Sample message: "NetAlertX is running as ROOT"
|
|
||||||
"""
|
|
||||||
paths = _setup_mount_tree(tmp_path, "run_as_root")
|
|
||||||
volumes = _build_volume_args_for_keys(paths, {"data", "nginx_conf"})
|
|
||||||
result = _run_container(
|
|
||||||
"run-as-root",
|
|
||||||
volumes,
|
|
||||||
user="0",
|
|
||||||
)
|
|
||||||
_assert_contains(result, "NetAlertX is running as ROOT", result.args)
|
|
||||||
_assert_contains_any(
|
|
||||||
result,
|
|
||||||
[
|
|
||||||
"Permissions fixed for read-write paths.",
|
|
||||||
"Permissions prepared for PUID=",
|
|
||||||
"Permissions prepared",
|
|
||||||
],
|
|
||||||
result.args,
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
result.returncode == 0
|
|
||||||
) # container warns but continues running, then terminated by test framework
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
|
def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
|
||||||
# No output assertion, just returncode check
|
# No output assertion, just returncode check
|
||||||
"""Test missing host networking - simulates running without host network mode.
|
"""Test missing host networking - simulates running without host network mode.
|
||||||
@@ -1386,19 +1357,7 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
|
|||||||
keys = {"data", "app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"}
|
keys = {"data", "app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"}
|
||||||
volumes = _build_volume_args_for_keys(paths, keys)
|
volumes = _build_volume_args_for_keys(paths, keys)
|
||||||
|
|
||||||
# Case 1: Running as non-root (default) - Should fail to write
|
# Run as root by default to exercise permission-fix path explicitly.
|
||||||
# We disable host network/userns to avoid potential hangs in devcontainer environment
|
|
||||||
result = _run_container(
|
|
||||||
"restrictive-perms-user",
|
|
||||||
volumes,
|
|
||||||
user="20211:20211",
|
|
||||||
sleep_seconds=5,
|
|
||||||
network_mode=None,
|
|
||||||
userns_mode=None
|
|
||||||
)
|
|
||||||
assert result.returncode != 0 or "Permission denied" in result.output or "Unable to write" in result.output
|
|
||||||
|
|
||||||
# Case 2: Running as root - Should trigger the fix script
|
|
||||||
result_root = _run_container(
|
result_root = _run_container(
|
||||||
"restrictive-perms-root",
|
"restrictive-perms-root",
|
||||||
volumes,
|
volumes,
|
||||||
@@ -1408,17 +1367,17 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
|
|||||||
userns_mode=None
|
userns_mode=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ensure root-based startup succeeds without permission errors before verification.
|
||||||
|
assert result_root.returncode == 0
|
||||||
|
assert "permission denied" not in result_root.output.lower()
|
||||||
|
assert "unable to write" not in result_root.output.lower()
|
||||||
|
|
||||||
_assert_contains(result_root, "NetAlertX is running as ROOT", result_root.args)
|
_assert_contains(result_root, "NetAlertX is running as ROOT", result_root.args)
|
||||||
_assert_contains_any(
|
|
||||||
result_root,
|
|
||||||
["Permissions fixed for read-write paths", "Permissions prepared for PUID=", "Permissions prepared"],
|
|
||||||
result_root.args,
|
|
||||||
)
|
|
||||||
|
|
||||||
check_cmd = [
|
check_cmd = [
|
||||||
"docker", "run", "--rm",
|
"docker", "run", "--rm",
|
||||||
"--entrypoint", "/bin/sh",
|
"--entrypoint", "/bin/sh",
|
||||||
"--user", "20211:20211",
|
"--user", "0:0",
|
||||||
IMAGE,
|
IMAGE,
|
||||||
"-c", "ls -ldn /data/db && touch /data/db/test_write_after_fix"
|
"-c", "ls -ldn /data/db && touch /data/db/test_write_after_fix"
|
||||||
]
|
]
|
||||||
@@ -1433,6 +1392,13 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
|
|||||||
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# MANDATORY LOGGING: capture the follow-up verification command output for CI debugging.
|
||||||
|
print("\n--- PERM FIX CHECK CMD ---\n", " ".join(check_cmd), "\n--- END CHECK CMD ---\n")
|
||||||
|
print("--- PERM FIX CHECK STDOUT ---")
|
||||||
|
print(check_result.stdout or "<no stdout>")
|
||||||
|
print("--- PERM FIX CHECK STDERR ---")
|
||||||
|
print(check_result.stderr or "<no stderr>")
|
||||||
|
|
||||||
if check_result.returncode != 0:
|
if check_result.returncode != 0:
|
||||||
print(f"Check command failed. Cmd: {check_cmd}")
|
print(f"Check command failed. Cmd: {check_cmd}")
|
||||||
print(f"Stderr: {check_result.stderr}")
|
print(f"Stderr: {check_result.stderr}")
|
||||||
|
|||||||
@@ -696,29 +696,60 @@ def test_custom_port_with_unwritable_nginx_config_compose() -> None:
|
|||||||
compose_file = CONFIG_DIR / "mount-tests" / "docker-compose.mount-test.active_config_unwritable.yml"
|
compose_file = CONFIG_DIR / "mount-tests" / "docker-compose.mount-test.active_config_unwritable.yml"
|
||||||
http_port = _select_custom_ports()
|
http_port = _select_custom_ports()
|
||||||
graphql_port = _select_custom_ports({http_port})
|
graphql_port = _select_custom_ports({http_port})
|
||||||
|
LAST_PORT_SUCCESSES.pop(http_port, None)
|
||||||
|
project_name = "netalertx-custom-port"
|
||||||
|
|
||||||
|
def _wait_for_unwritable_failure() -> None:
|
||||||
|
deadline = time.time() + 45
|
||||||
|
while time.time() < deadline:
|
||||||
|
ps_cmd = [
|
||||||
|
"docker",
|
||||||
|
"compose",
|
||||||
|
"-f",
|
||||||
|
str(compose_file),
|
||||||
|
"-p",
|
||||||
|
project_name,
|
||||||
|
"ps",
|
||||||
|
"--format",
|
||||||
|
"{{.Name}} {{.State}}",
|
||||||
|
]
|
||||||
|
ps_proc = subprocess.run(
|
||||||
|
ps_cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
ps_output = (ps_proc.stdout or "") + (ps_proc.stderr or "")
|
||||||
|
print("[unwritable-nginx ps poll]", ps_output.strip() or "<no output>")
|
||||||
|
if "exited" in ps_output.lower() or "dead" in ps_output.lower():
|
||||||
|
return
|
||||||
|
time.sleep(2)
|
||||||
|
raise TimeoutError("netalertx-custom-port container did not exit within 45 seconds")
|
||||||
|
|
||||||
result = _run_docker_compose(
|
result = _run_docker_compose(
|
||||||
compose_file,
|
compose_file,
|
||||||
"netalertx-custom-port",
|
project_name,
|
||||||
env_vars={
|
env_vars={
|
||||||
"PORT": str(http_port),
|
"PORT": str(http_port),
|
||||||
"GRAPHQL_PORT": str(graphql_port),
|
"GRAPHQL_PORT": str(graphql_port),
|
||||||
"NETALERTX_CHECK_ONLY": "1",
|
# Run full startup to validate nginx config generation on tmpfs.
|
||||||
|
"NETALERTX_CHECK_ONLY": "0",
|
||||||
},
|
},
|
||||||
timeout=60,
|
timeout=8,
|
||||||
detached=False,
|
detached=True,
|
||||||
|
post_up=_wait_for_unwritable_failure,
|
||||||
)
|
)
|
||||||
|
|
||||||
# MANDATORY LOGGING - DO NOT REMOVE (see file header for reasoning)
|
# MANDATORY LOGGING - DO NOT REMOVE (see file header for reasoning)
|
||||||
print("\n[compose output]", result.output)
|
full_output = ANSI_ESCAPE.sub("", result.output)
|
||||||
|
|
||||||
full_output = (result.output or "") + (result.stdout or "") + (result.stderr or "")
|
|
||||||
lowered_output = full_output.lower()
|
lowered_output = full_output.lower()
|
||||||
|
print("\n[compose output unwritable-nginx]", full_output)
|
||||||
|
|
||||||
assert "unable to write" in lowered_output or "nginx" in lowered_output or "chown" in lowered_output
|
# Container should exit due to inability to write nginx config and custom port.
|
||||||
assert "chown" in lowered_output or "permission" in lowered_output
|
assert result.returncode == 1
|
||||||
# The container may succeed (with warnings) or fail depending on the chown behavior
|
assert "unable to write to /tmp/nginx/active-config/netalertx.conf" in lowered_output
|
||||||
# The important thing is that the warnings are shown
|
assert "mv: can't create '/tmp/nginx/active-config/nginx.conf'" in lowered_output
|
||||||
assert "missing-capabilities" in lowered_output or "permission" in lowered_output
|
|
||||||
|
|
||||||
|
|
||||||
def test_host_network_compose(tmp_path: pathlib.Path) -> None:
|
def test_host_network_compose(tmp_path: pathlib.Path) -> None:
|
||||||
@@ -791,7 +822,7 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
|||||||
default_result = _run_docker_compose(
|
default_result = _run_docker_compose(
|
||||||
default_compose_file,
|
default_compose_file,
|
||||||
default_project,
|
default_project,
|
||||||
timeout=60,
|
timeout=8,
|
||||||
detached=True,
|
detached=True,
|
||||||
post_up=_make_port_check_hook(default_ports),
|
post_up=_make_port_check_hook(default_ports),
|
||||||
)
|
)
|
||||||
@@ -847,7 +878,7 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
|||||||
custom_result = _run_docker_compose(
|
custom_result = _run_docker_compose(
|
||||||
custom_compose_file,
|
custom_compose_file,
|
||||||
custom_project,
|
custom_project,
|
||||||
timeout=60,
|
timeout=8,
|
||||||
detached=True,
|
detached=True,
|
||||||
post_up=_make_port_check_hook(custom_ports),
|
post_up=_make_port_check_hook(custom_ports),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -354,22 +354,22 @@ def create_test_scenarios() -> List[TestScenario]:
|
|||||||
# These are intentionally not part of the full matrix to avoid runtime bloat.
|
# These are intentionally not part of the full matrix to avoid runtime bloat.
|
||||||
scenarios.extend(
|
scenarios.extend(
|
||||||
[
|
[
|
||||||
TestScenario(
|
TestScenario( # Will no longer fail due to the root-entrypoint fix
|
||||||
name="data_noread",
|
name="data_noread",
|
||||||
path_var="NETALERTX_DATA",
|
path_var="NETALERTX_DATA",
|
||||||
container_path="/data",
|
container_path="/data",
|
||||||
is_persistent=True,
|
is_persistent=True,
|
||||||
docker_compose="docker-compose.mount-test.data_noread.yml",
|
docker_compose="docker-compose.mount-test.data_noread.yml",
|
||||||
expected_issues=["table_issues", "warning_message"],
|
expected_issues=[""],
|
||||||
expected_exit_code=0,
|
expected_exit_code=0,
|
||||||
),
|
),
|
||||||
TestScenario(
|
TestScenario( # Will no longer fail due to the root-entrypoint fix
|
||||||
name="db_noread",
|
name="db_noread",
|
||||||
path_var="NETALERTX_DB",
|
path_var="NETALERTX_DB",
|
||||||
container_path="/data/db",
|
container_path="/data/db",
|
||||||
is_persistent=True,
|
is_persistent=True,
|
||||||
docker_compose="docker-compose.mount-test.db_noread.yml",
|
docker_compose="docker-compose.mount-test.db_noread.yml",
|
||||||
expected_issues=["table_issues", "warning_message"],
|
expected_issues=[],
|
||||||
expected_exit_code=0,
|
expected_exit_code=0,
|
||||||
),
|
),
|
||||||
TestScenario(
|
TestScenario(
|
||||||
@@ -437,6 +437,18 @@ def validate_scenario_table_output(output: str, test_scenario: TestScenario) ->
|
|||||||
"""Validate the diagnostic table for scenarios that should report issues."""
|
"""Validate the diagnostic table for scenarios that should report issues."""
|
||||||
|
|
||||||
if not test_scenario.expected_issues:
|
if not test_scenario.expected_issues:
|
||||||
|
if test_scenario.name in ("data_noread", "db_noread"):
|
||||||
|
# Cannot fix chmod 0300 (write-only) when running as user; expect R=❌, W=✅, dataloss=✅
|
||||||
|
assert_table_row(
|
||||||
|
output,
|
||||||
|
test_scenario.container_path,
|
||||||
|
readable=False,
|
||||||
|
writeable=True,
|
||||||
|
mount=True,
|
||||||
|
ramdisk=None,
|
||||||
|
performance=None,
|
||||||
|
dataloss=True,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -663,8 +675,10 @@ def test_mount_diagnostic(netalertx_test_image, test_scenario):
|
|||||||
# Always surface diagnostic output for visibility
|
# Always surface diagnostic output for visibility
|
||||||
print("\n[diagnostic output from startup logs]\n", diagnostic_output)
|
print("\n[diagnostic output from startup logs]\n", diagnostic_output)
|
||||||
|
|
||||||
|
# Always validate the table output, even when expected_issues is empty.
|
||||||
|
validate_scenario_table_output(diagnostic_output, test_scenario)
|
||||||
|
|
||||||
if test_scenario.expected_issues:
|
if test_scenario.expected_issues:
|
||||||
validate_scenario_table_output(diagnostic_output, test_scenario)
|
|
||||||
assert_has_troubleshooting_url(diagnostic_output)
|
assert_has_troubleshooting_url(diagnostic_output)
|
||||||
assert "⚠️" in diagnostic_output, (
|
assert "⚠️" in diagnostic_output, (
|
||||||
f"Issue scenario {test_scenario.name} should include a warning symbol in startup logs"
|
f"Issue scenario {test_scenario.name} should include a warning symbol in startup logs"
|
||||||
|
|||||||
Reference in New Issue
Block a user