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
@@ -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
@@ -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
@@ -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
@@ -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__":
@@ -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')
@@ -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
@@ -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
@@ -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
}
@@ -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.
@@ -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}"
@@ -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
@@ -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
@@ -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
@@ -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 ) ))