New PUID startup sequence

This commit is contained in:
Adam Outler
2026-01-03 01:13:18 +00:00
parent a01ccaec94
commit c15f621ad4
29 changed files with 620 additions and 884 deletions

View File

@@ -1,68 +0,0 @@
#!/bin/sh
# 0-storage-permission.sh: Fix permissions if running as root.
#
# This script checks if running as root and fixes ownership and permissions
# for read-write paths to ensure proper operation.
# --- Color Codes ---
MAGENTA=$(printf '\033[1;35m')
RESET=$(printf '\033[0m')
# --- Main Logic ---
# Define paths that need read-write access
READ_WRITE_PATHS="
${NETALERTX_DATA}
${NETALERTX_DB}
${NETALERTX_API}
${NETALERTX_LOG}
${SYSTEM_SERVICES_RUN}
${NETALERTX_CONFIG}
${NETALERTX_CONFIG_FILE}
${NETALERTX_DB_FILE}
"
TARGET_USER="${NETALERTX_USER:-netalertx}"
# If running as root, fix permissions first
if [ "$(id -u)" -eq 0 ]; then
>&2 printf "%s" "${MAGENTA}"
>&2 cat <<'EOF'
══════════════════════════════════════════════════════════════════════════════
🚨 CRITICAL SECURITY ALERT: NetAlertX is running as ROOT (UID 0)! 🚨
This configuration bypasses all built-in security hardening measures.
You've granted a network monitoring application unrestricted access to
your host system. A successful compromise here could jeopardize your
entire infrastructure.
IMMEDIATE ACTION REQUIRED: Switch to the dedicated 'netalertx' user:
* Remove any 'user:' directive specifying UID 0 from docker-compose.yml or
* switch to the default USER in the image (20211:20211)
IMPORTANT: This corrective mode automatically adjusts ownership of
/data/db and /data/config directories to the netalertx user, ensuring
proper operation in subsequent runs.
Remember: Never operate security-critical tools as root unless you're
actively trying to get pwned.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/running-as-root.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
# Set ownership and permissions for each read-write path individually
printf '%s\n' "${READ_WRITE_PATHS}" | while IFS= read -r path; do
[ -n "${path}" ] || continue
chown -R "${TARGET_USER}" "${path}" 2>/dev/null || true
find "${path}" -type d -exec chmod u+rwx {} \;
find "${path}" -type f -exec chmod u+rw {} \;
done
echo Permissions fixed for read-write paths. Please restart the container as user ${TARGET_USER}.
sleep infinity & wait $!
fi

View File

