/data and /tmp standarization

This commit is contained in:
Adam Outler
2025-11-04 22:26:35 +00:00
parent 90a07c61eb
commit 5b871865db
250 changed files with 7462 additions and 4940 deletions

View File

@@ -6,12 +6,20 @@ This document describes the filesystem structure of the NetAlertX production Doc
## Directory Structure
### `/app` - Main Application Directory
The core application location where NetAlertX runs. This directory contains the main application code and working data, with source code directories mounted in read-only mode for security. It provides the runtime environment for all NetAlertX operations including device scanning, web interface, and data processing.
The core application location where NetAlertX runs. This directory contains only the application code in production. Configuration, database files, and logs now live in dedicated `/data` and `/tmp` mounts to keep the runtime read-only and auditable.
The core application location. Contains:
- Source code directories (`back`, `front`, `server`) copied in read-only mode
- Working directories for runtime data (`config`, `db`, `log`)
- Other directories are not needed in production and are excluded
- Service orchestration scripts under `/services`
- No persistent data or logs—those are redirected to `/data` and `/tmp`
### `/data` - Persistent Configuration and Database
Writable volume that stores administrator-managed settings and database state. The entrypoint ensures directories are owned by the `netalertx` user (UID 20211).
Contains:
- `/data/config` - persisted settings such as `app.conf`
- `/data/db` - SQLite database files (e.g., `app.db`)
- Optional host bind mounts for backups or external sync
### `/build` - Build-Time Scripts
Temporary directory used during Docker image building to prepare the container environment. Scripts in this directory run during the build process to set up the system before it's locked down for production use. This ensures the container is properly configured before runtime.
@@ -59,10 +67,13 @@ Pre-startup checks and specialized maintenance tools:
- `list-ports.sh` - Network port enumeration script
- `opnsense_leases/` - OPNsense DHCP lease integration tools
#### `/services/run` - Runtime Data
Directory for storing runtime data and logs generated by services during container operation. This provides a centralized location for monitoring service activity and troubleshooting issues that occur during normal operation.
### `/tmp` - Ephemeral Runtime Data
All writable runtime data is consolidated under `/tmp`, which is mounted as `tmpfs` by default for speed and automatic cleanup on restart.
- `logs/` - Service runtime log files
- `/tmp/log` - Application, PHP, and plugin logs (bind mount to persist between restarts)
- `/tmp/api` - Cached API responses for the UI (configurable via `NETALERTX_API` environment variable)
- `/tmp/nginx/active-config` - Optional override directory for nginx configuration
- `/tmp/run` - Runtime socket and temp directories for nginx and PHP (`client_body`, `proxy`, `php.sock`, etc.)
#### Service Control Scripts
Scripts that start and manage the core services required for NetAlertX operation. These scripts handle the initialization of the web server, application server, task scheduler, and backend processing components that work together to provide network monitoring functionality.

View File

@@ -13,12 +13,13 @@ RESET=$(printf '\033[0m')
# 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}
${NETALERTX_DB_FILE}
"
@@ -39,7 +40,7 @@ if [ "$(id -u)" -eq 0 ]; then
* switch to the default USER in the image (20211:20211)
IMPORTANT: This corrective mode automatically adjusts ownership of
/app/db and /app/config directories to the netalertx user, ensuring
/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
@@ -54,8 +55,8 @@ EOF
chown -R netalertx ${READ_WRITE_PATHS} 2>/dev/null || true
# Set directory and file permissions for all read-write paths
find ${READ_WRITE_PATHS} -type d -exec chmod u+rwx {}
find ${READ_WRITE_PATHS} -type f -exec chmod u+rw {}
find ${READ_WRITE_PATHS} -type d -exec chmod u+rwx {} \;
find ${READ_WRITE_PATHS} -type f -exec chmod u+rw {} \;
echo Permissions fixed for read-write paths. Please restart the container as user 20211.
sleep infinity & wait $!
fi

View File

