adjust tests and allow other users

This commit is contained in:
Adam Outler
2025-12-19 04:26:16 +00:00
parent e17f355fbc
commit 0889741864
2 changed files with 72 additions and 284 deletions
@@ -18,26 +18,6 @@ fi
if [ "${CURRENT_UID}" -eq "${EXPECTED_UID}" ] && [ "${CURRENT_GID}" -eq "${EXPECTED_GID}" ]; then if [ "${CURRENT_UID}" -eq "${EXPECTED_UID}" ] && [ "${CURRENT_GID}" -eq "${EXPECTED_GID}" ]; then
exit 0 exit 0
fi fi
>&2 printf '\nNetAlertX note: current UID %s GID %s, expected UID %s GID %s\n' \
YELLOW=$(printf '\033[1;33m') "${CURRENT_UID}" "${CURRENT_GID}" "${EXPECTED_UID}" "${EXPECTED_GID}"
RESET=$(printf '\033[0m') exit 0
>&2 printf "%s" "${YELLOW}"
>&2 cat <<EOF
══════════════════════════════════════════════════════════════════════════════
⚠️ ATTENTION: NetAlertX is running as UID ${CURRENT_UID}:${CURRENT_GID}.
Hardened permissions, file ownership, and runtime isolation expect the
dedicated service account (${EXPECTED_USER} -> ${EXPECTED_UID}:${EXPECTED_GID}).
When you override the container user (for example, docker run --user 1000:1000
or a Compose "user:" directive), NetAlertX loses crucial safeguards and
future upgrades may silently fail.
Restore the container to the default user:
* Remove any custom --user flag
* Delete "user:" overrides in compose files
* Recreate the container so volume ownership is reset
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/incorrect-user.md
══════════════════════════════════════════════════════════════════════════════
EOF
>&2 printf "%s" "${RESET}"
+69 -261
View File
@@ -45,141 +45,26 @@ def _unique_label(prefix: str) -> str:
return f"{prefix.upper()}__NETALERTX_INTENTIONAL__{uuid.uuid4().hex[:6]}" return f"{prefix.upper()}__NETALERTX_INTENTIONAL__{uuid.uuid4().hex[:6]}"
def _create_docker_volume(prefix: str) -> str: def test_nonroot_custom_uid_logs_note(
name = f"netalertx-test-{prefix}-{uuid.uuid4().hex[:8]}".lower() tmp_path: pathlib.Path,
subprocess.run( uid_gid: tuple[int, int],
["docker", "volume", "create", name], ) -> None:
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return name
def _remove_docker_volume(name: str) -> None:
subprocess.run(
[
"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():
raise RuntimeError(f"Cannot chown missing path {host_path}")
cmd = [
"docker",
"run",
"--rm",
"--userns",
"host",
"--user",
"0:0",
"--entrypoint",
"/bin/chown",
"-v",
f"{host_path}:/mnt",
IMAGE,
"-R",
f"{uid}:{gid}",
"/mnt",
]
try:
subprocess.run(
cmd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError as exc:
raise RuntimeError(f"Failed to chown {host_path} to {uid}:{gid}") from exc
def _setup_mount_tree( def _setup_mount_tree(
tmp_path: pathlib.Path, tmp_path: pathlib.Path,
prefix: str, prefix: str,
seed_config: bool = True, paths = _setup_mount_tree(tmp_path, f"note_uid_{uid}")
seed_db: bool = True, for key in ["data", "app_db", "app_config"]:
) -> dict[str, pathlib.Path]: paths[key].chmod(0o777)
label = _unique_label(prefix) volumes = _build_volume_args_for_keys(paths, {"data"})
base = tmp_path / f"{label}_MOUNT_ROOT" result = _run_container(
base.mkdir() f"note-uid-{uid}",
paths: dict[str, pathlib.Path] = {} volumes,
user=f"{uid}:{gid}",
# Create unified /data mount root sleep_seconds=5,
data_root = base / f"{label}_DATA_INTENTIONAL_NETALERTX_TEST" )
data_root.mkdir(parents=True, exist_ok=True) _assert_contains(result, f"NetAlertX note: current UID {uid} GID {gid}", result.args)
data_root.chmod(0o777) assert "expected UID" in result.output
paths["data"] = data_root assert result.returncode == 0
# Create required data subdirectories and aliases
db_dir = data_root / "db"
db_dir.mkdir(exist_ok=True)
db_dir.chmod(0o777)
paths["app_db"] = db_dir
paths["data_db"] = db_dir
config_dir = data_root / "config"
config_dir.mkdir(exist_ok=True)
config_dir.chmod(0o777)
paths["app_config"] = config_dir paths["app_config"] = config_dir
paths["data_config"] = config_dir paths["data_config"] = config_dir
@@ -571,24 +456,14 @@ def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
Check script: check-network-mode.sh Check script: check-network-mode.sh
Sample message: "⚠️ ATTENTION: NetAlertX is not running with --network=host. Bridge networking..." Sample message: "⚠️ ATTENTION: NetAlertX is not running with --network=host. Bridge networking..."
""" """
data_volume = _seed_data_volume() paths = _setup_mount_tree(tmp_path, "missing_host_network")
config_volume = _seed_config_volume() volumes = _build_volume_args_for_keys(paths, {"data"})
result = None result = _run_container(
try: "missing-host-network",
result = _run_container( volumes,
"missing-host-network", network_mode=None,
None, sleep_seconds=5,
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) _assert_contains(result, "not running with --network=host", result.args)
@@ -632,60 +507,19 @@ def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None:
Check script: /entrypoint.d/20-first-run-db.sh Check script: /entrypoint.d/20-first-run-db.sh
Sample message: "Building initial database schema" Sample message: "Building initial database schema"
""" """
data_volume = _seed_data_volume() paths = _setup_mount_tree(tmp_path, "missing_app_db", seed_db=False)
config_volume = _seed_config_volume() config_file = paths["app_config"] / "app.conf"
# Prepare a minimal writable config that still contains TIMEZONE for plugin_helper config_file.write_text("TIMEZONE='UTC'\n")
subprocess.run( config_file.chmod(0o600)
[ volumes = _build_volume_args_for_keys(paths, {"data"})
"docker", result = _run_container(
"run", "missing-app-db",
"--rm", volumes,
"--entrypoint", user="20211:20211",
"/bin/sh", sleep_seconds=5,
"-v", wait_for_exit=True,
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 assert (paths["app_db"] / "app.db").exists()
db_file_check = None
try:
result = _run_container(
"missing-app-db",
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 assert result.returncode != 0
@@ -700,13 +534,11 @@ def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None:
Sample messages: "⚠️ ATTENTION: Nginx configuration mount /tmp/nginx/active-config is missing." Sample messages: "⚠️ ATTENTION: Nginx configuration mount /tmp/nginx/active-config is missing."
"⚠️ ATTENTION: Unable to write to /tmp/nginx/active-config/netalertx.conf." "⚠️ 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") paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf")
for key in ["data", "app_db", "app_config", "app_log", "app_api", "services_run"]: for key in ["data", "app_db", "app_config", "app_log", "app_api", "services_run"]:
paths[key].chmod(0o777) paths[key].chmod(0o777)
_chown_netalertx(paths[key]) volumes = _build_volume_args_for_keys(paths, {"data"})
volumes = _build_volume_args_for_keys( volumes += _build_volume_args_for_keys(
paths, paths,
{"app_log", "app_api", "services_run"}, {"app_log", "app_api", "services_run"},
) )
@@ -714,27 +546,19 @@ def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None:
"--tmpfs", "--tmpfs",
f"{VOLUME_MAP['nginx_conf']}:uid=20211,gid=20211,mode=500", f"{VOLUME_MAP['nginx_conf']}:uid=20211,gid=20211,mode=500",
] ]
try: result = _run_container(
result = _run_container( "custom-port-ro-conf",
"custom-port-ro-conf", volumes,
volumes, env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"},
env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"}, user="20211:20211",
user="20211:20211", extra_args=extra_args,
extra_args=extra_args, sleep_seconds=5,
volume_specs=[ )
f"{data_volume}:{VOLUME_MAP['data']}", _assert_contains(result, "Unable to write to", result.args)
f"{config_volume}:{VOLUME_MAP['app_config']}", _assert_contains(
], result, f"{VOLUME_MAP['nginx_conf']}/netalertx.conf", result.args
sleep_seconds=5, )
) assert result.returncode != 0
_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:
_remove_docker_volume(config_volume)
_remove_docker_volume(data_volume)
def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None: def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None:
"""Test excessive capabilities detection - simulates container with extra capabilities. """Test excessive capabilities detection - simulates container with extra capabilities.
@@ -746,24 +570,16 @@ def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None:
Check script: 90-excessive-capabilities.sh Check script: 90-excessive-capabilities.sh
Sample message: "Excessive capabilities detected" Sample message: "Excessive capabilities detected"
""" """
data_volume = _seed_data_volume() paths = _setup_mount_tree(tmp_path, "excessive_caps")
config_volume = _seed_config_volume() volumes = _build_volume_args_for_keys(paths, {"data"})
try: result = _run_container(
result = _run_container( "excessive-caps",
"excessive-caps", volumes,
None, extra_args=["--cap-add=SYS_ADMIN", "--cap-add=NET_BROADCAST"],
extra_args=["--cap-add=SYS_ADMIN", "--cap-add=NET_BROADCAST"], sleep_seconds=5,
sleep_seconds=5, )
volume_specs=[ _assert_contains(result, "Excessive capabilities detected", result.args)
f"{data_volume}:{VOLUME_MAP['data']}", _assert_contains(result, "bounding caps:", result.args)
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: def test_appliance_integrity_read_write_mode(tmp_path: pathlib.Path) -> None:
"""Test appliance integrity - simulates running with read-write root filesystem. """Test appliance integrity - simulates running with read-write root filesystem.
@@ -775,21 +591,13 @@ def test_appliance_integrity_read_write_mode(tmp_path: pathlib.Path) -> None:
Check script: 95-appliance-integrity.sh Check script: 95-appliance-integrity.sh
Sample message: "Container is running as read-write, not in read-only mode" Sample message: "Container is running as read-write, not in read-only mode"
""" """
data_volume = _seed_data_volume() paths = _setup_mount_tree(tmp_path, "appliance_integrity")
config_volume = _seed_config_volume() volumes = _build_volume_args_for_keys(paths, {"data"})
try: result = _run_container(
result = _run_container( "appliance-integrity",
"appliance-integrity", volumes,
None, sleep_seconds=5,
sleep_seconds=5, )
volume_specs=[
f"{data_volume}:{VOLUME_MAP['data']}",
f"{config_volume}:{VOLUME_MAP['app_config']}",
],
)
finally:
_remove_docker_volume(config_volume)
_remove_docker_volume(data_volume)
_assert_contains( _assert_contains(
result, "Container is running as read-write, not in read-only mode", result.args result, "Container is running as read-write, not in read-only mode", result.args
) )