From e17f355fbc027a30099e986a8a1d1672dcb1ee3f Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Fri, 19 Dec 2025 01:27:17 +0000 Subject: [PATCH] Fix existing unit tests and docs --- Dockerfile | 10 +- docs/DOCKER_COMPOSE.md | 15 +- docs/DOCKER_INSTALLATION.md | 7 +- docs/docker-troubleshooting/incorrect-user.md | 25 +- .../entrypoint.d/0-storage-permission.sh | 0 ...data-migration.sh => 05-data-migration.sh} | 0 ...-override.sh => 30-apply-conf-override.sh} | 0 ...itable-config.sh => 35-writable-config.sh} | 15 + ...{35-nginx-config.sh => 40-nginx-config.sh} | 0 .../entrypoint.d/99-ports-available.sh | 54 +-- .../test_container_environment.py | 384 ++++++++++++------ 11 files changed, 328 insertions(+), 182 deletions(-) mode change 100644 => 100755 install/production-filesystem/entrypoint.d/0-storage-permission.sh rename install/production-filesystem/entrypoint.d/{01-data-migration.sh => 05-data-migration.sh} (100%) rename install/production-filesystem/entrypoint.d/{31-apply-conf-override.sh => 30-apply-conf-override.sh} (100%) rename install/production-filesystem/entrypoint.d/{30-writable-config.sh => 35-writable-config.sh} (77%) rename install/production-filesystem/entrypoint.d/{35-nginx-config.sh => 40-nginx-config.sh} (100%) diff --git a/Dockerfile b/Dockerfile index fb562b23..1648bcd5 100755 --- a/Dockerfile +++ b/Dockerfile @@ -53,9 +53,9 @@ ARG INSTALL_DIR=/app # Runtime service account (override at build; container user can still be overridden at run time) ARG NETALERTX_UID=20211 ARG NETALERTX_GID=20211 -# Read-only lock owner (kept at 20211 by default for immutability) -ARG READONLY_UID=20211 -ARG READONLY_GID=20211 +# Read-only lock owner (separate from service account to avoid UID/GID collisions) +ARG READONLY_UID=20212 +ARG READONLY_GID=20212 # NetAlertX app directories ENV NETALERTX_APP=${INSTALL_DIR} @@ -196,8 +196,8 @@ FROM runner AS hardened # Re-declare UID/GID args for this stage ARG NETALERTX_UID=20211 ARG NETALERTX_GID=20211 -ARG READONLY_UID=20211 -ARG READONLY_GID=20211 +ARG READONLY_UID=20212 +ARG READONLY_GID=20212 ENV UMASK=0077 diff --git a/docs/DOCKER_COMPOSE.md b/docs/DOCKER_COMPOSE.md index cc337dc6..375cf5ad 100755 --- a/docs/DOCKER_COMPOSE.md +++ b/docs/DOCKER_COMPOSE.md @@ -51,18 +51,18 @@ services: # - path/on/host/to/dhcp.file:/resources/dhcp.file # tmpfs mount consolidates writable state for a read-only container and improves performance - # uid=20211 and gid=20211 is the netalertx user inside the container - # mode=1700 grants rwx------ permissions to the netalertx user only + # uid/gid default to the service user (NETALERTX_UID/GID, default 20211) + # mode=1700 grants rwx------ permissions to the runtime user only tmpfs: # Comment out to retain logs between container restarts - this has a server performance impact. - - "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + - "/tmp:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" # Retain logs - comment out tmpfs /tmp if you want to retain logs between container restarts # Please note if you remove the /tmp mount, you must create and maintain sub-folder mounts. # - /path/on/host/log:/tmp/log - # - "/tmp/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" - # - "/tmp/nginx:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" - # - "/tmp/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # - "/tmp/api:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # - "/tmp/nginx:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" + # - "/tmp/run:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime" environment: LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces @@ -94,6 +94,9 @@ Run or re-run it: docker compose up --force-recreate ``` +> [!TIP] +> Runtime UID/GID: The image ships with a service user `netalertx` (UID/GID 20211) and a readonly lock owner also at 20211 for 004/005 immutability. If you override the runtime user (compose `user:` or `NETALERTX_UID/GID` vars), ensure your `/data` volume and tmpfs mounts use matching `uid/gid` so startup checks and writable paths succeed. + ### Customize with Environmental Variables You can override the default settings by passing environmental variables to the `docker compose up` command. diff --git a/docs/DOCKER_INSTALLATION.md b/docs/DOCKER_INSTALLATION.md index daafe5ad..905e922d 100644 --- a/docs/DOCKER_INSTALLATION.md +++ b/docs/DOCKER_INSTALLATION.md @@ -27,12 +27,14 @@ Head to [https://netalertx.com/](https://netalertx.com/) for more gifs and scree docker run -d --rm --network=host \ -v /local_data_dir:/data \ -v /etc/localtime:/etc/localtime \ - --tmpfs /tmp:uid=20211,gid=20211,mode=1700 \ + --tmpfs /tmp:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700 \ -e PORT=20211 \ -e APP_CONF_OVERRIDE={"GRAPHQL_PORT":"20214"} \ ghcr.io/jokob-sk/netalertx:latest ``` +> Runtime UID/GID: The image defaults to a service user `netalertx` (UID/GID 20211). A separate readonly lock owner also uses UID/GID 20211 for 004/005 immutability. You can override the runtime UID/GID at build (ARG) or run (`--user` / compose `user:`) but must align writable mounts (`/data`, `/tmp*`) and tmpfs `uid/gid` to that choice. + See alternative [docked-compose examples](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md). ### Default ports @@ -83,7 +85,8 @@ data If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the `/local_data_dir/db` and `/local_data_dir/config` folders (replace `local_data_dir` with the location where your `/db` and `/config` folders are located). ```bash -sudo chown -R 20211:20211 /local_data_dir +# Use the runtime UID/GID you intend to run with (default 20211:20211) +sudo chown -R ${NETALERTX_UID:-20211}:${NETALERTX_GID:-20211} /local_data_dir sudo chmod -R a+rwx /local_data_dir ``` diff --git a/docs/docker-troubleshooting/incorrect-user.md b/docs/docker-troubleshooting/incorrect-user.md index 99af8e78..56d8349c 100644 --- a/docs/docker-troubleshooting/incorrect-user.md +++ b/docs/docker-troubleshooting/incorrect-user.md @@ -2,27 +2,30 @@ ## Issue Description -NetAlertX is running as UID:GID other than the expected 20211:20211. This bypasses hardened permissions, file ownership, and runtime isolation safeguards. +NetAlertX is running as a UID:GID that does not match the runtime service user configured for this container (default 20211:20211). Hardened ownership on writable paths may block writes if the UID/GID do not align with mounted volumes and tmpfs settings. ## Security Ramifications -The application is designed with security hardening that depends on running under a dedicated, non-privileged service account. Using a different user account can silently fail future upgrades and removes crucial isolation between the container and host system. +The image uses a dedicated service user for writes and a readonly lock owner (UID 20211) for code/venv with 004/005 permissions. Running as an arbitrary UID is supported, but only when writable mounts (`/data`, `/tmp/*`) are owned by that UID. Misalignment can cause startup failures or unexpected permission escalation attempts. ## Why You're Seeing This Issue -This occurs when you override the container's default user with custom `user:` directives in docker-compose.yml or `--user` flags in docker run commands. The container expects to run as the netalertx user for proper security isolation. +- A `user:` override in docker-compose.yml or `--user` flag on `docker run` changes the runtime UID/GID without updating mount ownership. +- Tmpfs mounts still use `uid=20211,gid=20211` while the container runs as another UID. +- Host bind mounts (e.g., `/data`) are owned by a different UID. ## How to Correct the Issue -Restore the container to the default user: +Option A: Use defaults (recommended) +- Remove custom `user:` overrides and `--user` flags. +- Let the container run as the built-in service user (UID/GID 20211) and keep tmpfs at `uid=20211,gid=20211`. -- Remove any `user:` overrides from docker-compose.yml -- Avoid `--user` flags in docker run commands -- Allow the container to run with its default UID:GID 20211:20211 -- Recreate the container so volume ownership is reset automatically +Option B: Run with a custom UID/GID +- Set `user:` (or `NETALERTX_UID/NETALERTX_GID`) to your desired UID/GID. +- Align mounts: ensure `/data` (and any `/tmp/*` tmpfs) use the same `uid=`/`gid=` and that host bind mounts are chowned to that UID/GID. +- Recreate the container so ownership is consistent. ## Additional Resources -Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally. - -For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md) \ No newline at end of file +- Default compose and tmpfs guidance: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md) +- General Docker install and runtime notes: [DOCKER_INSTALLATION.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_INSTALLATION.md) \ No newline at end of file diff --git a/install/production-filesystem/entrypoint.d/0-storage-permission.sh b/install/production-filesystem/entrypoint.d/0-storage-permission.sh old mode 100644 new mode 100755 diff --git a/install/production-filesystem/entrypoint.d/01-data-migration.sh b/install/production-filesystem/entrypoint.d/05-data-migration.sh similarity index 100% rename from install/production-filesystem/entrypoint.d/01-data-migration.sh rename to install/production-filesystem/entrypoint.d/05-data-migration.sh diff --git a/install/production-filesystem/entrypoint.d/31-apply-conf-override.sh b/install/production-filesystem/entrypoint.d/30-apply-conf-override.sh similarity index 100% rename from install/production-filesystem/entrypoint.d/31-apply-conf-override.sh rename to install/production-filesystem/entrypoint.d/30-apply-conf-override.sh diff --git a/install/production-filesystem/entrypoint.d/30-writable-config.sh b/install/production-filesystem/entrypoint.d/35-writable-config.sh similarity index 77% rename from install/production-filesystem/entrypoint.d/30-writable-config.sh rename to install/production-filesystem/entrypoint.d/35-writable-config.sh index 74d0df1e..56a5f1d3 100755 --- a/install/production-filesystem/entrypoint.d/30-writable-config.sh +++ b/install/production-filesystem/entrypoint.d/35-writable-config.sh @@ -36,6 +36,21 @@ for path in $READ_WRITE_PATHS; do https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/file-permissions.md ══════════════════════════════════════════════════════════════════════════════ +EOF + >&2 printf "%s" "${RESET}" + elif [ ! -f "$path" ]; then + failures=1 + >&2 printf "%s" "${YELLOW}" + >&2 cat </dev/null || echo unknown)). + This prevents NetAlertX from reading the configuration and indicates a + permissions or mount issue — often seen when running with custom UID/GID. + + https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/file-permissions.md +══════════════════════════════════════════════════════════════════════════════ EOF >&2 printf "%s" "${RESET}" elif [ ! -r "$path" ]; then diff --git a/install/production-filesystem/entrypoint.d/35-nginx-config.sh b/install/production-filesystem/entrypoint.d/40-nginx-config.sh similarity index 100% rename from install/production-filesystem/entrypoint.d/35-nginx-config.sh rename to install/production-filesystem/entrypoint.d/40-nginx-config.sh diff --git a/install/production-filesystem/entrypoint.d/99-ports-available.sh b/install/production-filesystem/entrypoint.d/99-ports-available.sh index d18aa4fd..7c069b22 100755 --- a/install/production-filesystem/entrypoint.d/99-ports-available.sh +++ b/install/production-filesystem/entrypoint.d/99-ports-available.sh @@ -5,22 +5,27 @@ # Define ports from ENV variables, applying defaults PORT_APP=${PORT:-20211} -# PORT_GQL=${APP_CONF_OVERRIDE:-${GRAPHQL_PORT:-20212}} -# # Check if ports are configured to be the same -# if [ "$PORT_APP" -eq "$PORT_GQL" ]; then -# cat </dev/null 2>&1; then @@ -53,17 +58,16 @@ if echo "$LISTENING_PORTS" | grep -q ":${PORT_APP}$"; then EOF fi -# # Check GraphQL Port -# # We add a check to avoid double-warning if ports are identical AND in use -# if [ "$PORT_APP" -ne "$PORT_GQL" ] && echo "$LISTENING_PORTS" | grep -q ":${PORT_GQL}$"; then -# cat < str: def _remove_docker_volume(name: str) -> None: subprocess.run( - ["docker", "volume", "rm", "-f", name], + [ + "docker", + "volume", + "rm", + "-f", + name, + ], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) +def _seed_config_volume() -> str: + name = _create_docker_volume("config") + cmd = [ + "docker", + "run", + "--rm", + "--user", + "0:0", + "--entrypoint", + "/bin/sh", + "-v", + f"{name}:/data", + IMAGE, + "-c", + "install -d /data && install -m 600 /app/back/app.conf /data/app.conf && chown 20211:20211 /data/app.conf", + ] + try: + subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + _remove_docker_volume(name) + raise + return name + + +def _seed_data_volume() -> str: + name = _create_docker_volume("data") + cmd = [ + "docker", + "run", + "--rm", + "--user", + "0:0", + "--entrypoint", + "/bin/sh", + "-v", + f"{name}:/data", + IMAGE, + "-c", + "install -d /data/db /data/config && chown -R 20211:20211 /data", + ] + try: + subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + _remove_docker_volume(name) + raise + return name + + def _chown_path(host_path: pathlib.Path, uid: int, gid: int) -> None: """Chown a host path using the test image with host user namespace.""" if not host_path.exists(): @@ -272,6 +326,7 @@ def _run_container( volume_specs: list[str] | None = None, sleep_seconds: float = GRACE_SECONDS, wait_for_exit: bool = False, + pre_entrypoint: str | None = None, ) -> subprocess.CompletedProcess[str]: name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower() @@ -323,11 +378,18 @@ def _run_container( mounts_ls += f" {target}" mounts_ls += " || true; echo '--- END MOUNTS ---'; \n" + setup_script = "" + if pre_entrypoint: + setup_script = pre_entrypoint + if not setup_script.endswith("\n"): + setup_script += "\n" + if wait_for_exit: - script = mounts_ls + "sh /entrypoint.sh" + script = mounts_ls + setup_script + "sh /entrypoint.sh" else: script = "".join([ mounts_ls, + setup_script, "sh /entrypoint.sh & pid=$!; ", f"sleep {sleep_seconds}; ", "if kill -0 $pid >/dev/null 2>&1; then kill -TERM $pid >/dev/null 2>&1 || true; fi; ", @@ -509,23 +571,24 @@ def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None: Check script: check-network-mode.sh Sample message: "⚠️ ATTENTION: NetAlertX is not running with --network=host. Bridge networking..." """ - base = tmp_path / "missing_host_net_base" - paths = _setup_fixed_mount_tree(base) - # Ensure directories are writable and owned by netalertx user so container can operate - for key in ["data", "app_db", "app_config"]: - paths[key].chmod(0o777) - _chown_netalertx(paths[key]) - # Create a config file so the writable check passes - config_file = paths["app_config"] / "app.conf" - config_file.write_text("test config") - config_file.chmod(0o666) - _chown_netalertx(config_file) - volumes = _build_volume_args_for_keys(paths, {"data"}) - result = _run_container( - "missing-host-network", - volumes, - network_mode=None, - ) + data_volume = _seed_data_volume() + config_volume = _seed_config_volume() + result = None + try: + result = _run_container( + "missing-host-network", + None, + network_mode=None, + volume_specs=[ + f"{data_volume}:{VOLUME_MAP['data']}", + f"{config_volume}:{VOLUME_MAP['app_config']}", + ], + sleep_seconds=5, + ) + finally: + _remove_docker_volume(config_volume) + _remove_docker_volume(data_volume) + assert result is not None _assert_contains(result, "not running with --network=host", result.args) @@ -536,146 +599,200 @@ def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None: # top level. -if False: # pragma: no cover - placeholder until writable /data fixtures exist for these flows - def test_running_as_uid_1000_warns(tmp_path: pathlib.Path) -> None: - # No output assertion, just returncode check - """Test running as wrong user - simulates using arbitrary user instead of netalertx. - 7. Running as Wrong User: Simulates running as arbitrary user (UID 1000) instead - of netalertx user. Permission errors due to incorrect user context. - Expected: Permission errors, guidance to use correct user. +def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None: + """Test missing configuration file seeding - simulates corrupted/missing app.conf. - Check script: /entrypoint.d/60-user-netalertx.sh - Sample message: "⚠️ ATTENTION: NetAlertX is running as UID 1000:1000. Hardened permissions..." - """ - paths = _setup_mount_tree(tmp_path, "run_as_1000") - volumes = _build_volume_args_for_keys(paths, {"data"}) - result = _run_container( - "run-as-1000", - volumes, - user="1000:1000", - ) - _assert_contains(result, "NetAlertX is running as UID 1000:1000", result.args) + 9. Missing Configuration File: Simulates corrupted/missing app.conf. + Container automatically regenerates default configuration on startup. + Expected: Automatic regeneration of default configuration. - def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None: - """Test missing configuration file seeding - simulates corrupted/missing app.conf. + Check script: /entrypoint.d/15-first-run-config.sh + Sample message: "Default configuration written to" + """ + base = tmp_path / "missing_app_conf_base" + paths = _setup_fixed_mount_tree(base) + for key in ["data", "app_db", "app_config"]: + paths[key].chmod(0o777) + _chown_netalertx(paths[key]) + (paths["app_config"] / "testfile.txt").write_text("test") + volumes = _build_volume_args_for_keys(paths, {"data"}) + result = _run_container("missing-app-conf", volumes, sleep_seconds=5) + _assert_contains(result, "Default configuration written to", result.args) + assert result.returncode == 0 - 9. Missing Configuration File: Simulates corrupted/missing app.conf. - Container automatically regenerates default configuration on startup. - Expected: Automatic regeneration of default configuration. - Check script: /entrypoint.d/15-first-run-config.sh - Sample message: "Default configuration written to" - """ - base = tmp_path / "missing_app_conf_base" - paths = _setup_fixed_mount_tree(base) - for key in ["data", "app_db", "app_config"]: - paths[key].chmod(0o777) - _chown_netalertx(paths[key]) - (paths["app_config"] / "testfile.txt").write_text("test") - volumes = _build_volume_args_for_keys(paths, {"data"}) - result = _run_container("missing-app-conf", volumes, sleep_seconds=5) - _assert_contains(result, "Default configuration written to", result.args) - assert result.returncode == 0 +def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None: + """Test missing database file seeding - simulates corrupted/missing app.db. - def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None: - """Test missing database file seeding - simulates corrupted/missing app.db. + 10. Missing Database File: Simulates corrupted/missing app.db. + Container automatically creates initial database schema on startup. + Expected: Automatic creation of initial database schema. - 10. Missing Database File: Simulates corrupted/missing app.db. - Container automatically creates initial database schema on startup. - Expected: Automatic creation of initial database schema. - - Check script: /entrypoint.d/20-first-run-db.sh - Sample message: "Building initial database schema" - """ - base = tmp_path / "missing_app_db_base" - paths = _setup_fixed_mount_tree(base) - _chown_netalertx(paths["app_db"]) - (paths["app_db"] / "testfile.txt").write_text("test") - volumes = _build_volume_args_for_keys(paths, {"data"}) + Check script: /entrypoint.d/20-first-run-db.sh + Sample message: "Building initial database schema" + """ + data_volume = _seed_data_volume() + config_volume = _seed_config_volume() + # Prepare a minimal writable config that still contains TIMEZONE for plugin_helper + subprocess.run( + [ + "docker", + "run", + "--rm", + "--entrypoint", + "/bin/sh", + "-v", + f"{config_volume}:/data", + IMAGE, + "-c", + "printf \"TIMEZONE='UTC'\\n\" >/data/app.conf && chown 20211:20211 /data/app.conf", + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + result = None + db_file_check = None + try: result = _run_container( "missing-app-db", - volumes, + None, user="20211:20211", sleep_seconds=5, wait_for_exit=True, + volume_specs=[ + f"{data_volume}:{VOLUME_MAP['data']}", + f"{config_volume}:{VOLUME_MAP['app_config']}", + ], + ) + finally: + db_file_check = subprocess.run( + [ + "docker", + "run", + "--rm", + "-v", + f"{data_volume}:/data", + IMAGE, + "/bin/sh", + "-c", + "test -f /data/db/app.db", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + _remove_docker_volume(config_volume) + _remove_docker_volume(data_volume) + assert db_file_check is not None and db_file_check.returncode == 0 + assert result is not None + assert result.returncode != 0 + + +def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None: + """Test custom port configuration without writable nginx config mount. + + 4. Custom Port Without Nginx Config Mount: Simulates setting custom LISTEN_ADDR/PORT + without mounting nginx config. Container starts but uses default address. + Expected: Container starts but uses default address, warning about missing config mount. + + Check script: check-nginx-config.sh + Sample messages: "⚠️ ATTENTION: Nginx configuration mount /tmp/nginx/active-config is missing." + "⚠️ ATTENTION: Unable to write to /tmp/nginx/active-config/netalertx.conf." + """ + data_volume = _seed_data_volume() + config_volume = _seed_config_volume() + paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf") + for key in ["data", "app_db", "app_config", "app_log", "app_api", "services_run"]: + paths[key].chmod(0o777) + _chown_netalertx(paths[key]) + volumes = _build_volume_args_for_keys( + paths, + {"app_log", "app_api", "services_run"}, + ) + extra_args = [ + "--tmpfs", + f"{VOLUME_MAP['nginx_conf']}:uid=20211,gid=20211,mode=500", + ] + try: + result = _run_container( + "custom-port-ro-conf", + volumes, + env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"}, + user="20211:20211", + extra_args=extra_args, + volume_specs=[ + f"{data_volume}:{VOLUME_MAP['data']}", + f"{config_volume}:{VOLUME_MAP['app_config']}", + ], + sleep_seconds=5, + ) + _assert_contains(result, "Unable to write to", result.args) + _assert_contains( + result, f"{VOLUME_MAP['nginx_conf']}/netalertx.conf", result.args ) - _assert_contains(result, "Building initial database schema", result.args) assert result.returncode != 0 + finally: + _remove_docker_volume(config_volume) + _remove_docker_volume(data_volume) - def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None: - """Test custom port configuration without writable nginx config mount. +def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None: + """Test excessive capabilities detection - simulates container with extra capabilities. - 4. Custom Port Without Nginx Config Mount: Simulates setting custom LISTEN_ADDR/PORT - without mounting nginx config. Container starts but uses default address. - Expected: Container starts but uses default address, warning about missing config mount. + 11. Excessive Capabilities: Simulates container with capabilities beyond the required + NET_ADMIN, NET_RAW, and NET_BIND_SERVICE. + Expected: Warning about excessive capabilities detected. - Check script: check-nginx-config.sh - Sample messages: "⚠️ ATTENTION: Nginx configuration mount /tmp/nginx/active-config is missing." - "⚠️ ATTENTION: Unable to write to /tmp/nginx/active-config/netalertx.conf." - """ - paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf") - for key in ["app_db", "app_config", "app_log", "app_api", "services_run"]: - paths[key].chmod(0o777) - paths["nginx_conf"].chmod(0o500) - volumes = _build_volume_args_for_keys( - paths, - {"data", "app_log", "app_api", "services_run", "nginx_conf"}, - ) - try: - result = _run_container( - "custom-port-ro-conf", - volumes, - env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"}, - user="20211:20211", - sleep_seconds=5, - ) - _assert_contains(result, "Unable to write to", result.args) - _assert_contains( - result, f"{VOLUME_MAP['nginx_conf']}/netalertx.conf", result.args - ) - assert result.returncode != 0 - finally: - paths["nginx_conf"].chmod(0o755) - - def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None: - """Test excessive capabilities detection - simulates container with extra capabilities. - - 11. Excessive Capabilities: Simulates container with capabilities beyond the required - NET_ADMIN, NET_RAW, and NET_BIND_SERVICE. - Expected: Warning about excessive capabilities detected. - - Check script: 90-excessive-capabilities.sh - Sample message: "Excessive capabilities detected" - """ - paths = _setup_mount_tree(tmp_path, "excessive_caps") - volumes = _build_volume_args_for_keys(paths, {"data"}) + Check script: 90-excessive-capabilities.sh + Sample message: "Excessive capabilities detected" + """ + data_volume = _seed_data_volume() + config_volume = _seed_config_volume() + try: result = _run_container( "excessive-caps", - volumes, + None, extra_args=["--cap-add=SYS_ADMIN", "--cap-add=NET_BROADCAST"], sleep_seconds=5, + volume_specs=[ + f"{data_volume}:{VOLUME_MAP['data']}", + f"{config_volume}:{VOLUME_MAP['app_config']}", + ], ) _assert_contains(result, "Excessive capabilities detected", result.args) _assert_contains(result, "bounding caps:", result.args) + finally: + _remove_docker_volume(config_volume) + _remove_docker_volume(data_volume) - def test_appliance_integrity_read_write_mode(tmp_path: pathlib.Path) -> None: - """Test appliance integrity - simulates running with read-write root filesystem. +def test_appliance_integrity_read_write_mode(tmp_path: pathlib.Path) -> None: + """Test appliance integrity - simulates running with read-write root filesystem. - 12. Appliance Integrity: Simulates running container with read-write root filesystem - instead of read-only mode. - Expected: Warning about running in read-write mode instead of read-only. + 12. Appliance Integrity: Simulates running container with read-write root filesystem + instead of read-only mode. + Expected: Warning about running in read-write mode instead of read-only. - Check script: 95-appliance-integrity.sh - Sample message: "Container is running as read-write, not in read-only mode" - """ - paths = _setup_mount_tree(tmp_path, "appliance_integrity") - volumes = _build_volume_args_for_keys(paths, {"data"}) - result = _run_container("appliance-integrity", volumes, sleep_seconds=5) - _assert_contains( - result, "Container is running as read-write, not in read-only mode", result.args + Check script: 95-appliance-integrity.sh + Sample message: "Container is running as read-write, not in read-only mode" + """ + data_volume = _seed_data_volume() + config_volume = _seed_config_volume() + try: + result = _run_container( + "appliance-integrity", + None, + sleep_seconds=5, + volume_specs=[ + f"{data_volume}:{VOLUME_MAP['data']}", + f"{config_volume}:{VOLUME_MAP['app_config']}", + ], ) - _assert_contains(result, "read-only: true", result.args) + finally: + _remove_docker_volume(config_volume) + _remove_docker_volume(data_volume) + _assert_contains( + result, "Container is running as read-write, not in read-only mode", result.args + ) def test_zero_permissions_app_db_dir(tmp_path: pathlib.Path) -> None: @@ -779,9 +896,10 @@ def test_writable_config_validation(tmp_path: pathlib.Path) -> None: Sample message: "Read permission denied" """ paths = _setup_mount_tree(tmp_path, "writable_config") - # Make config file read-only but keep directories writable so container gets past mounts.py + # Make config file unreadable/unwritable to the container user to force the check config_file = paths["app_config"] / "app.conf" - config_file.chmod(0o400) # Read-only for owner + _chown_root(config_file) + config_file.chmod(0o000) # Ensure directories are writable and owned by netalertx user so container gets past mounts.py for key in [