@@ -0,0 +1,145 @@
#!/bin/sh
# 01-data-migration.sh - consolidate legacy /app mounts into /data
set -eu
YELLOW=$(printf '\033[1;33m')
CYAN=$(printf '\033[1;36m')
RED=$(printf '\033[1;31m')
RESET=$(printf '\033[0m')
DATA_DIR=${NETALERTX_DATA:-/data}
TARGET_CONFIG=${NETALERTX_CONFIG:-${DATA_DIR}/config}
TARGET_DB=${NETALERTX_DB:-${DATA_DIR}/db}
LEGACY_CONFIG=/app/config
LEGACY_DB=/app/db
MARKER_NAME=.migration
is_mounted() {
local path="$1"
if [ ! -d "${path}" ]; then
return 1
fi
mountpoint -q "${path}" 2>/dev/null
}
warn_unmount_legacy() {
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Legacy mounts detected at ${LEGACY_CONFIG} or ${LEGACY_DB}.
Migration markers are present. Your data now lives under ${DATA_DIR}.
Unmount the legacy /app/config and /app/db paths from your docker-compose
file to avoid stale mounts on future starts.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
}
fatal_missing_data_mount() {
>&2 printf "%s" "${RED}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
❌ CRITICAL: /data is not mounted but legacy mounts are still present.
Mount the new consolidated volume at ${DATA_DIR} so data can be migrated.
Once mounted, restart the container to complete migration automatically.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
}
migrate_legacy_mounts() {
>&2 printf "%s" "${CYAN}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
🛠️ Migrating legacy /app mounts into ${DATA_DIR}.
Existing configuration and database files will be copied into the new
consolidated volume. This runs once per environment.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
mkdir -p "${TARGET_CONFIG}" "${TARGET_DB}" || return 1
chmod 700 "${TARGET_CONFIG}" "${TARGET_DB}" 2>/dev/null || true
if ! cp -a "${LEGACY_CONFIG}/." "${TARGET_CONFIG}/"; then
>&2 printf "%s" "${RED}"
>&2 echo "Migration failed while copying configuration files."
>&2 printf "%s" "${RESET}"
return 1
fi
if ! cp -a "${LEGACY_DB}/." "${TARGET_DB}/"; then
>&2 printf "%s" "${RED}"
>&2 echo "Migration failed while copying database files."
>&2 printf "%s" "${RESET}"
return 1
fi
touch "${LEGACY_CONFIG}/${MARKER_NAME}" "${LEGACY_DB}/${MARKER_NAME}" 2>/dev/null || true
warn_unmount_legacy
return 0
}
CONFIG_MARKED=false
DB_MARKED=false
[ -f "${LEGACY_CONFIG}/${MARKER_NAME}" ] && CONFIG_MARKED=true
[ -f "${LEGACY_DB}/${MARKER_NAME}" ] && DB_MARKED=true
if ${CONFIG_MARKED} || ${DB_MARKED}; then
warn_unmount_legacy
exit 0
fi
CONFIG_MOUNTED=false
DB_MOUNTED=false
DATA_MOUNTED=false
is_mounted "${LEGACY_CONFIG}" && CONFIG_MOUNTED=true
is_mounted "${LEGACY_DB}" && DB_MOUNTED=true
is_mounted "${DATA_DIR}" && DATA_MOUNTED=true
# Nothing to migrate if legacy mounts are absent
if ! ${CONFIG_MOUNTED} && ! ${DB_MOUNTED}; then
exit 0
fi
# Partial legacy mount state, notify and exit
if ${CONFIG_MOUNTED} && ! ${DB_MOUNTED}; then
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: /app/config is still mounted but /app/db is not.
Mount both legacy paths alongside ${DATA_DIR} to migrate automatically,
or unmount /app/config entirely.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 0
fi
if ${DB_MOUNTED} && ! ${CONFIG_MOUNTED}; then
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: /app/db is still mounted but /app/config is not.
Mount both legacy paths alongside ${DATA_DIR} to migrate automatically,
or unmount /app/db entirely.
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
exit 0
fi
if ! ${DATA_MOUNTED}; then
fatal_missing_data_mount
exit 1
fi
migrate_legacy_mounts || exit 1
exit 0

View File

