Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ RUN apk add --no-cache \
&& python -m venv /opt/venv

# Upgrade pip/wheel/setuptools and install Python packages
# hadolint ignore=DL3013
RUN python -m pip install --no-cache-dir --upgrade pip setuptools wheel && \
# hadolint ignore=DL3013, DL3042
RUN python -m pip install --upgrade pip setuptools wheel && \
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
chmod -R u-rwx,g-rwx /opt

Expand Down Expand Up @@ -133,8 +133,8 @@ ENV READ_ONLY_USER=readonly READ_ONLY_GROUP=readonly
ENV NETALERTX_USER=netalertx NETALERTX_GROUP=netalertx
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 \
sqlite php83 php83-fpm php83-cgi php83-curl php83-sqlite3 php83-session python3 envsubst \
nginx supercronic shadow su-exec && \
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ RUN apk add --no-cache \
&& python -m venv /opt/venv

# Upgrade pip/wheel/setuptools and install Python packages
# hadolint ignore=DL3013
# hadolint ignore=DL3013, DL3042
RUN python -m pip install --upgrade pip setuptools wheel && \
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
chmod -R u-rwx,g-rwx /opt
Expand Down
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ services:
cpu_shares: 512 # Relative CPU weight for CPU contention scenarios
pids_limit: 512 # Limit the number of processes/threads to prevent fork bombs
logging:
driver: "json-file" # Use JSON file logging driver
options:
max-size: "10m" # Rotate log files after they reach 10MB
max-file: "3" # Keep a maximum of 3 log files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#
# This script runs early to detect missing capabilities that would cause later
# 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')
YELLOW=$(printf '\033[1;33m')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
#!/bin/sh
# 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
if [ ! -f "${NETALERTX_CONFIG}/app.conf" ]; then
mkdir -p "${NETALERTX_CONFIG}" || {
Expand Down
6 changes: 6 additions & 0 deletions install/production-filesystem/entrypoint.d/25-first-run-db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
# Ensures the database exists, or creates a new one on first run.
# 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

CYAN=$(printf '\033[1;36m')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ ensure_dir() {
# When creating as the user running the services, we ensure correct ownership and access
path="$1"
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 is_tmp_path "${path}"; then
warn_tmp_skip "${path}" "${label}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/sh
# POSIX-compliant shell script for capability checking.
# excessive-capabilities.sh checks that no more than the necessary
# NET_ADMIN NET_BIND_SERVICE and NET_RAW capabilities are present.
# CHOWN SETGID SETUID NET_ADMIN NET_BIND_SERVICE and NET_RAW capabilities are present.


# if we are running in devcontainer then we should exit immediately without checking
Expand All @@ -21,8 +21,8 @@ fi
#POSIX compliant base16 on permissions
BND_DEC=$(awk 'BEGIN { h = "0x'"$BND_HEX"'"; if (h ~ /^0x[0-9A-Fa-f]+$/) { printf "%d", h; exit 0 } else { exit 1 } }') || exit 0

# Allowed capabilities: NET_BIND_SERVICE (10), NET_ADMIN (12), NET_RAW (13)
ALLOWED_DEC=$(( ( 1 << 10 ) | ( 1 << 12 ) | ( 1 << 13 ) ))
# Allowed capabilities: CHOWN (0), SETGID (6), SETUID (7), NET_BIND_SERVICE (10), NET_ADMIN (12), NET_RAW (13)
ALLOWED_DEC=$(( ( 1 << 0 ) | ( 1 << 6 ) | ( 1 << 7 ) | ( 1 << 10 ) | ( 1 << 12 ) | ( 1 << 13 ) ))

# Check for excessive capabilities (any bits set outside allowed)
EXTRA=$(( BND_DEC & ~ALLOWED_DEC ))
Expand All @@ -32,8 +32,8 @@ if [ "$EXTRA" -ne 0 ]; then
══════════════════════════════════════════════════════════════════════════════
⚠️ Warning: Excessive capabilities detected (bounding caps: 0x$BND_HEX).

Only NET_ADMIN, NET_BIND_SERVICE, and NET_RAW are required in this container.
Please remove unnecessary capabilities.
Only CHOWN, SETGID, SETUID, NET_ADMIN, NET_BIND_SERVICE, and NET_RAW are
required in this container. Please remove unnecessary capabilities.

https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/excessive-capabilities.md
══════════════════════════════════════════════════════════════════════════════
Expand Down
138 changes: 86 additions & 52 deletions install/production-filesystem/root-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,57 +1,75 @@
#!/bin/bash
# NetAlertX Root-Priming Entrypoint — best-effort permission priming 🔧
#
# Purpose:
# Responsibilities:
# - Provide a runtime, best-effort remedy for host volume ownership/mode issues
# (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.
#
# 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.
# - 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
# operator's responsibility to ensure matching ownership on the host.
# - If `su-exec` cannot drop privileges, we log a note and continue as the current user
# rather than aborting (keeps first-run resilient).
#
# Operational recommendation:
# - For deterministic ownership, explicitly set `PUID`/`PGID` (or pre-chown host volumes),
# and when hardening capabilities add `cap_add: [CHOWN]` so priming can succeed.
# Behavioral conditions:
# 1. RUNTIME: NON-ROOT (Container started as user: 1000)
# - 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}}"
PGID="${PGID:-${NETALERTX_GID:-20211}}"

# Pretty terminal colors used for fatal messages (kept minimal + POSIX printf)
RED=$(printf '\033[1;31m')
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() {
value="$1"
name="$2"

if ! printf '%s' "${value}" | grep -qxE '[0-9]+'; then
>&2 printf "%s" "${RED}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
🔒 SECURITY - FATAL: invalid ${name} value (non-numeric)

Startup halted because the provided ${name} environmental variable
contains non-digit characters. This is a deliberate security measure to
prevent environment-variable command injection while the container runs as
root during initial startup.
_error_msg "INVALID ${name} VALUE (non-numeric)" \
" Startup halted because the provided ${name} environmental variable
contains non-digit characters.

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
Action: set a numeric ${name} (for example: ${name}=1000) in your environment
or docker-compose file. Default: 20211."
exit 1
fi
}

Expand All @@ -61,70 +79,86 @@ _validate_id "${PGID}" "PGID"
_cap_bits_warn_missing_setid() {
cap_hex=$(awk '/CapEff/ {print $2}' /proc/self/status 2>/dev/null || echo "")
[ -n "${cap_hex}" ] || return

# POSIX compliant base16 on permissions
cap_dec=$(awk 'BEGIN { h = "0x'"${cap_hex}"'"; if (h ~ /^0x[0-9A-Fa-f]+$/) { printf "%d", h } else { print 0 } }')

has_setgid=0
has_setuid=0
has_net_caps=0

if [ $((cap_dec & (1 << 6))) -ne 0 ]; then
cap_dec=$((0x${cap_hex}))

has_setgid=0; has_setuid=0; has_net_caps=0

# Bit masks (use numeric constants to avoid editor/HL issues and improve clarity)
# 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

if (( cap_dec & SETGID_MASK )); then
has_setgid=1
fi
if [ $((cap_dec & (1 << 7))) -ne 0 ]; then
if (( cap_dec & SETUID_MASK )); then
has_setuid=1
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
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."
fi
}

_cap_bits_warn_missing_setid

if [ "$(id -u)" -ne 0 ]; then
if [ -n "${PUID:-}" ] || [ -n "${PGID:-}" ]; then
>&2 printf 'Note: container running as UID %s GID %s; requested PUID/PGID=%s:%s will not be applied.\n' \
"$(id -u)" "$(id -g)" "${PUID}" "${PGID}"
for path in "/tmp" "${NETALERTX_DATA:-/data}"; do
if [ -n "$path" ] && [ ! -w "$path" ]; then
_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
exec /entrypoint.sh "$@"
fi

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 "$@"
fi

_prime_paths() {
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"

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
[ -n "${path}" ] || continue
if [ "${path}" = "/tmp" ]; then
continue
fi
install -d -o "${PUID}" -g "${PGID}" -m 700 "${path}" 2>/dev/null || true
if [ "${path}" = "/tmp" ]; then continue; fi
install -d -o "${PUID}" -g "${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

>&2 echo "Permissions prepared for PUID=${PUID}."
}

_prime_paths

unset NETALERTX_PRIVDROP_FAILED
if ! su-exec "${PUID}:${PGID}" /entrypoint.sh "$@"; then
rc=$?
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."
exec /entrypoint.sh "$@"
fi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ services:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
- NET_ADMIN
- NET_RAW
- NET_BIND_SERVICE
Expand All @@ -36,7 +38,11 @@ services:
target: /tmp/nginx/active-config
read_only: true
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
- /tmp/log:uid=20211,gid=20211,mode=1777,noexec,nosuid,nodev,size=64m
- /tmp/api:uid=20211,gid=20211,mode=1777,noexec,nosuid,nodev,size=64m
- /tmp/run:uid=20211,gid=20211,mode=1777,noexec,nosuid,nodev,size=64m
volumes:
test_netalertx_data:
test_system_services_active_config:
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ services:
dockerfile: Dockerfile
image: netalertx-test
container_name: netalertx-test-mount-data_noread
user: "20211:20211"
cap_drop:
- ALL
cap_add:
Expand Down Expand Up @@ -38,7 +37,7 @@ services:
read_only: false

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:
test_netalertx_data:
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ services:
read_only: false

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:
test_netalertx_data:
Loading
Loading