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

View File

@@ -18,26 +18,6 @@ fi
if [ "${CURRENT_UID}" -eq "${EXPECTED_UID}" ] && [ "${CURRENT_GID}" -eq "${EXPECTED_GID}" ]; then
exit 0
fi
YELLOW=$(printf '\033[1;33m')
RESET=$(printf '\033[0m')
>&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}"
>&2 printf '\nNetAlertX note: current UID %s GID %s, expected UID %s GID %s\n' \
"${CURRENT_UID}" "${CURRENT_GID}" "${EXPECTED_UID}" "${EXPECTED_GID}"
exit 0

View File

@@ -45,141 +45,26 @@ def _unique_label(prefix: str) -> str:
return f"{prefix.upper()}__NETALERTX_INTENTIONAL__{uuid.uuid4().hex[:6]}"
def _create_docker_volume(prefix: str) -> str:
name = f"netalertx-test-{prefix}-{uuid.uuid4().hex[:8]}".lower()
subprocess.run(
["docker", "volume", "create", name],
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 test_nonroot_custom_uid_logs_note(
tmp_path: pathlib.Path,
uid_gid: tuple[int, int],
) -> None:
def _setup_mount_tree(
tmp_path: pathlib.Path,
prefix: str,
seed_config: bool = True,
seed_db: bool = True,
) -> dict[str, pathlib.Path]:
label = _unique_label(prefix)
base = tmp_path / f"{label}_MOUNT_ROOT"
base.mkdir()
paths: dict[str, pathlib.Path] = {}
# Create unified /data mount root
data_root = base / f"{label}_DATA_INTENTIONAL_NETALERTX_TEST"
data_root.mkdir(parents=True, exist_ok=True)
data_root.chmod(0o777)
paths["data"] = data_root
# 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 = _setup_mount_tree(tmp_path, f"note_uid_{uid}")
for key in ["data", "app_db", "app_config"]:
paths[key].chmod(0o777)
volumes = _build_volume_args_for_keys(paths, {"data"})
result = _run_container(
f"note-uid-{uid}",
volumes,
user=f"{uid}:{gid}",
sleep_seconds=5,
)
_assert_contains(result, f"NetAlertX note: current UID {uid} GID {gid}", result.args)
assert "expected UID" in result.output
assert result.returncode == 0
paths["app_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
Sample message: "⚠️ ATTENTION: NetAlertX is not running with --network=host. Bridge networking..."
"""
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
paths = _setup_mount_tree(tmp_path, "missing_host_network")
volumes = _build_volume_args_for_keys(paths, {"data"})
result = _run_container(
"missing-host-network",
volumes,
network_mode=None,
sleep_seconds=5,
)
_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
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,
paths = _setup_mount_tree(tmp_path, "missing_app_db", seed_db=False)
config_file = paths["app_config"] / "app.conf"
config_file.write_text("TIMEZONE='UTC'\n")
config_file.chmod(0o600)
volumes = _build_volume_args_for_keys(paths, {"data"})
result = _run_container(
"missing-app-db",
volumes,
user="20211:20211",
sleep_seconds=5,
wait_for_exit=True,
)
result = None
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 (paths["app_db"] / "app.db").exists()
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."
"⚠️ 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(
volumes = _build_volume_args_for_keys(paths, {"data"})
volumes += _build_volume_args_for_keys(
paths,
{"app_log", "app_api", "services_run"},
)
@@ -714,27 +546,19 @@ def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None:
"--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 result.returncode != 0
finally:
_remove_docker_volume(config_volume)
_remove_docker_volume(data_volume)
result = _run_container(
"custom-port-ro-conf",
volumes,
env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"},
user="20211:20211",
extra_args=extra_args,
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
def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None:
"""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
Sample message: "Excessive capabilities detected"
"""
data_volume = _seed_data_volume()
config_volume = _seed_config_volume()
try:
result = _run_container(
"excessive-caps",
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)
paths = _setup_mount_tree(tmp_path, "excessive_caps")
volumes = _build_volume_args_for_keys(paths, {"data"})
result = _run_container(
"excessive-caps",
volumes,
extra_args=["--cap-add=SYS_ADMIN", "--cap-add=NET_BROADCAST"],
sleep_seconds=5,
)
_assert_contains(result, "Excessive capabilities detected", result.args)
_assert_contains(result, "bounding caps:", result.args)
def test_appliance_integrity_read_write_mode(tmp_path: pathlib.Path) -> None:
"""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
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']}",
],
)
finally:
_remove_docker_volume(config_volume)
_remove_docker_volume(data_volume)
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
)