@@ -7,30 +7,60 @@ from dataclasses import dataclass
# if NETALERTX_DEBUG is 1 then exit
if os.environ.get("NETALERTX_DEBUG") == "1":
sys.exit(0)
@dataclass
class MountCheckResult:
"""Object to track mount status and potential issues."""
var_name: str
path: str = ""
is_writeable: bool = False
is_mounted: bool = False
is_mount_point: bool = False
is_ramdisk: bool = False
underlying_fs_is_ramdisk: bool = False # Track this separately
underlying_fs_is_ramdisk: bool = False # Track this separately
fstype: str = "N/A"
error: bool = False
write_error: bool = False
performance_issue: bool = False
dataloss_risk: bool = False
category: str = ""
role: str = ""
group: str = ""
@dataclass(frozen=True)
class PathSpec:
"""Describes how a filesystem path should behave."""
var_name: str
category: str # e.g. persist, ramdisk
role: str # primary, sub, secondary
group: str # logical grouping for primary/sub relationships
PATH_SPECS = (
PathSpec("NETALERTX_DATA", "persist", "primary", "data"),
PathSpec("NETALERTX_DB", "persist", "sub", "data"),
PathSpec("NETALERTX_CONFIG", "persist", "sub", "data"),
PathSpec("SYSTEM_SERVICES_RUN_TMP", "ramdisk", "primary", "tmp"),
PathSpec("NETALERTX_API", "ramdisk", "sub", "tmp"),
PathSpec("NETALERTX_LOG", "ramdisk", "sub", "tmp"),
PathSpec("SYSTEM_SERVICES_RUN", "ramdisk", "sub", "tmp"),
PathSpec("SYSTEM_SERVICES_ACTIVE_CONFIG", "ramdisk", "secondary", "tmp"),
)
def get_mount_info():
"""Parses /proc/mounts to get a dict of {mount_point: fstype}."""
mounts = {}
try:
with open('/proc/mounts', 'r') as f:
with open("/proc/mounts", "r") as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 3:
mount_point = parts[1].replace('\\040', ' ')
mount_point = parts[1].replace("\\040", " ")
fstype = parts[2]
mounts[mount_point] = fstype
except FileNotFoundError:
@@ -38,78 +68,112 @@ def get_mount_info():
return None
return mounts
def analyze_path(var_name, is_persistent, mounted_filesystems, non_persistent_fstypes, read_only_vars):
def _resolve_writeable_state(target_path: str) -> bool:
"""Determine if a path is writeable, ascending to the first existing parent."""
current = target_path
seen: set[str] = set()
while True:
if current in seen:
break
seen.add(current)
if os.path.exists(current):
return os.access(current, os.W_OK)
parent_dir = os.path.dirname(current)
if not parent_dir or parent_dir == current:
break
current = parent_dir
return False
def analyze_path(
spec: PathSpec,
mounted_filesystems,
non_persistent_fstypes,
):
"""
Analyzes a single path, checking for errors, performance, and dataloss.
"""
result = MountCheckResult(var_name=var_name)
target_path = os.environ.get(var_name)
result = MountCheckResult(
var_name=spec.var_name,
category=spec.category,
role=spec.role,
group=spec.group,
)
target_path = os.environ.get(spec.var_name)
if target_path is None:
result.path = f"({var_name} unset)"
result.path = f"({spec.var_name} unset)"
result.error = True
return result
result.path = target_path
# --- 1. Check Write Permissions ---
is_writeable = os.access(target_path, os.W_OK)
if not is_writeable and not os.path.exists(target_path):
parent_dir = os.path.dirname(target_path)
if os.access(parent_dir, os.W_OK):
is_writeable = True
result.is_writeable = is_writeable
if var_name not in read_only_vars and not result.is_writeable:
result.is_writeable = _resolve_writeable_state(target_path)
if not result.is_writeable:
result.error = True
result.write_error = True
if spec.role != "secondary":
result.write_error = True
# --- 2. Check Filesystem Type (Parent and Self) ---
parent_mount_fstype = ""
longest_mount = ""
for mount_point, fstype in mounted_filesystems.items():
if target_path.startswith(mount_point):
if len(mount_point) > len(longest_mount):
longest_mount = mount_point
normalized = mount_point.rstrip("/") if mount_point != "/" else "/"
if target_path == normalized or target_path.startswith(f"{normalized}/"):
if len(normalized) > len(longest_mount):
longest_mount = normalized
parent_mount_fstype = fstype
result.underlying_fs_is_ramdisk = parent_mount_fstype in non_persistent_fstypes
if parent_mount_fstype:
result.fstype = parent_mount_fstype
# --- 3. Check if path IS a mount point ---
if target_path in mounted_filesystems:
result.is_mounted = True
result.is_mount_point = True
result.fstype = mounted_filesystems[target_path]
result.is_ramdisk = result.fstype in non_persistent_fstypes
else:
result.is_mounted = False
result.is_ramdisk = False
if longest_mount and longest_mount != "/":
if target_path == longest_mount or target_path.startswith(
f"{longest_mount}/"
):
result.is_mounted = True
result.fstype = parent_mount_fstype
result.is_ramdisk = parent_mount_fstype in non_persistent_fstypes
# --- 4. Apply Risk Logic ---
if is_persistent:
if result.underlying_fs_is_ramdisk:
if spec.category == "persist":
if result.underlying_fs_is_ramdisk or result.is_ramdisk:
result.dataloss_risk = True
if not result.is_mounted:
result.dataloss_risk = True
else:
# Performance issue if it's not a ramdisk mount
elif spec.category == "ramdisk":
if not result.is_mounted or not result.is_ramdisk:
result.performance_issue = True
return result
def print_warning_message():
"""Prints a formatted warning to stderr."""
YELLOW = '\033[1;33m'
RESET = '\033[0m'
YELLOW = "\033[1;33m"
RESET = "\033[0m"
message = (
"══════════════════════════════════════════════════════════════════════════════\n"
"⚠️ ATTENTION: Configuration issues detected (marked with ❌).\n\n"
@@ -122,61 +186,139 @@ def print_warning_message():
" https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/mount-configuration-issues.md\n"
"══════════════════════════════════════════════════════════════════════════════\n"
)
print(f"{YELLOW}{message}{RESET}", file=sys.stderr)
def main():
NON_PERSISTENT_FSTYPES = {'tmpfs', 'ramfs'}
PERSISTENT_VARS = {'NETALERTX_DB', 'NETALERTX_CONFIG'}
# Define all possible read-only vars
READ_ONLY_VARS = {'SYSTEM_NGINX_CONFIG', 'SYSTEM_SERVICES_ACTIVE_CONFIG'}
# Base paths to check
PATHS_TO_CHECK = {
'NETALERTX_DB': True,
'NETALERTX_CONFIG': True,
'NETALERTX_API': False,
'NETALERTX_LOG': False,
'SYSTEM_SERVICES_RUN': False,
}
# *** KEY CHANGE: Conditionally add path based on PORT ***
port_val = os.environ.get("PORT")
if port_val is not None and port_val != "20211":
PATHS_TO_CHECK['SYSTEM_SERVICES_ACTIVE_CONFIG'] = False
# *** END KEY CHANGE ***
def _get_active_specs() -> list[PathSpec]:
"""Return the path specifications that should be evaluated for this run."""
return list(PATH_SPECS)
def _sub_result_is_healthy(result: MountCheckResult) -> bool:
"""Determine if a sub-path result satisfies its expected constraints."""
if result.category == "persist":
if not result.is_mounted:
return False
if result.dataloss_risk or result.write_error or result.error:
return False
return True
if result.category == "ramdisk":
if not result.is_mounted or not result.is_ramdisk:
return False
if result.performance_issue or result.write_error or result.error:
return False
return True
return False
def _apply_primary_rules(specs: list[PathSpec], results_map: dict[str, MountCheckResult]) -> list[MountCheckResult]:
"""Suppress or flag primary rows based on the state of their sub-paths."""
final_results: list[MountCheckResult] = []
specs_by_group: dict[str, list[PathSpec]] = {}
for spec in specs:
specs_by_group.setdefault(spec.group, []).append(spec)
for spec in specs:
result = results_map.get(spec.var_name)
if result is None:
continue
if spec.role == "primary":
group_specs = specs_by_group.get(spec.group, [])
sub_results_all = [
results_map[s.var_name]
for s in group_specs
if s.var_name in results_map and s.var_name != spec.var_name
]
core_sub_results = [
results_map[s.var_name]
for s in group_specs
if s.var_name in results_map and s.role == "sub"
]
sub_mount_points = [sub for sub in sub_results_all if sub.is_mount_point]
core_mount_points = [sub for sub in core_sub_results if sub.is_mount_point]
all_core_subs_healthy = bool(core_sub_results) and all(
_sub_result_is_healthy(sub) for sub in core_sub_results
)
all_core_subs_are_mounts = bool(core_sub_results) and len(core_mount_points) == len(core_sub_results)
if all_core_subs_healthy:
if result.write_error:
result.write_error = False
if not result.is_writeable:
result.is_writeable = True
if spec.category == "persist" and result.dataloss_risk:
result.dataloss_risk = False
if result.error and not (result.performance_issue or result.dataloss_risk or result.write_error):
result.error = False
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:
suppress_primary = True
if suppress_primary:
# All sub-paths are healthy and mounted; suppress the aggregate row.
continue
if sub_mount_points and result.is_mount_point:
result.error = True
if result.category == "persist":
result.dataloss_risk = True
elif result.category == "ramdisk":
result.performance_issue = True
final_results.append(result)
return final_results
def main():
NON_PERSISTENT_FSTYPES = {"tmpfs", "ramfs"}
active_specs = _get_active_specs()
mounted_filesystems = get_mount_info()
if mounted_filesystems is None:
sys.exit(1)
results = []
has_issues = False
has_write_errors = False
for var_name, is_persistent in PATHS_TO_CHECK.items():
result = analyze_path(
var_name, is_persistent,
mounted_filesystems, NON_PERSISTENT_FSTYPES, READ_ONLY_VARS
results_map: dict[str, MountCheckResult] = {}
for spec in active_specs:
results_map[spec.var_name] = analyze_path(
spec,
mounted_filesystems,
NON_PERSISTENT_FSTYPES,
)
if result.dataloss_risk or result.error or result.write_error or result.performance_issue:
has_issues = True
if result.write_error:
has_write_errors = True
results.append(result)
results = _apply_primary_rules(active_specs, results_map)
has_issues = any(
r.dataloss_risk or r.error or r.write_error or r.performance_issue
for r in results
)
has_write_errors = any(r.write_error for r in results)
if has_issues or True: # Always print table for diagnostic purposes
# --- Print Table ---
headers = ["Path", "Writeable", "Mount", "RAMDisk", "Performance", "DataLoss"]
CHECK_SYMBOL = ""
CROSS_SYMBOL = ""
BLANK_SYMBOL = ""
bool_to_check = lambda is_good: CHECK_SYMBOL if is_good else CROSS_SYMBOL
BLANK_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[0] = max(col_widths[0], len(str(r.path)))
header_fmt = (
f" {{:<{col_widths[0]}}} |"
@@ -186,7 +328,7 @@ def main():
f" {{:^{col_widths[4]}}} |"
f" {{:^{col_widths[5]}}} "
)
row_fmt = (
f" {{:<{col_widths[0]}}} |"
f" {{:^{col_widths[1]}}}|" # No space
@@ -195,59 +337,64 @@ def main():
f" {{:^{col_widths[4]}}}|" # No space
f" {{:^{col_widths[5]}}} " # DataLoss is last, needs space
)
separator = (
"-" * (col_widths[0] + 2) + "+" +
"-" * (col_widths[1] + 2) + "+" +
"-" * (col_widths[2] + 2) + "+" +
"-" * (col_widths[3] + 2) + "+" +
"-" * (col_widths[4] + 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)
)
])
print(header_fmt.format(*headers))
print(separator)
for r in results:
is_persistent = r.var_name in PERSISTENT_VARS
# --- Symbol Logic ---
# Symbol Logic
write_symbol = bool_to_check(r.is_writeable)
# Special case for read-only vars
if r.var_name in READ_ONLY_VARS:
write_symbol = CHECK_SYMBOL
mount_symbol = CHECK_SYMBOL if r.is_mounted else CROSS_SYMBOL
ramdisk_symbol = ""
if is_persistent:
ramdisk_symbol = CROSS_SYMBOL if r.underlying_fs_is_ramdisk else BLANK_SYMBOL
else:
ramdisk_symbol = CHECK_SYMBOL if r.is_ramdisk else CROSS_SYMBOL
if is_persistent:
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
else:
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)
print(row_fmt.format(
r.path,
write_symbol,
mount_symbol,
ramdisk_symbol,
perf_symbol,
dataloss_symbol
))
print(
row_fmt.format(
r.path,
write_symbol,
mount_symbol,
ramdisk_symbol,
perf_symbol,
dataloss_symbol,
)
)
# --- Print Warning ---
if has_issues:
print("\n", file=sys.stderr)
print_warning_message()
# Exit with error only if there are write permission issues
if has_write_errors and os.environ.get("NETALERTX_DEBUG") != "1":
sys.exit(1)
if __name__ == "__main__":
main()
main()