@@ -1,5 +1,28 @@
#!/bin/sh
# 01-data-migration.sh - consolidate legacy /app mounts into /data
# 05-data-migration.sh - Consolidate legacy /app mounts into /data
#
# This script migrates NetAlertX data from legacy mount points (/app/config and /app/db)
# to the new consolidated /data directory. It runs during container startup as part of the
# entrypoint process.
#
# Function:
# - Checks for existing migration markers to avoid re-migration.
# - Detects if legacy directories are mounted.
# - Ensures the new /data directory is mounted.
# - Copies configuration and database files from legacy paths to /data.
# - Sets migration markers in legacy directories to prevent future migrations.
# - Provides warnings and errors for various mount states.
#
# Migration Conditions:
# - Both /app/config and /app/db must be mounted (legacy mounts present).
# - /data must be mounted (new consolidated volume).
# - No .migration marker files exist in legacy directories (not already migrated).
#
# Exit Codes:
# - 0: Success, no action needed, or migration completed.
# - 1: Migration failure (e.g., copy errors).
#
# The script exits early with 0 for non-fatal conditions like partial mounts or already migrated.
set -eu
@@ -37,7 +60,7 @@ EOF
>&2 printf "%s" "${RESET}"
}
fatal_missing_data_mount() {
possibly_fatal_missing_data_mount() { # Fatal if read-only mode, data loss if not.
>&2 printf "%s" "${RED}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
@@ -137,8 +160,11 @@ EOF
fi
if ! ${DATA_MOUNTED}; then
fatal_missing_data_mount
exit 1
# Warn about missing /data mount. Migration cannot proceed, but we allow container
# startup to continue. Data written to /data will be ephemeral, though legacy
# mount data remains safe and accessible.
possibly_fatal_missing_data_mount
exit 0
fi
migrate_legacy_mounts || exit 1

View File

@@ -0,0 +1,69 @@
#!/bin/sh
# 10-capabilities-audit.sh - Inspects the container bounding set for required privileges.
#
# This script runs early to detect missing capabilities that would cause later
# scripts (like Python-based checks) to fail with "Operation not permitted".
RED=$(printf '\033[1;31m')
YELLOW=$(printf '\033[1;33m')
GREY=$(printf '\033[90m')
RESET=$(printf '\033[0m')
# Parse Bounding Set from /proc/self/status
cap_bnd_hex=$(awk '/CapBnd/ {print $2}' /proc/self/status 2>/dev/null || echo "0")
# Convert hex to dec (POSIX compliant)
cap_bnd_dec=$(awk -v hex="$cap_bnd_hex" 'BEGIN { h = "0x" hex; if (h ~ /^0x[0-9A-Fa-f]+$/) { printf "%d", h } else { print 0 } }')
has_cap() {
bit=$1
# Check if bit is set in cap_bnd_dec
[ $(( (cap_bnd_dec >> bit) & 1 )) -eq 1 ]
}
# 1. ALERT: Python Requirements (NET_RAW=13, NET_ADMIN=12)
if ! has_cap 13 || ! has_cap 12; then
printf "%s" "${RED}"
cat <<'EOF'
══════════════════════════════════════════════════════════════════════════════
🚨 ALERT: Python execution capabilities (NET_RAW/NET_ADMIN) are missing.
The Python binary in this image has file capabilities (+eip) that
require these bits in the container's bounding set. Without them,
the binary will fail to execute (Operation not permitted).
Restart with: --cap-add=NET_RAW --cap-add=NET_ADMIN
══════════════════════════════════════════════════════════════════════════════
EOF
printf "%s" "${RESET}"
fi
# 2. WARNING: NET_BIND_SERVICE (10)
if ! has_cap 10; then
printf "%s" "${YELLOW}"
cat <<'EOF'
══════════════════════════════════════════════════════════════════════════════
⚠️ WARNING: Reduced functionality (NET_BIND_SERVICE missing).
Tools like nbtscan cannot bind to privileged ports (UDP 137).
This will reduce discovery accuracy for legacy devices.
Consider adding: --cap-add=NET_BIND_SERVICE
══════════════════════════════════════════════════════════════════════════════
EOF
printf "%s" "${RESET}"
fi
# 3. NOTE: Security Context (CHOWN=0, SETGID=6, SETUID=7)
missing_admin=""
has_cap 0 || missing_admin="${missing_admin} CHOWN"
has_cap 6 || missing_admin="${missing_admin} SETGID"
has_cap 7 || missing_admin="${missing_admin} SETUID"
if [ -n "${missing_admin}" ]; then
printf "%sSecurity context: Operational capabilities (%s) not granted.%s\n" "${GREY}" "${missing_admin# }" "${RESET}"
if echo "${missing_admin}" | grep -q "CHOWN"; then
printf "%sSee https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/missing-capabilities.md%s\n" "${GREY}" "${RESET}"
fi
fi
exit 0

View File

@@ -326,8 +326,7 @@ def _apply_primary_rules(specs: list[PathSpec], results_map: dict[str, MountChec
suppress_primary = False
if all_core_subs_healthy and all_core_subs_are_mounts:
if not result.is_mount_point and not result.error and not result.write_error and not result.read_error:
suppress_primary = True
suppress_primary = True
if suppress_primary:
# All sub-paths are healthy and mounted; suppress the aggregate row.
@@ -368,104 +367,110 @@ def main():
r.dataloss_risk or r.error or r.write_error or r.read_error or r.performance_issue
for r in results
)
has_rw_errors = any(r.write_error or r.read_error for r in results)
has_rw_errors = any(
(r.write_error or r.read_error) and r.category == "persist"
for r in results
)
has_primary_dataloss = any(
r.category == "persist" and r.role == "primary" and r.dataloss_risk and r.is_mount_point
for r in results
)
if has_issues or True: # Always print table for diagnostic purposes
# --- Print Table ---
headers = ["Path", "R", "W", "Mount", "RAMDisk", "Performance", "DataLoss"]
# --- Print Table ---
headers = ["Path", "R", "W", "Mount", "RAMDisk", "Performance", "DataLoss"]
CHECK_SYMBOL = ""
CROSS_SYMBOL = ""
BLANK_SYMBOL = ""
CHECK_SYMBOL = ""
CROSS_SYMBOL = ""
BLANK_SYMBOL = ""
def bool_to_check(is_good):
return CHECK_SYMBOL if is_good else CROSS_SYMBOL
def bool_to_check(is_good):
return CHECK_SYMBOL if is_good else CROSS_SYMBOL
col_widths = [len(h) for h in headers]
for r in results:
col_widths[0] = max(col_widths[0], len(str(r.path)))
col_widths = [len(h) for h in headers]
for r in results:
col_widths[0] = max(col_widths[0], len(str(r.path)))
header_fmt = (
f" {{:<{col_widths[0]}}} |"
f" {{:^{col_widths[1]}}} |"
f" {{:^{col_widths[2]}}} |"
f" {{:^{col_widths[3]}}} |"
f" {{:^{col_widths[4]}}} |"
f" {{:^{col_widths[5]}}} |"
f" {{:^{col_widths[6]}}} "
)
header_fmt = (
f" {{:<{col_widths[0]}}} |"
f" {{:^{col_widths[1]}}} |"
f" {{:^{col_widths[2]}}} |"
f" {{:^{col_widths[3]}}} |"
f" {{:^{col_widths[4]}}} |"
f" {{:^{col_widths[5]}}} |"
f" {{:^{col_widths[6]}}} "
)
row_fmt = (
f" {{:<{col_widths[0]}}} |"
f" {{:^{col_widths[1]}}}|" # No space
f" {{:^{col_widths[2]}}}|" # No space
f" {{:^{col_widths[3]}}}|" # No space
f" {{:^{col_widths[4]}}}|" # No space
f" {{:^{col_widths[5]}}}|" # No space
f" {{:^{col_widths[6]}}} " # DataLoss is last, needs space
)
row_fmt = (
f" {{:<{col_widths[0]}}} |"
f" {{:^{col_widths[1]}}}|" # No space - intentional
f" {{:^{col_widths[2]}}}|" # No space - intentional
f" {{:^{col_widths[3]}}}|" # No space - intentional
f" {{:^{col_widths[4]}}}|" # No space - intentional
f" {{:^{col_widths[5]}}}|" # No space - intentional
f" {{:^{col_widths[6]}}} " # DataLoss is last, needs space
)
separator = "".join([
"-" * (col_widths[0] + 2),
"+",
"-" * (col_widths[1] + 2),
"+",
"-" * (col_widths[2] + 2),
"+",
"-" * (col_widths[3] + 2),
"+",
"-" * (col_widths[4] + 2),
"+",
"-" * (col_widths[5] + 2),
"+",
"-" * (col_widths[6] + 2)
])
separator = "".join([
"-" * (col_widths[0] + 2),
"+",
"-" * (col_widths[1] + 2),
"+",
"-" * (col_widths[2] + 2),
"+",
"-" * (col_widths[3] + 2),
"+",
"-" * (col_widths[4] + 2),
"+",
"-" * (col_widths[5] + 2),
"+",
"-" * (col_widths[6] + 2)
])
print(header_fmt.format(*headers), file=sys.stderr)
print(separator, file=sys.stderr)
for r in results:
# Symbol Logic
read_symbol = bool_to_check(r.is_readable)
write_symbol = bool_to_check(r.is_writeable)
print(header_fmt.format(*headers), file=sys.stderr)
print(separator, file=sys.stderr)
for r in results:
# Symbol Logic
read_symbol = bool_to_check(r.is_readable)
write_symbol = bool_to_check(r.is_writeable)
mount_symbol = CHECK_SYMBOL if r.is_mounted else CROSS_SYMBOL
mount_symbol = CHECK_SYMBOL if r.is_mounted else CROSS_SYMBOL
if r.category == "persist":
if r.underlying_fs_is_ramdisk or r.is_ramdisk:
ramdisk_symbol = CROSS_SYMBOL
else:
ramdisk_symbol = BLANK_SYMBOL
perf_symbol = BLANK_SYMBOL
elif r.category == "ramdisk":
ramdisk_symbol = CHECK_SYMBOL if r.is_ramdisk else CROSS_SYMBOL
perf_symbol = bool_to_check(not r.performance_issue)
if r.category == "persist":
if r.underlying_fs_is_ramdisk or r.is_ramdisk:
ramdisk_symbol = CROSS_SYMBOL
else:
ramdisk_symbol = BLANK_SYMBOL
perf_symbol = bool_to_check(not r.performance_issue)
perf_symbol = BLANK_SYMBOL
elif r.category == "ramdisk":
ramdisk_symbol = CHECK_SYMBOL if r.is_ramdisk else CROSS_SYMBOL
perf_symbol = bool_to_check(not r.performance_issue)
else:
ramdisk_symbol = BLANK_SYMBOL
perf_symbol = bool_to_check(not r.performance_issue)
dataloss_symbol = bool_to_check(not r.dataloss_risk)
dataloss_symbol = bool_to_check(not r.dataloss_risk)
print(
row_fmt.format(
r.path,
read_symbol,
write_symbol,
mount_symbol,
ramdisk_symbol,
perf_symbol,
dataloss_symbol,
),
file=sys.stderr
)
print(
row_fmt.format(
r.path,
read_symbol,
write_symbol,
mount_symbol,
ramdisk_symbol,
perf_symbol,
dataloss_symbol,
),
file=sys.stderr
)
# --- Print Warning ---
if has_issues:
print("\n", file=sys.stderr)
print_warning_message(results)
# --- Print Warning ---
if has_issues:
print("\n", file=sys.stderr)
print_warning_message(results)
# Exit with error only if there are read/write permission issues
if has_rw_errors and os.environ.get("NETALERTX_DEBUG") != "1":
sys.exit(1)
# Exit with error only if there are read/write permission issues
if (has_rw_errors or has_primary_dataloss) and os.environ.get("NETALERTX_DEBUG") != "1":
sys.exit(1)
if __name__ == "__main__":

View File

@@ -4,7 +4,6 @@
set -eu
YELLOW=$(printf '\033[1;33m')
CYAN=$(printf '\033[1;36m')
RED=$(printf '\033[1;31m')
RESET=$(printf '\033[0m')

View File

@@ -1,93 +0,0 @@
#!/bin/sh
# Initialize required directories and log files
# These must exist before services start to avoid permission/write errors
check_mandatory_folders() {
# Base volatile directories live on /tmp mounts and must always exist
if [ ! -d "${NETALERTX_LOG}" ]; then
echo " * Creating NetAlertX log directory."
if ! mkdir -p "${NETALERTX_LOG}"; then
echo "Error: Failed to create log directory: ${NETALERTX_LOG}"
return 1
fi
chmod 700 "${NETALERTX_LOG}" 2>/dev/null || true
fi
if [ ! -d "${NETALERTX_API}" ]; then
echo " * Creating NetAlertX API cache."
if ! mkdir -p "${NETALERTX_API}"; then
echo "Error: Failed to create API cache directory: ${NETALERTX_API}"
return 1
fi
chmod 700 "${NETALERTX_API}" 2>/dev/null || true
fi
if [ ! -d "${SYSTEM_SERVICES_RUN}" ]; then
echo " * Creating System services runtime directory."
if ! mkdir -p "${SYSTEM_SERVICES_RUN}"; then
echo "Error: Failed to create System services runtime directory: ${SYSTEM_SERVICES_RUN}"
return 1
fi
chmod 700 "${SYSTEM_SERVICES_RUN}" 2>/dev/null || true
fi
if [ ! -d "${SYSTEM_SERVICES_ACTIVE_CONFIG}" ]; then
echo " * Creating nginx active configuration directory."
if ! mkdir -p "${SYSTEM_SERVICES_ACTIVE_CONFIG}"; then
echo "Error: Failed to create nginx active configuration directory: ${SYSTEM_SERVICES_ACTIVE_CONFIG}"
return 1
fi
chmod 700 "${SYSTEM_SERVICES_ACTIVE_CONFIG}" 2>/dev/null || true
fi
# Check and create plugins log directory
if [ ! -d "${NETALERTX_PLUGINS_LOG}" ]; then
echo " * Creating Plugins log."
if ! mkdir -p "${NETALERTX_PLUGINS_LOG}"; then
echo "Error: Failed to create plugins log directory: ${NETALERTX_PLUGINS_LOG}"
return 1
fi
chmod 700 "${NETALERTX_PLUGINS_LOG}" 2>/dev/null || true
fi
# Check and create system services run log directory
if [ ! -d "${SYSTEM_SERVICES_RUN_LOG}" ]; then
echo " * Creating System services run log."
if ! mkdir -p "${SYSTEM_SERVICES_RUN_LOG}"; then
echo "Error: Failed to create system services run log directory: ${SYSTEM_SERVICES_RUN_LOG}"
return 1
fi
chmod 700 "${SYSTEM_SERVICES_RUN_LOG}" 2>/dev/null || true
fi
# Check and create system services run tmp directory
if [ ! -d "${SYSTEM_SERVICES_RUN_TMP}" ]; then
echo " * Creating System services run tmp."
if ! mkdir -p "${SYSTEM_SERVICES_RUN_TMP}"; then
echo "Error: Failed to create system services run tmp directory: ${SYSTEM_SERVICES_RUN_TMP}"
return 1
fi
chmod 700 "${SYSTEM_SERVICES_RUN_TMP}" 2>/dev/null || true
fi
# Check and create DB locked log file
if [ ! -f "${LOG_DB_IS_LOCKED}" ]; then
echo " * Creating DB locked log."
if ! touch "${LOG_DB_IS_LOCKED}"; then
echo "Error: Failed to create DB locked log file: ${LOG_DB_IS_LOCKED}"
return 1
fi
fi
# Check and create execution queue log file
if [ ! -f "${LOG_EXECUTION_QUEUE}" ]; then
echo " * Creating Execution queue log."
if ! touch "${LOG_EXECUTION_QUEUE}"; then
echo "Error: Failed to create execution queue log file: ${LOG_EXECUTION_QUEUE}"
return 1
fi
fi
}
# Run the function
check_mandatory_folders

View File

@@ -0,0 +1,103 @@
#!/bin/sh
# Initialize required directories and log files
# These must exist before services start to avoid permission/write errors
# This script is intended to enhance observability of system startup issues.
is_tmp_path() {
case "$1" in
/tmp/*|/tmp) return 0 ;;
*) return 1 ;;
esac
}
warn_tmp_skip() {
echo "Warning: Unable to create $2 at $1 (tmpfs not writable with current capabilities)."
}
ensure_dir() {
# When creating as the user running the services, we ensure correct ownership and access
path="$1"
label="$2"
if ! mkdir -p "${path}" 2>/dev/null; then
if is_tmp_path "${path}"; then
warn_tmp_skip "${path}" "${label}"
return 0
fi
echo "Error: Failed to create ${label}: ${path}"
return 1
fi
chmod 700 "${path}" 2>/dev/null || true
}
ensure_file() {
path="$1"
label="$2"
# When we touch as the user running the services, we ensure correct ownership
if ! touch "${path}" 2>/dev/null; then
if is_tmp_path "${path}"; then
warn_tmp_skip "${path}" "${label}"
return 0
fi
echo "Error: Failed to create ${label}: ${path}"
return 1
fi
}
check_mandatory_folders() {
# Base volatile directories live on /tmp mounts and must always exist
if [ ! -d "${NETALERTX_LOG}" ]; then
echo " * Creating NetAlertX log directory."
ensure_dir "${NETALERTX_LOG}" "log directory" || return 1
fi
if [ ! -d "${NETALERTX_API}" ]; then
echo " * Creating NetAlertX API cache."
ensure_dir "${NETALERTX_API}" "API cache directory" || return 1
fi
if [ ! -d "${SYSTEM_SERVICES_RUN}" ]; then
echo " * Creating System services runtime directory."
ensure_dir "${SYSTEM_SERVICES_RUN}" "System services runtime directory" || return 1
fi
if [ ! -d "${SYSTEM_SERVICES_ACTIVE_CONFIG}" ]; then
echo " * Creating nginx active configuration directory."
ensure_dir "${SYSTEM_SERVICES_ACTIVE_CONFIG}" "nginx active configuration directory" || return 1
fi
# Check and create plugins log directory
if [ ! -d "${NETALERTX_PLUGINS_LOG}" ]; then
echo " * Creating Plugins log."
ensure_dir "${NETALERTX_PLUGINS_LOG}" "plugins log directory" || return 1
fi
# Check and create system services run log directory
if [ ! -d "${SYSTEM_SERVICES_RUN_LOG}" ]; then
echo " * Creating System services run log."
ensure_dir "${SYSTEM_SERVICES_RUN_LOG}" "system services run log directory" || return 1
fi
# Check and create system services run tmp directory
if [ ! -d "${SYSTEM_SERVICES_RUN_TMP}" ]; then
echo " * Creating System services run tmp."
ensure_dir "${SYSTEM_SERVICES_RUN_TMP}" "system services run tmp directory" || return 1
fi
# Check and create DB locked log file
if [ ! -f "${LOG_DB_IS_LOCKED}" ]; then
echo " * Creating DB locked log."
ensure_file "${LOG_DB_IS_LOCKED}" "DB locked log file" || return 1
fi
# Check and create execution queue log file
if [ ! -f "${LOG_EXECUTION_QUEUE}" ]; then
echo " * Creating Execution queue log."
ensure_file "${LOG_EXECUTION_QUEUE}" "execution queue log file" || return 1
fi
}
# Create the folders and files.
# Create a log message for observability if any fail.
check_mandatory_folders

View File

@@ -4,8 +4,8 @@
OVERRIDE_FILE="${NETALERTX_CONFIG}/app_conf_override.json"
# Ensure config directory exists
mkdir -p "$(dirname "$NETALERTX_CONFIG")" || {
>&2 echo "ERROR: Failed to create config directory $(dirname "$NETALERTX_CONFIG")"
mkdir -p "$NETALERTX_CONFIG" || {
>&2 echo "ERROR: Failed to create config directory $NETALERTX_CONFIG"
exit 1
}

View File

@@ -1,6 +1,6 @@
#!/bin/sh
# 30-writable-config.sh: Verify read/write permissions for config and database files.
# 40-writable-config.sh: Verify read/write permissions for config and database files.
#
# This script ensures that the application can read from and write to the
# critical configuration and database files after startup.
@@ -72,7 +72,7 @@ EOF
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Read permission denied (write permission denied).
⚠️ ATTENTION: Write permission denied.
The application cannot write to "${path}". This will prevent it from
saving data, logs, or configuration.

View File

@@ -1,6 +1,7 @@
#!/bin/sh
# check-nginx-config.sh - verify nginx conf.active mount is writable when PORT != 20211.
# Only check nginx config writability if PORT is not the default 20211
if [ "${PORT:-20211}" = "20211" ]; then
exit 0
@@ -9,7 +10,7 @@ fi
CONF_ACTIVE_DIR="${SYSTEM_SERVICES_ACTIVE_CONFIG}"
TARGET_FILE="${CONF_ACTIVE_DIR}/netalertx.conf"
# If the directory is missing entirely we warn and exit failure so the caller can see the message.
# If the directory is missing entirely we warn and exit 0 to allow startup with defaults.
if [ ! -d "${CONF_ACTIVE_DIR}" ]; then
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
@@ -30,7 +31,7 @@ if [ ! -d "${CONF_ACTIVE_DIR}" ]; then
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 1
exit 0
fi
TMP_FILE="${CONF_ACTIVE_DIR}/.netalertx-write-test"
@@ -52,7 +53,7 @@ if ! ( : >"${TMP_FILE}" ) 2>/dev/null; then
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 1
exit 0 # Nginx can continue using default config on port 20211
fi
rm -f "${TMP_FILE}"

View File

@@ -0,0 +1,48 @@
#!/bin/sh
# expected-user-id-match.sh - ensure the container is running as the intended runtime UID/GID.
EXPECTED_USER="${NETALERTX_USER:-netalertx}"
CURRENT_UID="$(id -u)"
CURRENT_GID="$(id -g)"
# If PUID/PGID explicitly set, require that we are running as them.
if [ -n "${PUID:-}" ] || [ -n "${PGID:-}" ]; then
TARGET_UID="${PUID:-${CURRENT_UID}}"
TARGET_GID="${PGID:-${CURRENT_GID}}"
if [ "${CURRENT_UID}" -ne "${TARGET_UID}" ] || [ "${CURRENT_GID}" -ne "${TARGET_GID}" ]; then
if [ "${NETALERTX_PRIVDROP_FAILED:-0}" -ne 0 ]; then
>&2 printf 'Note: PUID/PGID=%s:%s requested but privilege drop failed; continuing as UID %s GID %s. See docs/docker-troubleshooting/missing-capabilities.md\n' \
"${TARGET_UID}" "${TARGET_GID}" "${CURRENT_UID}" "${CURRENT_GID}"
exit 0
fi
if [ "${CURRENT_UID}" -ne 0 ]; then
>&2 printf 'Note: PUID/PGID=%s:%s requested but container is running as fixed UID %s GID %s; PUID/PGID will not be applied.\n' \
"${TARGET_UID}" "${TARGET_GID}" "${CURRENT_UID}" "${CURRENT_GID}"
exit 0
fi
>&2 printf 'FATAL: NetAlertX running as UID %s GID %s, expected PUID/PGID %s:%s\n' \
"${CURRENT_UID}" "${CURRENT_GID}" "${TARGET_UID}" "${TARGET_GID}"
exit 1
fi
exit 0
fi
EXPECTED_UID="$(getent passwd "${EXPECTED_USER}" 2>/dev/null | cut -d: -f3)"
EXPECTED_GID="$(getent passwd "${EXPECTED_USER}" 2>/dev/null | cut -d: -f4)"
# Fallback to known defaults when lookups fail
if [ -z "${EXPECTED_UID}" ]; then
EXPECTED_UID="${CURRENT_UID}"
fi
if [ -z "${EXPECTED_GID}" ]; then
EXPECTED_GID="${CURRENT_GID}"
fi
if [ "${CURRENT_UID}" -eq "${EXPECTED_UID}" ] && [ "${CURRENT_GID}" -eq "${EXPECTED_GID}" ]; then
exit 0
fi
>&2 printf '\nNetAlertX note: current UID %s GID %s, expected UID %s GID %s\n' \
"${CURRENT_UID}" "${CURRENT_GID}" "${EXPECTED_UID}" "${EXPECTED_GID}"
exit 0

View File

@@ -1,23 +0,0 @@
#!/bin/sh
# check-user-netalertx.sh - ensure the container is running as the hardened service user.
EXPECTED_USER="${NETALERTX_USER:-netalertx}"
EXPECTED_UID="$(getent passwd "${EXPECTED_USER}" 2>/dev/null | cut -d: -f3)"
EXPECTED_GID="$(getent passwd "${EXPECTED_USER}" 2>/dev/null | cut -d: -f4)"
CURRENT_UID="$(id -u)"
CURRENT_GID="$(id -g)"
# Fallback to known defaults when lookups fail
if [ -z "${EXPECTED_UID}" ]; then
EXPECTED_UID="${CURRENT_UID}"
fi
if [ -z "${EXPECTED_GID}" ]; then
EXPECTED_GID="${CURRENT_GID}"
fi
if [ "${CURRENT_UID}" -eq "${EXPECTED_UID}" ] && [ "${CURRENT_GID}" -eq "${EXPECTED_GID}" ]; then
exit 0
fi
>&2 printf '\nNetAlertX note: current UID %s GID %s, expected UID %s GID %s\n' \
"${CURRENT_UID}" "${CURRENT_GID}" "${EXPECTED_UID}" "${EXPECTED_GID}"
exit 0

View File

@@ -1,33 +0,0 @@
#!/bin/sh
# layer-2-network.sh - Uses a real nmap command to detect missing container
# privileges and warns the user. It is silent on success.
# Run a fast nmap command that requires raw sockets, capturing only stderr.
ERROR_OUTPUT=$(nmap --privileged -sS -p 20211 127.0.0.1 2>&1)
EXIT_CODE=$?
# Flag common capability errors regardless of exact exit code.
if [ "$EXIT_CODE" -ne 0 ] && \
echo "$ERROR_OUTPUT" | grep -q -e "Operation not permitted" -e "requires root privileges"
then
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&2 printf "%s" "${YELLOW}"
>&2 cat <<'EOF'
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Raw network capabilities are missing.
Tools that rely on NET_RAW/NET_ADMIN/NET_BIND_SERVICE (e.g. nmap -sS,
arp-scan, nbtscan) will not function. Restart the container with:
--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=NET_BIND_SERVICE
Without those caps, NetAlertX cannot inspect your network. Fix it before
trusting any results.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/missing-capabilities.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
fi
exit 0 # Always exit success even after warnings

View File

@@ -1,13 +1,13 @@
#!/bin/bash
# Bash used in this check for simplicty of math operations.
#!/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.
# if we are running in devcontainer then we should exit imemditely without checking
# if we are running in devcontainer then we should exit immediately without checking
# The devcontainer is set up to have additional permissions which are not granted
# in production so this check would always fail there.
if [ "${NETALERTX_DEBUG}" == "1" ]; then
if [ "${NETALERTX_DEBUG}" = "1" ]; then
exit 0
fi
@@ -18,8 +18,8 @@ if [ -z "$BND_HEX" ]; then
exit 0
fi
# Convert hex to decimal
BND_DEC=$(( 16#$BND_HEX )) || exit 0
#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 ) ))

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/bash
################################################################################
# NetAlertX Container Entrypoint
@@ -46,6 +46,17 @@ if [ "$#" -gt 0 ]; then
esac
fi
# If invoked directly (bypassing root-entrypoint), re-enter through it once for priming
# and privilege drop. Guard with ENTRYPOINT_PRIMED to avoid loops when root-entrypoint
# hands control back to this script.
if [ "${ENTRYPOINT_PRIMED:-0}" != "1" ] && [ "$(id -u)" -eq 0 ] && [ -x "/root-entrypoint.sh" ]; then
>&2 cat <<'EOF'
NetAlertX is running as ROOT (UID 0). Prefer setting PUID/PGID to 20211 for better isolation.
EOF
export ENTRYPOINT_PRIMED=1
exec /root-entrypoint.sh "$@"
fi
# Banner display
RED='\033[1;31m'
GREY='\033[90m'
@@ -92,12 +103,9 @@ https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/trou
EOF
>&2 printf "%s" "${RESET}"
FAILED_STATUS="1"
if [ "${NETALERTX_DEBUG:-0}" -eq 1 ]; then
FAILED_STATUS="1"
echo "NETALERTX_DEBUG=1, continuing despite critical failure in ${script_name}."
else
exit 1
fi
elif [ ${NETALERTX_DOCKER_ERROR_CHECK} -ne 0 ]; then
# fail but continue checks so user can see all issues
@@ -264,9 +272,6 @@ trap on_signal INT TERM
################################################################################
# Service Startup Section
################################################################################
# Start services based on environment configuration
# Only start crond scheduler on Alpine (non-Debian) environments

View File

@@ -0,0 +1,130 @@
#!/bin/bash
# NetAlertX Root-Priming Entrypoint — best-effort permission priming 🔧
#
# Purpose:
# - Provide a runtime, best-effort remedy for host volume ownership/mode issues
# (common on appliances like Synology where Docker volume copyup is limited).
# - Ensure writable paths exist, attempt to `chown`/`chmod` 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
# 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.
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')
_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.
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
}
_validate_id "${PUID}" "PUID"
_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
has_setgid=1
fi
if [ $((cap_dec & (1 << 7))) -ne 0 ]; 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
has_net_caps=1
fi
if [ "${has_net_caps}" -eq 1 ] && { [ "${has_setgid}" -eq 0 ] || [ "${has_setuid}" -eq 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}"
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"
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
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
chown -R "${PUID}:${PGID}" "${path}" 2>/dev/null || true
chmod -R u+rwX "${path}" 2>/dev/null || true
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}"
>&2 echo "Note: su-exec failed (exit ${rc}); continuing as current user without privilege drop."
exec /entrypoint.sh "$@"
fi

View File

@@ -54,11 +54,11 @@ chmod -R 777 "/tmp/nginx" 2>/dev/null || true
# Execute nginx with overrides
# echo the full nginx command then run it
echo "Starting /usr/sbin/nginx -p \"${RUN_DIR}/\" -c \"${SYSTEM_SERVICES_ACTIVE_CONFIG_FILE}\" -g \"error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;\" &"
echo "Starting /usr/sbin/nginx -p \"${RUN_DIR}/\" -c \"${SYSTEM_SERVICES_ACTIVE_CONFIG_FILE}\" -g \"error_log stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;\" &"
/usr/sbin/nginx \
-p "${RUN_DIR}/" \
-c "${SYSTEM_SERVICES_ACTIVE_CONFIG_FILE}" \
-g "error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;" &
-g "error_log stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;" &
nginx_pid=$!
wait "${nginx_pid}"

View File

@@ -26,8 +26,9 @@ done
trap cleanup EXIT
trap forward_signal INT TERM
echo "Starting /usr/sbin/php-fpm83 -y \"${PHP_FPM_CONFIG_FILE}\" -F >>\"${LOG_APP_PHP_ERRORS}\" 2>/dev/stderr &"
/usr/sbin/php-fpm83 -y "${PHP_FPM_CONFIG_FILE}" -F >>"${LOG_APP_PHP_ERRORS}" 2> /dev/stderr &
echo "Starting /usr/sbin/php-fpm83 -y \"${PHP_FPM_CONFIG_FILE}\" -F (tee stderr to app.php_errors.log)"
php_fpm_cmd=(/usr/sbin/php-fpm83 -y "${PHP_FPM_CONFIG_FILE}" -F)
"${php_fpm_cmd[@]}" 2> >(tee -a "${LOG_APP_PHP_ERRORS}" >&2) &
php_fpm_pid=$!
wait "${php_fpm_pid}"