View File

@@ -3,6 +3,43 @@
# 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."
@@ -10,6 +47,7 @@ check_mandatory_folders() {
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
@@ -19,6 +57,7 @@ check_mandatory_folders() {
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
@@ -28,6 +67,7 @@ check_mandatory_folders() {
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

View File

@@ -57,7 +57,7 @@ EOF
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: Write permission denied.
⚠️ ATTENTION: Read permission denied (write permission denied).
The application cannot write to "${path}". This will prevent it from
saving data, logs, or configuration.

View File

@@ -3,6 +3,14 @@
# 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
# 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
exit 0
fi
# Get bounding capabilities from /proc/self/status (what can be acquired)
BND_HEX=$(grep '^CapBnd:' /proc/self/status 2>/dev/null | awk '{print $2}' | tr -d '\t')

View File

@@ -1,6 +1,13 @@
#!/bin/bash
# read-only-mode.sh detects and warns if running read-write on the root filesystem.
# This check is skipped in devcontainer mode as the devcontainer is not set up to run
# read-only and this would always trigger a warning. RW is required for development
# in the devcontainer.
if [ "${NETALERTX_DEBUG}" == "1" ]; then
exit 0
fi
# Check if the root filesystem is mounted as read-only
if ! awk '$2 == "/" && $4 ~ /ro/ {found=1} END {exit !found}' /proc/mounts; then
cat <<EOF

View File

@@ -37,8 +37,18 @@
#
################################################################################
# Allow direct command execution (e.g., `docker run -it netalertx bash`).
if [ "$#" -gt 0 ]; then
case "$1" in
bash|/bin/bash|sh|/bin/sh)
exec "$@"
;;
esac
fi
# Banner display
RED='\033[1;31m'
GREY='\033[90m'
RESET='\033[0m'
printf "${RED}"
echo '
@@ -64,25 +74,50 @@ for script in ${ENTRYPOINT_CHECKS}/*; do
echo "Skipping startup checks as SKIP_TESTS is set."
break
fi
script_name=$(basename "$script" | sed 's/^[0-9]*-//;s/\.sh$//;s/-/ /g')
echo " --> ${script_name}"
script_name=$(basename "$script" | sed 's/^[0-9]*-//;s/\.(sh|py)$//;s/-/ /g')
echo "--> ${script_name} "
if [ -n "${SKIP_STARTUP_CHECKS:-}" ] && echo "${SKIP_STARTUP_CHECKS}" | grep -q "\b${script_name}\b"; then
printf "${GREY}skip${RESET}\n"
continue
fi
"$script"
NETALERTX_DOCKER_ERROR_CHECK=$?
if [ ${NETALERTX_DOCKER_ERROR_CHECK} -ne 0 ]; then
if [ ${NETALERTX_DOCKER_ERROR_CHECK} -eq 1 ]; then
>&2 printf "%s" "${RED}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
❌ NetAlertX startup aborted: critical failure in ${script_name}.
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/troubleshooting.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
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
FAILED_STATUS="${NETALERTX_DOCKER_ERROR_CHECK}"
echo "${script_name}: FAILED with ${FAILED_STATUS}"
echo "Failure detected in: ${script}"
# Continue to next check instead of exiting immediately
fi
fi
done
if [ -n "${FAILED_STATUS}" ]; then
echo "Container startup checks failed with exit code ${FAILED_STATUS}."
# Continue with startup despite failures for testing purposes
if [ "${NETALERTX_DEBUG:-0}" -eq 1 ]; then
echo "NETALERTX_DEBUG=1, continuing despite failed pre-checks."
else
exit "${FAILED_STATUS}"
fi
fi
# Set APP_CONF_OVERRIDE based on GRAPHQL_PORT if not already set

View File

@@ -1,6 +1,6 @@
Nginx's conf is in /services/config/nginx/conf.active. This is the default configuration when run as a read-only container without a mount.
Nginx's active configuration lives in /tmp/nginx/active-config by default when the container runs read-only without a bind mount.
With a tmpfs mount on /services/config/nginx/conf.active, the nginx template will be rewritten to allow ENV customization of listen address and port.
Mounting a writable directory at /tmp/nginx/active-config allows the entrypoint to rewrite the nginx template so LISTEN_ADDR and PORT environment overrides take effect.
The act of running /services/start-nginx.sh writes a new nginx.conf file, using envsubst, then starts nginx based on the parameters in that file.

View File

@@ -0,0 +1 @@
/tmp/nginx/active-config

View File

@@ -5,7 +5,9 @@ worker_processes auto;
pcre_jit on;
# Configures default error logger.
error_log /app/log/nginx-error.log warn;
error_log /tmp/log/nginx-error.log warn;
pid /tmp/run/nginx.pid;
events {
# The maximum number of simultaneous connections that can be opened by
@@ -16,11 +18,11 @@ events {
http {
# Mapping of temp paths for various nginx modules.
client_body_temp_path /services/run/tmp/client_body;
proxy_temp_path /services/run/tmp/proxy;
fastcgi_temp_path /services/run/tmp/fastcgi;
uwsgi_temp_path /services/run/tmp/uwsgi;
scgi_temp_path /services/run/tmp/scgi;
client_body_temp_path /tmp/nginx/client_body;
proxy_temp_path /tmp/nginx/proxy;
fastcgi_temp_path /tmp/nginx/fastcgi;
uwsgi_temp_path /tmp/nginx/uwsgi;
scgi_temp_path /tmp/nginx/scgi;
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
@@ -86,7 +88,7 @@ http {
'"$http_user_agent" "$http_x_forwarded_for"';
# Sets the path, format, and configuration for a buffered log write.
access_log /app/log/nginx-access.log main;
access_log /tmp/log/nginx-access.log main;
# Virtual host config
@@ -101,7 +103,7 @@ http {
location ~* \.php$ {
# Set Cache-Control header to prevent caching on the first load
add_header Cache-Control "no-store";
fastcgi_pass unix:/services/run/php.sock;
fastcgi_pass unix:/tmp/run/php.sock;
include /services/config/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;

View File

@@ -7,6 +7,6 @@
;
[global]
pid = /services/run/php8.3-fpm.pid
error_log = /app/log/app.php_errors.log
pid = /tmp/run/php8.3-fpm.pid
error_log = /tmp/log/app.php_errors.log
include=/services/config/php/php-fpm.d/*.conf

View File

@@ -43,7 +43,7 @@
; (IPv6 and IPv4-mapped) on a specific port;
; '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = /services/run/php.sock
listen = /tmp/run/php.sock
; Set listen(2) backlog.
; Default Value: 511 (-1 on Linux, FreeBSD and OpenBSD)
@@ -465,9 +465,9 @@ pm.max_spare_servers = 3
; Default Value: clean env
;env[HOSTNAME] = $HOSTNAME
env[PATH] = /opt/venv:/usr/local/bin:/usr/bin:/bin
env[TMP] = /services/run/tmp
env[TMPDIR] = /services/run/tmp
env[TEMP] = /services/run/tmp
env[TMP] = /tmp/run/tmp
env[TMPDIR] = /tmp/run/tmp
env[TEMP] = /tmp/run/tmp
; Additional php.ini defines, specific to this pool of workers. These settings
; overwrite the values previously defined in the php.ini. The directives are the
@@ -489,9 +489,9 @@ env[TEMP] = /services/run/tmp
; Default Value: nothing is defined by default except the values in php.ini and
; specified at startup with the -d argument
;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
php_admin_value[sys_temp_dir] = /services/run/tmp
php_admin_value[upload_tmp_dir] = /services/run/tmp
php_admin_value[session.save_path] = /services/run/tmp
php_admin_value[sys_temp_dir] = /tmp/run/tmp
php_admin_value[upload_tmp_dir] = /tmp/run/tmp
php_admin_value[session.save_path] = /tmp/run/tmp
php_admin_value[output_buffering] = 262144
php_admin_flag[implicit_flush] = off
php_admin_value[realpath_cache_size] = 4096K

View File

@@ -15,8 +15,8 @@ set -euo pipefail
# /usr/share/arp-scan
# ----------------------------------------------------------------------
TEMP_FILE="/services/run/tmp/ieee-oui.txt.tmp"
OUTPUT_FILE="/services/run/tmp/ieee-oui.txt"
TEMP_FILE="${SYSTEM_SERVICES_RUN_TMP}/ieee-oui.txt.tmp"
OUTPUT_FILE="${SYSTEM_SERVICES_RUN_TMP}/ieee-oui.txt"
# Download the file using wget to stdout and process it
if ! wget --timeout=30 --tries=3 "https://standards-oui.ieee.org/oui/oui.txt" -O /dev/stdout 2>/dev/null | \

View File

@@ -11,5 +11,5 @@ done
# Force kill if graceful shutdown failed
killall -KILL python3 &>/dev/null
echo "Starting python3 $(cat /services/config/python/backend-extra-launch-parameters 2>/dev/null) -m server > /app/log/stdout.log 2> >(tee /app/log/stderr.log >&2)"
exec python3 $(cat /services/config/python/backend-extra-launch-parameters 2>/dev/null) -m server > /app/log/stdout.log 2> >(tee /app/log/stderr.log >&2)
echo "Starting python3 $(cat /services/config/python/backend-extra-launch-parameters 2>/dev/null) -m server > ${NETALERTX_LOG}/stdout.log 2> >(tee ${NETALERTX_LOG}/stderr.log >&2)"
exec python3 $(cat /services/config/python/backend-extra-launch-parameters 2>/dev/null) -m server > ${NETALERTX_LOG}/stdout.log 2> >(tee ${NETALERTX_LOG}/stderr.log >&2)

View File

@@ -4,14 +4,13 @@ set -euo pipefail
LOG_DIR=${NETALERTX_LOG}
RUN_DIR=${SYSTEM_SERVICES_RUN}
TMP_DIR=${SYSTEM_SERVICES_RUN_TMP}
TMP_DIR=/tmp/nginx
SYSTEM_NGINX_CONFIG_TEMPLATE="/services/config/nginx/netalertx.conf.template"
SYSTEM_NGINX_CONFIG_FILE="/services/config/nginx/conf.active/netalertx.conf"
# Create directories if they don't exist
mkdir -p "${LOG_DIR}" "${RUN_DIR}" "${TMP_DIR}"
nginx_pid=""
cleanup() {
@@ -43,15 +42,18 @@ fi
trap cleanup EXIT
trap forward_signal INT TERM
# Ensure temp dirs have correct permissions
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_NGINX_CONFIG_FILE}\" -g \"error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; pid ${RUN_DIR}/nginx.pid; daemon off;\" &"
echo "Starting /usr/sbin/nginx -p \"${RUN_DIR}/\" -c \"${SYSTEM_NGINX_CONFIG_FILE}\" -g \"error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;\" &"
/usr/sbin/nginx \
-p "${RUN_DIR}/" \
-c "${SYSTEM_NGINX_CONFIG_FILE}" \
-g "error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; pid ${RUN_DIR}/nginx.pid; daemon off;" &
-g "error_log /dev/stderr; error_log ${NETALERTX_LOG}/nginx-error.log; daemon off;" &
nginx_pid=$!
wait "${nginx_pid}"

View File

@@ -5,7 +5,7 @@ worker_processes auto;
pcre_jit on;
# Configures default error logger.
error_log /app/log/nginx-error.log warn;
error_log /tmp/log/nginx-error.log warn;
events {
# The maximum number of simultaneous connections that can be opened by
@@ -16,11 +16,11 @@ events {
http {
# Mapping of temp paths for various nginx modules.
client_body_temp_path /services/run/tmp/client_body;
proxy_temp_path /services/run/tmp/proxy;
fastcgi_temp_path /services/run/tmp/fastcgi;
uwsgi_temp_path /services/run/tmp/uwsgi;
scgi_temp_path /services/run/tmp/scgi;
client_body_temp_path /tmp/run/tmp/client_body;
proxy_temp_path /tmp/run/tmp/proxy;
fastcgi_temp_path /tmp/run/tmp/fastcgi;
uwsgi_temp_path /tmp/run/tmp/uwsgi;
scgi_temp_path /tmp/run/tmp/scgi;
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
@@ -93,7 +93,7 @@ http {
'"$http_user_agent" "$http_x_forwarded_for"';
# Sets the path, format, and configuration for a buffered log write.
access_log /app/log/nginx-access.log main;
access_log /tmp/log/nginx-access.log main;
# Virtual host config
@@ -109,7 +109,7 @@ http {
try_files $uri =404;
# Set Cache-Control header to prevent caching on the first load
add_header Cache-Control "no-store";
fastcgi_pass unix:/services/run/php.sock;
fastcgi_pass unix:/tmp/run/php.sock;
include /services/config/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;