mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-07 18:51:35 -07:00
Merge pull request #1363 from adamoutler/allow-other-users
Allow other users (Non-Synology)
This commit is contained in:
@@ -29,13 +29,23 @@ ENV PATH="/opt/venv/bin:$PATH"
|
|||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
COPY requirements.txt /tmp/requirements.txt
|
COPY requirements.txt /tmp/requirements.txt
|
||||||
RUN apk add --no-cache bash shadow python3 python3-dev gcc musl-dev libffi-dev openssl-dev git \
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
shadow \
|
||||||
|
python3 \
|
||||||
|
python3-dev \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
libffi-dev \
|
||||||
|
openssl-dev \
|
||||||
|
git \
|
||||||
|
rust \
|
||||||
|
cargo \
|
||||||
&& python -m venv /opt/venv
|
&& python -m venv /opt/venv
|
||||||
|
|
||||||
# Create virtual environment owned by root, but readable by everyone else. This makes it easy to copy
|
# Upgrade pip/wheel/setuptools and install Python packages
|
||||||
# into hardened stage without worrying about permissions and keeps image size small. Keeping the commands
|
RUN python -m pip install --upgrade pip setuptools wheel && \
|
||||||
# together makes for a slightly smaller image size.
|
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
|
||||||
RUN pip install --no-cache-dir -r /tmp/requirements.txt && \
|
|
||||||
chmod -R u-rwx,g-rwx /opt
|
chmod -R u-rwx,g-rwx /opt
|
||||||
|
|
||||||
# second stage is the main runtime stage with just the minimum required to run the application
|
# second stage is the main runtime stage with just the minimum required to run the application
|
||||||
@@ -43,6 +53,12 @@ RUN pip install --no-cache-dir -r /tmp/requirements.txt && \
|
|||||||
FROM alpine:3.22 AS runner
|
FROM alpine:3.22 AS runner
|
||||||
|
|
||||||
ARG INSTALL_DIR=/app
|
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 (separate from service account to avoid UID/GID collisions)
|
||||||
|
ARG READONLY_UID=20212
|
||||||
|
ARG READONLY_GID=20212
|
||||||
|
|
||||||
# NetAlertX app directories
|
# NetAlertX app directories
|
||||||
ENV NETALERTX_APP=${INSTALL_DIR}
|
ENV NETALERTX_APP=${INSTALL_DIR}
|
||||||
@@ -122,8 +138,8 @@ RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 i
|
|||||||
nginx supercronic shadow && \
|
nginx supercronic shadow && \
|
||||||
rm -Rf /var/cache/apk/* && \
|
rm -Rf /var/cache/apk/* && \
|
||||||
rm -Rf /etc/nginx && \
|
rm -Rf /etc/nginx && \
|
||||||
addgroup -g 20211 ${NETALERTX_GROUP} && \
|
addgroup -g ${NETALERTX_GID} ${NETALERTX_GROUP} && \
|
||||||
adduser -u 20211 -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
|
adduser -u ${NETALERTX_UID} -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
|
||||||
apk del shadow
|
apk del shadow
|
||||||
|
|
||||||
|
|
||||||
@@ -141,21 +157,22 @@ RUN install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 ${READ_WRITE_FO
|
|||||||
|
|
||||||
# Copy version information into the image
|
# Copy version information into the image
|
||||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION
|
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION
|
||||||
|
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION_PREV
|
||||||
|
|
||||||
# Copy the virtualenv from the builder stage
|
# Copy the virtualenv from the builder stage (owned by readonly lock owner)
|
||||||
COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
COPY --from=builder --chown=${READONLY_UID}:${READONLY_GID} ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||||
|
|
||||||
|
|
||||||
# Initialize each service with the dockerfiles/init-*.sh scripts, once.
|
# Initialize each service with the dockerfiles/init-*.sh scripts, once.
|
||||||
# This is done after the copy of the venv to ensure the venv is in place
|
# This is done after the copy of the venv to ensure the venv is in place
|
||||||
# although it may be quicker to do it before the copy, it keeps the image
|
# although it may be quicker to do it before the copy, it keeps the image
|
||||||
# layers smaller to do it after.
|
# layers smaller to do it after.
|
||||||
RUN if [ -f '.VERSION' ]; then \
|
RUN for vfile in .VERSION .VERSION_PREV; do \
|
||||||
cp '.VERSION' "${NETALERTX_APP}/.VERSION"; \
|
if [ ! -f "${NETALERTX_APP}/${vfile}" ]; then \
|
||||||
else \
|
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/${vfile}"; \
|
||||||
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/.VERSION"; \
|
fi; \
|
||||||
fi && \
|
chown ${READONLY_UID}:${READONLY_GID} "${NETALERTX_APP}/${vfile}"; \
|
||||||
chown 20212:20212 "${NETALERTX_APP}/.VERSION" && \
|
done && \
|
||||||
apk add --no-cache libcap && \
|
apk add --no-cache libcap && \
|
||||||
setcap cap_net_raw+ep /bin/busybox && \
|
setcap cap_net_raw+ep /bin/busybox && \
|
||||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
|
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
|
||||||
@@ -179,6 +196,12 @@ ENTRYPOINT ["/bin/sh","/entrypoint.sh"]
|
|||||||
# This stage is separate from Runner stage so that devcontainer can use the Runner stage.
|
# This stage is separate from Runner stage so that devcontainer can use the Runner stage.
|
||||||
FROM runner AS hardened
|
FROM runner AS hardened
|
||||||
|
|
||||||
|
# Re-declare UID/GID args for this stage
|
||||||
|
ARG NETALERTX_UID=20211
|
||||||
|
ARG NETALERTX_GID=20211
|
||||||
|
ARG READONLY_UID=20212
|
||||||
|
ARG READONLY_GID=20212
|
||||||
|
|
||||||
ENV UMASK=0077
|
ENV UMASK=0077
|
||||||
|
|
||||||
# Create readonly user and group with no shell access.
|
# Create readonly user and group with no shell access.
|
||||||
@@ -186,8 +209,8 @@ ENV UMASK=0077
|
|||||||
# AI may claim this is stupid, but it's actually least possible permissions as
|
# AI may claim this is stupid, but it's actually least possible permissions as
|
||||||
# read-only user cannot login, cannot sudo, has no write permission, and cannot even
|
# read-only user cannot login, cannot sudo, has no write permission, and cannot even
|
||||||
# read the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
|
# read the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
|
||||||
RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
|
RUN addgroup -g ${READONLY_GID} "${READ_ONLY_GROUP}" && \
|
||||||
adduser -u 20212 -G "${READ_ONLY_GROUP}" -D -h /app "${READ_ONLY_USER}"
|
adduser -u ${READONLY_UID} -G "${READ_ONLY_GROUP}" -D -h /app "${READ_ONLY_USER}"
|
||||||
|
|
||||||
|
|
||||||
# reduce permissions to minimum necessary for all NetAlertX files and folders
|
# reduce permissions to minimum necessary for all NetAlertX files and folders
|
||||||
@@ -198,15 +221,17 @@ RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
|
|||||||
RUN chown -R ${READ_ONLY_USER}:${READ_ONLY_GROUP} ${READ_ONLY_FOLDERS} && \
|
RUN chown -R ${READ_ONLY_USER}:${READ_ONLY_GROUP} ${READ_ONLY_FOLDERS} && \
|
||||||
chmod -R 004 ${READ_ONLY_FOLDERS} && \
|
chmod -R 004 ${READ_ONLY_FOLDERS} && \
|
||||||
find ${READ_ONLY_FOLDERS} -type d -exec chmod 005 {} + && \
|
find ${READ_ONLY_FOLDERS} -type d -exec chmod 005 {} + && \
|
||||||
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 ${READ_WRITE_FOLDERS} && \
|
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 0777 ${READ_WRITE_FOLDERS} && \
|
||||||
chown -R ${NETALERTX_USER}:${NETALERTX_GROUP} ${READ_WRITE_FOLDERS} && \
|
|
||||||
chmod -R 600 ${READ_WRITE_FOLDERS} && \
|
|
||||||
find ${READ_WRITE_FOLDERS} -type d -exec chmod 700 {} + && \
|
|
||||||
chown ${READ_ONLY_USER}:${READ_ONLY_GROUP} /entrypoint.sh /opt /opt/venv && \
|
chown ${READ_ONLY_USER}:${READ_ONLY_GROUP} /entrypoint.sh /opt /opt/venv && \
|
||||||
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh ${SYSTEM_SERVICES_SCRIPTS}/* ${ENTRYPOINT_CHECKS}/* /app /opt /opt/venv && \
|
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh ${SYSTEM_SERVICES_SCRIPTS}/* ${ENTRYPOINT_CHECKS}/* /app /opt /opt/venv && \
|
||||||
for dir in ${READ_WRITE_FOLDERS}; do \
|
# Do not bake first-run artifacts into the image. If present, Docker volume copy-up
|
||||||
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 "$dir"; \
|
# will persist restrictive ownership/modes into fresh named volumes, breaking
|
||||||
done && \
|
# arbitrary non-root UID/GID runs.
|
||||||
|
rm -f \
|
||||||
|
"${NETALERTX_CONFIG}/app.conf" \
|
||||||
|
"${NETALERTX_DB_FILE}" \
|
||||||
|
"${NETALERTX_DB_FILE}-shm" \
|
||||||
|
"${NETALERTX_DB_FILE}-wal" || true && \
|
||||||
apk del apk-tools && \
|
apk del apk-tools && \
|
||||||
rm -Rf /var /etc/sudoers.d/* /etc/shadow /etc/gshadow /etc/sudoers \
|
rm -Rf /var /etc/sudoers.d/* /etc/shadow /etc/gshadow /etc/sudoers \
|
||||||
/lib/apk /lib/firmware /lib/modules-load.d /lib/sysctl.d /mnt /home/ /root \
|
/lib/apk /lib/firmware /lib/modules-load.d /lib/sysctl.d /mnt /home/ /root \
|
||||||
@@ -249,7 +274,7 @@ USER root
|
|||||||
|
|
||||||
RUN apk add --no-cache git nano vim jq php83-pecl-xdebug py3-pip nodejs sudo gpgconf pytest \
|
RUN apk add --no-cache git nano vim jq php83-pecl-xdebug py3-pip nodejs sudo gpgconf pytest \
|
||||||
pytest-cov zsh alpine-zsh-config shfmt github-cli py3-yaml py3-docker-py docker-cli docker-cli-buildx \
|
pytest-cov zsh alpine-zsh-config shfmt github-cli py3-yaml py3-docker-py docker-cli docker-cli-buildx \
|
||||||
docker-cli-compose shellcheck
|
docker-cli-compose shellcheck py3-psutil
|
||||||
|
|
||||||
# Install hadolint (Dockerfile linter)
|
# Install hadolint (Dockerfile linter)
|
||||||
RUN curl -L https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint && \
|
RUN curl -L https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint && \
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ USER root
|
|||||||
|
|
||||||
RUN apk add --no-cache git nano vim jq php83-pecl-xdebug py3-pip nodejs sudo gpgconf pytest \
|
RUN apk add --no-cache git nano vim jq php83-pecl-xdebug py3-pip nodejs sudo gpgconf pytest \
|
||||||
pytest-cov zsh alpine-zsh-config shfmt github-cli py3-yaml py3-docker-py docker-cli docker-cli-buildx \
|
pytest-cov zsh alpine-zsh-config shfmt github-cli py3-yaml py3-docker-py docker-cli docker-cli-buildx \
|
||||||
docker-cli-compose shellcheck
|
docker-cli-compose shellcheck py3-psutil
|
||||||
|
|
||||||
# Install hadolint (Dockerfile linter)
|
# Install hadolint (Dockerfile linter)
|
||||||
RUN curl -L https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint && \
|
RUN curl -L https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint && \
|
||||||
|
|||||||
42
Dockerfile
42
Dockerfile
@@ -50,6 +50,12 @@ RUN python -m pip install --upgrade pip setuptools wheel && \
|
|||||||
FROM alpine:3.22 AS runner
|
FROM alpine:3.22 AS runner
|
||||||
|
|
||||||
ARG INSTALL_DIR=/app
|
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 (separate from service account to avoid UID/GID collisions)
|
||||||
|
ARG READONLY_UID=20212
|
||||||
|
ARG READONLY_GID=20212
|
||||||
|
|
||||||
# NetAlertX app directories
|
# NetAlertX app directories
|
||||||
ENV NETALERTX_APP=${INSTALL_DIR}
|
ENV NETALERTX_APP=${INSTALL_DIR}
|
||||||
@@ -129,8 +135,8 @@ RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 i
|
|||||||
nginx supercronic shadow && \
|
nginx supercronic shadow && \
|
||||||
rm -Rf /var/cache/apk/* && \
|
rm -Rf /var/cache/apk/* && \
|
||||||
rm -Rf /etc/nginx && \
|
rm -Rf /etc/nginx && \
|
||||||
addgroup -g 20211 ${NETALERTX_GROUP} && \
|
addgroup -g ${NETALERTX_GID} ${NETALERTX_GROUP} && \
|
||||||
adduser -u 20211 -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
|
adduser -u ${NETALERTX_UID} -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
|
||||||
apk del shadow
|
apk del shadow
|
||||||
|
|
||||||
|
|
||||||
@@ -150,8 +156,8 @@ RUN install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 ${READ_WRITE_FO
|
|||||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION
|
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION
|
||||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION_PREV
|
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION_PREV
|
||||||
|
|
||||||
# Copy the virtualenv from the builder stage
|
# Copy the virtualenv from the builder stage (owned by readonly lock owner)
|
||||||
COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
COPY --from=builder --chown=${READONLY_UID}:${READONLY_GID} ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||||
|
|
||||||
|
|
||||||
# Initialize each service with the dockerfiles/init-*.sh scripts, once.
|
# Initialize each service with the dockerfiles/init-*.sh scripts, once.
|
||||||
@@ -162,7 +168,7 @@ RUN for vfile in .VERSION .VERSION_PREV; do \
|
|||||||
if [ ! -f "${NETALERTX_APP}/${vfile}" ]; then \
|
if [ ! -f "${NETALERTX_APP}/${vfile}" ]; then \
|
||||||
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/${vfile}"; \
|
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/${vfile}"; \
|
||||||
fi; \
|
fi; \
|
||||||
chown 20212:20212 "${NETALERTX_APP}/${vfile}"; \
|
chown ${READONLY_UID}:${READONLY_GID} "${NETALERTX_APP}/${vfile}"; \
|
||||||
done && \
|
done && \
|
||||||
apk add --no-cache libcap && \
|
apk add --no-cache libcap && \
|
||||||
setcap cap_net_raw+ep /bin/busybox && \
|
setcap cap_net_raw+ep /bin/busybox && \
|
||||||
@@ -187,6 +193,12 @@ ENTRYPOINT ["/bin/sh","/entrypoint.sh"]
|
|||||||
# This stage is separate from Runner stage so that devcontainer can use the Runner stage.
|
# This stage is separate from Runner stage so that devcontainer can use the Runner stage.
|
||||||
FROM runner AS hardened
|
FROM runner AS hardened
|
||||||
|
|
||||||
|
# Re-declare UID/GID args for this stage
|
||||||
|
ARG NETALERTX_UID=20211
|
||||||
|
ARG NETALERTX_GID=20211
|
||||||
|
ARG READONLY_UID=20212
|
||||||
|
ARG READONLY_GID=20212
|
||||||
|
|
||||||
ENV UMASK=0077
|
ENV UMASK=0077
|
||||||
|
|
||||||
# Create readonly user and group with no shell access.
|
# Create readonly user and group with no shell access.
|
||||||
@@ -194,8 +206,8 @@ ENV UMASK=0077
|
|||||||
# AI may claim this is stupid, but it's actually least possible permissions as
|
# AI may claim this is stupid, but it's actually least possible permissions as
|
||||||
# read-only user cannot login, cannot sudo, has no write permission, and cannot even
|
# read-only user cannot login, cannot sudo, has no write permission, and cannot even
|
||||||
# read the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
|
# read the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
|
||||||
RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
|
RUN addgroup -g ${READONLY_GID} "${READ_ONLY_GROUP}" && \
|
||||||
adduser -u 20212 -G "${READ_ONLY_GROUP}" -D -h /app "${READ_ONLY_USER}"
|
adduser -u ${READONLY_UID} -G "${READ_ONLY_GROUP}" -D -h /app "${READ_ONLY_USER}"
|
||||||
|
|
||||||
|
|
||||||
# reduce permissions to minimum necessary for all NetAlertX files and folders
|
# reduce permissions to minimum necessary for all NetAlertX files and folders
|
||||||
@@ -206,15 +218,17 @@ RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
|
|||||||
RUN chown -R ${READ_ONLY_USER}:${READ_ONLY_GROUP} ${READ_ONLY_FOLDERS} && \
|
RUN chown -R ${READ_ONLY_USER}:${READ_ONLY_GROUP} ${READ_ONLY_FOLDERS} && \
|
||||||
chmod -R 004 ${READ_ONLY_FOLDERS} && \
|
chmod -R 004 ${READ_ONLY_FOLDERS} && \
|
||||||
find ${READ_ONLY_FOLDERS} -type d -exec chmod 005 {} + && \
|
find ${READ_ONLY_FOLDERS} -type d -exec chmod 005 {} + && \
|
||||||
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 ${READ_WRITE_FOLDERS} && \
|
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 0777 ${READ_WRITE_FOLDERS} && \
|
||||||
chown -R ${NETALERTX_USER}:${NETALERTX_GROUP} ${READ_WRITE_FOLDERS} && \
|
|
||||||
chmod -R 600 ${READ_WRITE_FOLDERS} && \
|
|
||||||
find ${READ_WRITE_FOLDERS} -type d -exec chmod 700 {} + && \
|
|
||||||
chown ${READ_ONLY_USER}:${READ_ONLY_GROUP} /entrypoint.sh /opt /opt/venv && \
|
chown ${READ_ONLY_USER}:${READ_ONLY_GROUP} /entrypoint.sh /opt /opt/venv && \
|
||||||
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh ${SYSTEM_SERVICES_SCRIPTS}/* ${ENTRYPOINT_CHECKS}/* /app /opt /opt/venv && \
|
chmod 005 /entrypoint.sh ${SYSTEM_SERVICES}/*.sh ${SYSTEM_SERVICES_SCRIPTS}/* ${ENTRYPOINT_CHECKS}/* /app /opt /opt/venv && \
|
||||||
for dir in ${READ_WRITE_FOLDERS}; do \
|
# Do not bake first-run artifacts into the image. If present, Docker volume copy-up
|
||||||
install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 "$dir"; \
|
# will persist restrictive ownership/modes into fresh named volumes, breaking
|
||||||
done && \
|
# arbitrary non-root UID/GID runs.
|
||||||
|
rm -f \
|
||||||
|
"${NETALERTX_CONFIG}/app.conf" \
|
||||||
|
"${NETALERTX_DB_FILE}" \
|
||||||
|
"${NETALERTX_DB_FILE}-shm" \
|
||||||
|
"${NETALERTX_DB_FILE}-wal" || true && \
|
||||||
apk del apk-tools && \
|
apk del apk-tools && \
|
||||||
rm -Rf /var /etc/sudoers.d/* /etc/shadow /etc/gshadow /etc/sudoers \
|
rm -Rf /var /etc/sudoers.d/* /etc/shadow /etc/gshadow /etc/sudoers \
|
||||||
/lib/apk /lib/firmware /lib/modules-load.d /lib/sysctl.d /mnt /home/ /root \
|
/lib/apk /lib/firmware /lib/modules-load.d /lib/sysctl.d /mnt /home/ /root \
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ services:
|
|||||||
image: netalertx:latest
|
image: netalertx:latest
|
||||||
container_name: netalertx # The name when you docker contiainer ls
|
container_name: netalertx # The name when you docker contiainer ls
|
||||||
read_only: true # Make the container filesystem read-only
|
read_only: true # Make the container filesystem read-only
|
||||||
|
# Runtime user is configurable; defaults align with image build args
|
||||||
|
user: "${NETALERTX_UID:-20211}:${NETALERTX_GID:-20211}"
|
||||||
cap_drop: # Drop all capabilities for enhanced security
|
cap_drop: # Drop all capabilities for enhanced security
|
||||||
- ALL
|
- ALL
|
||||||
cap_add: # Add only the necessary capabilities
|
cap_add: # Add only the necessary capabilities
|
||||||
@@ -49,7 +51,7 @@ services:
|
|||||||
# uid=20211 and gid=20211 is the netalertx user inside the container
|
# uid=20211 and gid=20211 is the netalertx user inside the container
|
||||||
# mode=1700 gives rwx------ permissions to the netalertx user only
|
# mode=1700 gives rwx------ permissions to the netalertx user only
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- "/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"
|
||||||
environment:
|
environment:
|
||||||
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces
|
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces
|
||||||
PORT: ${PORT:-20211} # Application port
|
PORT: ${PORT:-20211} # Application port
|
||||||
|
|||||||
@@ -51,18 +51,18 @@ services:
|
|||||||
# - path/on/host/to/dhcp.file:/resources/dhcp.file
|
# - path/on/host/to/dhcp.file:/resources/dhcp.file
|
||||||
|
|
||||||
# tmpfs mount consolidates writable state for a read-only container and improves performance
|
# 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
|
# uid/gid default to the service user (NETALERTX_UID/GID, default 20211)
|
||||||
# mode=1700 grants rwx------ permissions to the netalertx user only
|
# mode=1700 grants rwx------ permissions to the runtime user only
|
||||||
tmpfs:
|
tmpfs:
|
||||||
# Comment out to retain logs between container restarts - this has a server performance impact.
|
# 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
|
# 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.
|
# Please note if you remove the /tmp mount, you must create and maintain sub-folder mounts.
|
||||||
# - /path/on/host/log:/tmp/log
|
# - /path/on/host/log:/tmp/log
|
||||||
# - "/tmp/api: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=20211,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=20211,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:
|
environment:
|
||||||
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces
|
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
|
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
|
### Customize with Environmental Variables
|
||||||
|
|
||||||
You can override the default settings by passing environmental variables to the `docker compose up` command.
|
You can override the default settings by passing environmental variables to the `docker compose up` command.
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ Head to [https://netalertx.com/](https://netalertx.com/) for more gifs and scree
|
|||||||
docker run -d --rm --network=host \
|
docker run -d --rm --network=host \
|
||||||
-v /local_data_dir:/data \
|
-v /local_data_dir:/data \
|
||||||
-v /etc/localtime:/etc/localtime \
|
-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 PORT=20211 \
|
||||||
-e APP_CONF_OVERRIDE={"GRAPHQL_PORT":"20214"} \
|
-e APP_CONF_OVERRIDE={"GRAPHQL_PORT":"20214"} \
|
||||||
ghcr.io/jokob-sk/netalertx:latest
|
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).
|
See alternative [docked-compose examples](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md).
|
||||||
|
|
||||||
### Default ports
|
### 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).
|
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
|
```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
|
sudo chmod -R a+rwx /local_data_dir
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,30 @@
|
|||||||
|
|
||||||
## Issue Description
|
## 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
|
## 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
|
## 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
|
## 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
|
Option B: Run with a custom UID/GID
|
||||||
- Avoid `--user` flags in docker run commands
|
- Set `user:` (or `NETALERTX_UID/NETALERTX_GID`) to your desired UID/GID.
|
||||||
- Allow the container to run with its default UID:GID 20211:20211
|
- 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 volume ownership is reset automatically
|
- Recreate the container so ownership is consistent.
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.
|
- 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)
|
||||||
For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)
|
|
||||||
@@ -37,7 +37,7 @@ def read_config_file():
|
|||||||
|
|
||||||
|
|
||||||
configFile = read_config_file()
|
configFile = read_config_file()
|
||||||
timeZoneSetting = configFile['TIMEZONE']
|
timeZoneSetting = configFile.get('TIMEZONE', default_tz)
|
||||||
if timeZoneSetting not in all_timezones:
|
if timeZoneSetting not in all_timezones:
|
||||||
timeZoneSetting = default_tz
|
timeZoneSetting = default_tz
|
||||||
timeZone = pytz.timezone(timeZoneSetting)
|
timeZone = pytz.timezone(timeZoneSetting)
|
||||||
|
|||||||
6
install/production-filesystem/entrypoint.d/0-storage-permission.sh
Normal file → Executable file
6
install/production-filesystem/entrypoint.d/0-storage-permission.sh
Normal file → Executable file
@@ -23,6 +23,8 @@ ${NETALERTX_CONFIG_FILE}
|
|||||||
${NETALERTX_DB_FILE}
|
${NETALERTX_DB_FILE}
|
||||||
"
|
"
|
||||||
|
|
||||||
|
TARGET_USER="${NETALERTX_USER:-netalertx}"
|
||||||
|
|
||||||
# If running as root, fix permissions first
|
# If running as root, fix permissions first
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
>&2 printf "%s" "${MAGENTA}"
|
>&2 printf "%s" "${MAGENTA}"
|
||||||
@@ -54,11 +56,11 @@ EOF
|
|||||||
# Set ownership and permissions for each read-write path individually
|
# Set ownership and permissions for each read-write path individually
|
||||||
printf '%s\n' "${READ_WRITE_PATHS}" | while IFS= read -r path; do
|
printf '%s\n' "${READ_WRITE_PATHS}" | while IFS= read -r path; do
|
||||||
[ -n "${path}" ] || continue
|
[ -n "${path}" ] || continue
|
||||||
chown -R netalertx "${path}" 2>/dev/null || true
|
chown -R "${TARGET_USER}" "${path}" 2>/dev/null || true
|
||||||
find "${path}" -type d -exec chmod u+rwx {} \;
|
find "${path}" -type d -exec chmod u+rwx {} \;
|
||||||
find "${path}" -type f -exec chmod u+rw {} \;
|
find "${path}" -type f -exec chmod u+rw {} \;
|
||||||
done
|
done
|
||||||
echo Permissions fixed for read-write paths. Please restart the container as user 20211.
|
echo Permissions fixed for read-write paths. Please restart the container as user ${TARGET_USER}.
|
||||||
sleep infinity & wait $!
|
sleep infinity & wait $!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Mount Diagnostic Tool
|
||||||
|
|
||||||
|
Analyzes container mount points for permission issues, persistence risks, and performance problems.
|
||||||
|
|
||||||
|
TODO: Future Enhancements (Roadmap Step 3 & 4)
|
||||||
|
1. Text-based Output: Replace emoji status indicators (✅, ❌) with plain text (e.g., [OK], [FAIL])
|
||||||
|
to ensure compatibility with all terminal types and logging systems.
|
||||||
|
2. OverlayFS/Copy-up Support: Improve detection logic for filesystems like Synology's OverlayFS
|
||||||
|
where files may appear writable but fail on specific operations (locking, mmap).
|
||||||
|
3. Root-to-User Context: Ensure this tool remains accurate when the container starts as root
|
||||||
|
to fix permissions and then drops privileges to the 'netalertx' user. The check should
|
||||||
|
reflect the *effective* permissions of the application user.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -80,7 +95,21 @@ def _resolve_writeable_state(target_path: str) -> bool:
|
|||||||
seen.add(current)
|
seen.add(current)
|
||||||
|
|
||||||
if os.path.exists(current):
|
if os.path.exists(current):
|
||||||
return os.access(current, os.W_OK)
|
if not os.access(current, os.W_OK):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# OverlayFS/Copy-up check: Try to actually write a file to verify
|
||||||
|
if os.path.isdir(current):
|
||||||
|
test_file = os.path.join(current, f".netalertx_write_test_{os.getpid()}")
|
||||||
|
try:
|
||||||
|
with open(test_file, "w") as f:
|
||||||
|
f.write("test")
|
||||||
|
os.remove(test_file)
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
parent_dir = os.path.dirname(current)
|
parent_dir = os.path.dirname(current)
|
||||||
if not parent_dir or parent_dir == current:
|
if not parent_dir or parent_dir == current:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ if [ ! -f "${NETALERTX_CONFIG}/app.conf" ]; then
|
|||||||
>&2 echo "ERROR: Failed to create config directory ${NETALERTX_CONFIG}"
|
>&2 echo "ERROR: Failed to create config directory ${NETALERTX_CONFIG}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
install -m 600 -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} /app/back/app.conf "${NETALERTX_CONFIG}/app.conf" || {
|
install -m 600 /app/back/app.conf "${NETALERTX_CONFIG}/app.conf" || {
|
||||||
>&2 echo "ERROR: Failed to deploy default config to ${NETALERTX_CONFIG}/app.conf"
|
>&2 echo "ERROR: Failed to deploy default config to ${NETALERTX_CONFIG}/app.conf"
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ mkdir -p "$(dirname "$NETALERTX_CONFIG")" || {
|
|||||||
rm -f "$OVERRIDE_FILE"
|
rm -f "$OVERRIDE_FILE"
|
||||||
|
|
||||||
# Check if APP_CONF_OVERRIDE is set
|
# Check if APP_CONF_OVERRIDE is set
|
||||||
if [ -z "$APP_CONF_OVERRIDE" ]; then
|
if [ -n "$APP_CONF_OVERRIDE" ]; then
|
||||||
>&2 echo "APP_CONF_OVERRIDE is not set. Skipping override config file creation."
|
|
||||||
else
|
|
||||||
# Save the APP_CONF_OVERRIDE env variable as a JSON file
|
# Save the APP_CONF_OVERRIDE env variable as a JSON file
|
||||||
echo "$APP_CONF_OVERRIDE" > "$OVERRIDE_FILE" || {
|
echo "$APP_CONF_OVERRIDE" > "$OVERRIDE_FILE" || {
|
||||||
>&2 echo "ERROR: Failed to write override config to $OVERRIDE_FILE"
|
>&2 echo "ERROR: Failed to write override config to $OVERRIDE_FILE"
|
||||||
@@ -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
|
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 <<EOF
|
||||||
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
|
⚠️ ATTENTION: Path is not a regular file.
|
||||||
|
|
||||||
|
The path "${path}" is not a regular file (current type: $(stat -c %F "$path" 2>/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
|
EOF
|
||||||
>&2 printf "%s" "${RESET}"
|
>&2 printf "%s" "${RESET}"
|
||||||
elif [ ! -r "$path" ]; then
|
elif [ ! -r "$path" ]; then
|
||||||
@@ -9,35 +9,15 @@ CURRENT_GID="$(id -g)"
|
|||||||
|
|
||||||
# Fallback to known defaults when lookups fail
|
# Fallback to known defaults when lookups fail
|
||||||
if [ -z "${EXPECTED_UID}" ]; then
|
if [ -z "${EXPECTED_UID}" ]; then
|
||||||
EXPECTED_UID="20211"
|
EXPECTED_UID="${CURRENT_UID}"
|
||||||
fi
|
fi
|
||||||
if [ -z "${EXPECTED_GID}" ]; then
|
if [ -z "${EXPECTED_GID}" ]; then
|
||||||
EXPECTED_GID="20211"
|
EXPECTED_GID="${CURRENT_GID}"
|
||||||
fi
|
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}"
|
|
||||||
|
|||||||
@@ -5,22 +5,27 @@
|
|||||||
|
|
||||||
# Define ports from ENV variables, applying defaults
|
# Define ports from ENV variables, applying defaults
|
||||||
PORT_APP=${PORT:-20211}
|
PORT_APP=${PORT:-20211}
|
||||||
# PORT_GQL=${APP_CONF_OVERRIDE:-${GRAPHQL_PORT:-20212}}
|
|
||||||
|
|
||||||
# # Check if ports are configured to be the same
|
# Prefer explicit GRAPHQL_PORT, fall back to parsed override if present.
|
||||||
# if [ "$PORT_APP" -eq "$PORT_GQL" ]; then
|
if [ -n "${APP_CONF_OVERRIDE:-}" ]; then
|
||||||
# cat <<EOF
|
# crude parse: look for GRAPHQL_PORT in JSON-like string
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
PORT_GQL=$(printf '%s' "${APP_CONF_OVERRIDE}" | grep -o 'GRAPHQL_PORT"*[:=]\"*[0-9]\+' | tr -cd '0-9' || true)
|
||||||
# ⚠️ Configuration Warning: Both ports are set to ${PORT_APP}.
|
fi
|
||||||
|
PORT_GQL=${PORT_GQL:-${GRAPHQL_PORT:-20212}}
|
||||||
|
|
||||||
# The Application port (\$PORT) and the GraphQL API port
|
# Check if ports are configured to be the same
|
||||||
# (\$APP_CONF_OVERRIDE or \$GRAPHQL_PORT) are configured to use the
|
if [ "${PORT_APP}" -eq "${PORT_GQL}" ]; then
|
||||||
# same port. This will cause a conflict.
|
cat <<EOF
|
||||||
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
|
⚠️ Configuration Warning: Both ports are set to ${PORT_APP}.
|
||||||
|
|
||||||
# https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
|
The Application port (\$PORT) and the GraphQL API port (\$GRAPHQL_PORT)
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
are configured to use the same port. This will cause a conflict.
|
||||||
# EOF
|
|
||||||
# fi
|
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
|
||||||
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
# Check for netstat (usually provided by busybox)
|
# Check for netstat (usually provided by busybox)
|
||||||
if ! command -v netstat >/dev/null 2>&1; then
|
if ! command -v netstat >/dev/null 2>&1; then
|
||||||
@@ -53,17 +58,16 @@ if echo "$LISTENING_PORTS" | grep -q ":${PORT_APP}$"; then
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# # Check GraphQL Port
|
# Check GraphQL Port (always emit if listening, even if same port)
|
||||||
# # We add a check to avoid double-warning if ports are identical AND in use
|
if echo "$LISTENING_PORTS" | grep -q ":${PORT_GQL}$"; then
|
||||||
# if [ "$PORT_APP" -ne "$PORT_GQL" ] && echo "$LISTENING_PORTS" | grep -q ":${PORT_GQL}$"; then
|
cat <<EOF
|
||||||
# cat <<EOF
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
⚠️ Port Warning: GraphQL API port ${PORT_GQL} is already in use.
|
||||||
# ⚠️ Port Warning: GraphQL API port ${PORT_GQL} is already in use.
|
|
||||||
|
|
||||||
# The GraphQL API (defined by \$APP_CONF_OVERRIDE or \$GRAPHQL_PORT)
|
The GraphQL API (defined by \$APP_CONF_OVERRIDE or \$GRAPHQL_PORT)
|
||||||
# may fail to start.
|
may fail to start.
|
||||||
|
|
||||||
# https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
|
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
══════════════════════════════════════════════════════════════════════════════
|
||||||
# EOF
|
EOF
|
||||||
# fi
|
fi
|
||||||
@@ -50,8 +50,7 @@ fi
|
|||||||
RED='\033[1;31m'
|
RED='\033[1;31m'
|
||||||
GREY='\033[90m'
|
GREY='\033[90m'
|
||||||
RESET='\033[0m'
|
RESET='\033[0m'
|
||||||
printf "%s" "${RED}"
|
NAX='
|
||||||
echo '
|
|
||||||
_ _ _ ___ _ _ __ __
|
_ _ _ ___ _ _ __ __
|
||||||
| \ | | | | / _ \| | | | \ \ / /
|
| \ | | | | / _ \| | | | \ \ / /
|
||||||
| \| | ___| |_/ /_\ \ | ___ _ __| |_ \ V /
|
| \| | ___| |_/ /_\ \ | ___ _ __| |_ \ V /
|
||||||
@@ -60,13 +59,12 @@ echo '
|
|||||||
\_| \_/\___|\__\_| |_/_|\___|_| \__\/ \/
|
\_| \_/\___|\__\_| |_/_|\___|_| \__\/ \/
|
||||||
'
|
'
|
||||||
|
|
||||||
printf "%s" "${RESET}"
|
printf "%b%s%b" "${RED}" "${NAX}" "${RESET}"
|
||||||
echo ' Network intruder and presence detector.
|
echo ' Network intruder and presence detector.
|
||||||
https://netalertx.com
|
https://netalertx.com
|
||||||
|
|
||||||
'
|
'
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
FAILED_STATUS=""
|
FAILED_STATUS=""
|
||||||
echo "Startup pre-checks"
|
echo "Startup pre-checks"
|
||||||
for script in "${ENTRYPOINT_CHECKS}"/*; do
|
for script in "${ENTRYPOINT_CHECKS}"/*; do
|
||||||
@@ -123,7 +121,7 @@ fi
|
|||||||
# Set APP_CONF_OVERRIDE based on GRAPHQL_PORT if not already set
|
# Set APP_CONF_OVERRIDE based on GRAPHQL_PORT if not already set
|
||||||
if [ -n "${GRAPHQL_PORT:-}" ] && [ -z "${APP_CONF_OVERRIDE:-}" ]; then
|
if [ -n "${GRAPHQL_PORT:-}" ] && [ -z "${APP_CONF_OVERRIDE:-}" ]; then
|
||||||
export APP_CONF_OVERRIDE='{"GRAPHQL_PORT":"'"${GRAPHQL_PORT}"'"}'
|
export APP_CONF_OVERRIDE='{"GRAPHQL_PORT":"'"${GRAPHQL_PORT}"'"}'
|
||||||
echo "Setting APP_CONF_OVERRIDE to $APP_CONF_OVERRIDE"
|
>&2 echo "APP_CONF_OVERRIDE detected (set from GRAPHQL_PORT)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@@ -283,15 +281,6 @@ add_service "${SYSTEM_SERVICES}/start-php-fpm.sh" "php-fpm83"
|
|||||||
add_service "${SYSTEM_SERVICES}/start-nginx.sh" "nginx"
|
add_service "${SYSTEM_SERVICES}/start-nginx.sh" "nginx"
|
||||||
add_service "${SYSTEM_SERVICES}/start-backend.sh" "python3"
|
add_service "${SYSTEM_SERVICES}/start-backend.sh" "python3"
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Development Mode Debug Switch
|
|
||||||
################################################################################
|
|
||||||
# If NETALERTX_DEBUG=1, skip automatic service restart on failure
|
|
||||||
# Useful for devcontainer debugging where individual services need to be debugged
|
|
||||||
if [ "${NETALERTX_DEBUG:-0}" -eq 1 ]; then
|
|
||||||
echo "NETALERTX_DEBUG is set to 1, will not shut down other services if one fails."
|
|
||||||
fi
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Service Monitoring Loop (Production Mode)
|
# Service Monitoring Loop (Production Mode)
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import pytest
|
|||||||
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
|
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
|
||||||
GRACE_SECONDS = float(os.environ.get("NETALERTX_TEST_GRACE", "2"))
|
GRACE_SECONDS = float(os.environ.get("NETALERTX_TEST_GRACE", "2"))
|
||||||
DEFAULT_CAPS = ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"]
|
DEFAULT_CAPS = ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"]
|
||||||
|
SUBPROCESS_TIMEOUT_SECONDS = float(os.environ.get("NETALERTX_TEST_SUBPROCESS_TIMEOUT", "60"))
|
||||||
|
|
||||||
CONTAINER_TARGETS: dict[str, str] = {
|
CONTAINER_TARGETS: dict[str, str] = {
|
||||||
"data": "/data",
|
"data": "/data",
|
||||||
@@ -45,78 +46,73 @@ 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 _repo_root() -> pathlib.Path:
|
||||||
name = f"netalertx-test-{prefix}-{uuid.uuid4().hex[:8]}".lower()
|
env = os.environ.get("NETALERTX_REPO_ROOT")
|
||||||
subprocess.run(
|
if env:
|
||||||
["docker", "volume", "create", name],
|
return pathlib.Path(env)
|
||||||
check=True,
|
cur = pathlib.Path(__file__).resolve()
|
||||||
stdout=subprocess.DEVNULL,
|
for parent in cur.parents:
|
||||||
stderr=subprocess.DEVNULL,
|
if any(
|
||||||
)
|
[
|
||||||
return name
|
(parent / "pyproject.toml").exists(),
|
||||||
|
(parent / ".git").exists(),
|
||||||
|
(parent / "back").exists() and (parent / "db").exists(),
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return parent
|
||||||
|
return cur.parents[2]
|
||||||
|
|
||||||
|
|
||||||
def _remove_docker_volume(name: str) -> None:
|
def _docker_visible_tmp_root() -> pathlib.Path:
|
||||||
subprocess.run(
|
"""Return a docker-daemon-visible scratch directory for bind mounts.
|
||||||
["docker", "volume", "rm", "-f", name],
|
|
||||||
check=False,
|
Pytest's default tmp_path lives under /tmp inside the devcontainer, which may
|
||||||
stdout=subprocess.DEVNULL,
|
not be visible to the Docker daemon that evaluates bind mount source paths.
|
||||||
stderr=subprocess.DEVNULL,
|
We use /tmp/pytest-docker-mounts instead of the repo.
|
||||||
)
|
"""
|
||||||
|
|
||||||
|
root = pathlib.Path("/tmp/pytest-docker-mounts")
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
root.chmod(0o777)
|
||||||
|
except PermissionError:
|
||||||
|
# Best-effort; the directory only needs to be writable by the current user.
|
||||||
|
pass
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
def _chown_path(host_path: pathlib.Path, uid: int, gid: int) -> None:
|
def _docker_visible_path(path: pathlib.Path) -> pathlib.Path:
|
||||||
"""Chown a host path using the test image with host user namespace."""
|
"""Map a path into `_docker_visible_tmp_root()` when it lives under /tmp."""
|
||||||
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:
|
try:
|
||||||
subprocess.run(
|
if str(path).startswith("/tmp/"):
|
||||||
cmd,
|
return _docker_visible_tmp_root() / path.name
|
||||||
check=True,
|
except Exception:
|
||||||
stdout=subprocess.DEVNULL,
|
pass
|
||||||
stderr=subprocess.DEVNULL,
|
return path
|
||||||
)
|
|
||||||
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,
|
seed_config: bool = True,
|
||||||
seed_db: bool = True,
|
seed_db: bool = True,
|
||||||
) -> dict[str, pathlib.Path]:
|
) -> dict[str, pathlib.Path]:
|
||||||
|
"""Create a compose-like host tree with permissive perms for arbitrary UID/GID."""
|
||||||
|
|
||||||
label = _unique_label(prefix)
|
label = _unique_label(prefix)
|
||||||
base = tmp_path / f"{label}_MOUNT_ROOT"
|
base = _docker_visible_tmp_root() / f"{label}_MOUNT_ROOT"
|
||||||
base.mkdir()
|
base.mkdir()
|
||||||
|
base.chmod(0o777)
|
||||||
|
|
||||||
paths: dict[str, pathlib.Path] = {}
|
paths: dict[str, pathlib.Path] = {}
|
||||||
|
|
||||||
# Create unified /data mount root
|
|
||||||
data_root = base / f"{label}_DATA_INTENTIONAL_NETALERTX_TEST"
|
data_root = base / f"{label}_DATA_INTENTIONAL_NETALERTX_TEST"
|
||||||
data_root.mkdir(parents=True, exist_ok=True)
|
data_root.mkdir(parents=True, exist_ok=True)
|
||||||
data_root.chmod(0o777)
|
data_root.chmod(0o777)
|
||||||
paths["data"] = data_root
|
paths["data"] = data_root
|
||||||
|
|
||||||
# Create required data subdirectories and aliases
|
|
||||||
db_dir = data_root / "db"
|
db_dir = data_root / "db"
|
||||||
db_dir.mkdir(exist_ok=True)
|
db_dir.mkdir(exist_ok=True)
|
||||||
db_dir.chmod(0o777)
|
db_dir.chmod(0o777)
|
||||||
@@ -129,17 +125,12 @@ def _setup_mount_tree(
|
|||||||
paths["app_config"] = config_dir
|
paths["app_config"] = config_dir
|
||||||
paths["data_config"] = config_dir
|
paths["data_config"] = config_dir
|
||||||
|
|
||||||
# Optional /tmp mounts that certain tests intentionally bind
|
|
||||||
for key in OPTIONAL_TMP_KEYS:
|
for key in OPTIONAL_TMP_KEYS:
|
||||||
folder_name = f"{label}_{key.upper()}_INTENTIONAL_NETALERTX_TEST"
|
folder_name = f"{label}_{key.upper()}_INTENTIONAL_NETALERTX_TEST"
|
||||||
host_path = base / folder_name
|
host_path = base / folder_name
|
||||||
host_path.mkdir(parents=True, exist_ok=True)
|
host_path.mkdir(parents=True, exist_ok=True)
|
||||||
try:
|
host_path.chmod(0o777)
|
||||||
host_path.chmod(0o777)
|
|
||||||
except PermissionError:
|
|
||||||
pass
|
|
||||||
paths[key] = host_path
|
paths[key] = host_path
|
||||||
# Provide backwards-compatible aliases where helpful
|
|
||||||
if key == "app_log":
|
if key == "app_log":
|
||||||
paths["log"] = host_path
|
paths["log"] = host_path
|
||||||
elif key == "app_api":
|
elif key == "app_api":
|
||||||
@@ -147,54 +138,45 @@ def _setup_mount_tree(
|
|||||||
elif key == "nginx_conf":
|
elif key == "nginx_conf":
|
||||||
paths["nginx_active"] = host_path
|
paths["nginx_active"] = host_path
|
||||||
|
|
||||||
# Determine repo root from env or by walking up from this file
|
repo_root = _repo_root()
|
||||||
repo_root_env = os.environ.get("NETALERTX_REPO_ROOT")
|
|
||||||
if repo_root_env:
|
|
||||||
repo_root = pathlib.Path(repo_root_env)
|
|
||||||
else:
|
|
||||||
repo_root = None
|
|
||||||
cur = pathlib.Path(__file__).resolve()
|
|
||||||
for parent in cur.parents:
|
|
||||||
if any([
|
|
||||||
(parent / "pyproject.toml").exists(),
|
|
||||||
(parent / ".git").exists(),
|
|
||||||
(parent / "back").exists() and (parent / "db").exists()
|
|
||||||
]):
|
|
||||||
repo_root = parent
|
|
||||||
break
|
|
||||||
if repo_root is None:
|
|
||||||
repo_root = cur.parents[2]
|
|
||||||
|
|
||||||
if seed_config:
|
if seed_config:
|
||||||
config_file = paths["app_config"] / "app.conf"
|
|
||||||
config_src = repo_root / "back" / "app.conf"
|
config_src = repo_root / "back" / "app.conf"
|
||||||
if not config_src.exists():
|
config_dst = paths["app_config"] / "app.conf"
|
||||||
print(
|
if config_src.exists():
|
||||||
f"[WARN] Seed file not found: {config_src}. Set NETALERTX_REPO_ROOT or run from repo root. Skipping copy."
|
shutil.copyfile(config_src, config_dst)
|
||||||
)
|
config_dst.chmod(0o666)
|
||||||
else:
|
|
||||||
shutil.copyfile(config_src, config_file)
|
|
||||||
config_file.chmod(0o600)
|
|
||||||
if seed_db:
|
if seed_db:
|
||||||
db_file = paths["app_db"] / "app.db"
|
|
||||||
db_src = repo_root / "db" / "app.db"
|
db_src = repo_root / "db" / "app.db"
|
||||||
if not db_src.exists():
|
db_dst = paths["app_db"] / "app.db"
|
||||||
print(
|
if db_src.exists():
|
||||||
f"[WARN] Seed file not found: {db_src}. Set NETALERTX_REPO_ROOT or run from repo root. Skipping copy."
|
shutil.copyfile(db_src, db_dst)
|
||||||
)
|
db_dst.chmod(0o666)
|
||||||
else:
|
|
||||||
shutil.copyfile(db_src, db_file)
|
|
||||||
db_file.chmod(0o600)
|
|
||||||
|
|
||||||
_chown_netalertx(base)
|
# Ensure every mount point is world-writable so arbitrary UID/GID can write
|
||||||
|
for p in paths.values():
|
||||||
|
if p.is_dir():
|
||||||
|
p.chmod(0o777)
|
||||||
|
for child in p.iterdir():
|
||||||
|
if child.is_dir():
|
||||||
|
child.chmod(0o777)
|
||||||
|
else:
|
||||||
|
child.chmod(0o666)
|
||||||
|
else:
|
||||||
|
p.chmod(0o666)
|
||||||
|
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
def _setup_fixed_mount_tree(base: pathlib.Path) -> dict[str, pathlib.Path]:
|
def _setup_fixed_mount_tree(base: pathlib.Path) -> dict[str, pathlib.Path]:
|
||||||
|
base = _docker_visible_path(base)
|
||||||
|
|
||||||
if base.exists():
|
if base.exists():
|
||||||
shutil.rmtree(base)
|
shutil.rmtree(base)
|
||||||
base.mkdir(parents=True)
|
base.mkdir(parents=True)
|
||||||
|
try:
|
||||||
|
base.chmod(0o777)
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
|
||||||
paths: dict[str, pathlib.Path] = {}
|
paths: dict[str, pathlib.Path] = {}
|
||||||
|
|
||||||
@@ -252,6 +234,42 @@ def _build_volume_args_for_keys(
|
|||||||
return bindings
|
return bindings
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
raise RuntimeError(f"Failed to chown {host_path} to {uid}:{gid}") from exc
|
||||||
|
|
||||||
|
|
||||||
def _chown_root(host_path: pathlib.Path) -> None:
|
def _chown_root(host_path: pathlib.Path) -> None:
|
||||||
_chown_path(host_path, 0, 0)
|
_chown_path(host_path, 0, 0)
|
||||||
|
|
||||||
@@ -260,6 +278,166 @@ def _chown_netalertx(host_path: pathlib.Path) -> None:
|
|||||||
_chown_path(host_path, 20211, 20211)
|
_chown_path(host_path, 20211, 20211)
|
||||||
|
|
||||||
|
|
||||||
|
def _docker_volume_rm(volume_name: str) -> None:
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "volume", "rm", "-f", volume_name],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _docker_volume_create(volume_name: str) -> None:
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "volume", "create", volume_name],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _fresh_named_volume(prefix: str) -> str:
|
||||||
|
name = _unique_label(prefix).lower().replace("__", "-")
|
||||||
|
# Ensure we're exercising Docker's fresh-volume copy-up behavior.
|
||||||
|
_docker_volume_rm(name)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_volume_copy_up(volume_name: str) -> None:
|
||||||
|
"""Ensure a named volume is initialized from the NetAlertX image.
|
||||||
|
|
||||||
|
If we write into the volume first (e.g., with an Alpine helper container),
|
||||||
|
Docker will not perform the image-to-volume copy-up and the volume root may
|
||||||
|
stay root:root 0755, breaking arbitrary UID/GID runs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--userns",
|
||||||
|
"host",
|
||||||
|
"-v",
|
||||||
|
f"{volume_name}:/data",
|
||||||
|
"--entrypoint",
|
||||||
|
"/bin/sh",
|
||||||
|
IMAGE,
|
||||||
|
"-c",
|
||||||
|
"true",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_volume_text_file(
|
||||||
|
volume_name: str,
|
||||||
|
container_path: str,
|
||||||
|
content: str,
|
||||||
|
*,
|
||||||
|
chmod_mode: str = "644",
|
||||||
|
user: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Create/overwrite a text file inside a named volume.
|
||||||
|
|
||||||
|
Uses a tiny helper container so we don't rely on bind mounts (which are
|
||||||
|
resolved on the Docker daemon host).
|
||||||
|
"""
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--userns",
|
||||||
|
"host",
|
||||||
|
]
|
||||||
|
if user:
|
||||||
|
cmd.extend(["--user", user])
|
||||||
|
cmd.extend(
|
||||||
|
[
|
||||||
|
"-v",
|
||||||
|
f"{volume_name}:/data",
|
||||||
|
"alpine:3.22",
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
f"set -eu; mkdir -p \"$(dirname '{container_path}')\"; cat > '{container_path}'; chmod {chmod_mode} '{container_path}'",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
input=content,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _volume_has_file(volume_name: str, container_path: str) -> bool:
|
||||||
|
return (
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--userns",
|
||||||
|
"host",
|
||||||
|
"-v",
|
||||||
|
f"{volume_name}:/data",
|
||||||
|
"alpine:3.22",
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
f"test -f '{container_path}'",
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
).returncode
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"uid_gid",
|
||||||
|
[
|
||||||
|
(1001, 1001),
|
||||||
|
(1502, 1502),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_nonroot_custom_uid_logs_note(
|
||||||
|
tmp_path: pathlib.Path,
|
||||||
|
uid_gid: tuple[int, int],
|
||||||
|
) -> None:
|
||||||
|
"""Ensure arbitrary non-root UID/GID can run with compose-like mounts."""
|
||||||
|
|
||||||
|
uid, gid = uid_gid
|
||||||
|
|
||||||
|
vol = _fresh_named_volume(f"note_uid_{uid}")
|
||||||
|
try:
|
||||||
|
# Fresh named volume at /data: matches default docker-compose UX.
|
||||||
|
result = _run_container(
|
||||||
|
f"note-uid-{uid}",
|
||||||
|
volumes=None,
|
||||||
|
volume_specs=[f"{vol}:/data"],
|
||||||
|
user=f"{uid}:{gid}",
|
||||||
|
sleep_seconds=5,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
_docker_volume_rm(vol)
|
||||||
|
|
||||||
|
_assert_contains(result, f"NetAlertX note: current UID {uid} GID {gid}", result.args)
|
||||||
|
assert "expected UID" in result.output
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
def _run_container(
|
def _run_container(
|
||||||
label: str,
|
label: str,
|
||||||
volumes: list[tuple[str, str, bool]] | None = None,
|
volumes: list[tuple[str, str, bool]] | None = None,
|
||||||
@@ -272,34 +450,64 @@ def _run_container(
|
|||||||
volume_specs: list[str] | None = None,
|
volume_specs: list[str] | None = None,
|
||||||
sleep_seconds: float = GRACE_SECONDS,
|
sleep_seconds: float = GRACE_SECONDS,
|
||||||
wait_for_exit: bool = False,
|
wait_for_exit: bool = False,
|
||||||
|
pre_entrypoint: str | None = None,
|
||||||
|
userns_mode: str | None = "host",
|
||||||
|
image: str = IMAGE,
|
||||||
) -> subprocess.CompletedProcess[str]:
|
) -> subprocess.CompletedProcess[str]:
|
||||||
name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower()
|
name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower()
|
||||||
|
|
||||||
|
tmp_uid = 20211
|
||||||
|
tmp_gid = 20211
|
||||||
|
if user:
|
||||||
|
try:
|
||||||
|
u_str, g_str = user.split(":", 1)
|
||||||
|
tmp_uid = int(u_str)
|
||||||
|
tmp_gid = int(g_str)
|
||||||
|
except Exception:
|
||||||
|
# Keep defaults if user format is unexpected.
|
||||||
|
tmp_uid = 20211
|
||||||
|
tmp_gid = 20211
|
||||||
|
|
||||||
# Clean up any existing container with this name
|
# Clean up any existing container with this name
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["docker", "rm", "-f", name],
|
["docker", "rm", "-f", name],
|
||||||
check=False,
|
check=False,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd: list[str] = ["docker", "run", "--rm", "--name", name]
|
cmd: list[str] = ["docker", "run", "--rm", "--name", name]
|
||||||
|
|
||||||
|
# Avoid flakiness in host-network runs when the host already uses the
|
||||||
|
# default NetAlertX ports. Tests can still override explicitly via `env`.
|
||||||
|
effective_env: dict[str, str] = dict(env or {})
|
||||||
|
if network_mode == "host":
|
||||||
|
if "PORT" not in effective_env:
|
||||||
|
effective_env["PORT"] = str(30000 + (int(uuid.uuid4().hex[:4], 16) % 20000))
|
||||||
|
if "GRAPHQL_PORT" not in effective_env:
|
||||||
|
gql = 30000 + (int(uuid.uuid4().hex[4:8], 16) % 20000)
|
||||||
|
if str(gql) == effective_env["PORT"]:
|
||||||
|
gql = 30000 + ((gql + 1) % 20000)
|
||||||
|
effective_env["GRAPHQL_PORT"] = str(gql)
|
||||||
|
|
||||||
if network_mode:
|
if network_mode:
|
||||||
cmd.extend(["--network", network_mode])
|
cmd.extend(["--network", network_mode])
|
||||||
cmd.extend(["--userns", "host"])
|
if userns_mode:
|
||||||
# Add default ramdisk to /tmp with permissions 777
|
cmd.extend(["--userns", userns_mode])
|
||||||
cmd.extend(["--tmpfs", "/tmp:mode=777"])
|
# Match docker-compose UX: /tmp is tmpfs with 1700 and owned by the runtime UID/GID.
|
||||||
|
cmd.extend(["--tmpfs", f"/tmp:mode=1700,uid={tmp_uid},gid={tmp_gid}"])
|
||||||
if user:
|
if user:
|
||||||
cmd.extend(["--user", user])
|
cmd.extend(["--user", user])
|
||||||
if drop_caps:
|
if drop_caps is not None:
|
||||||
for cap in drop_caps:
|
for cap in drop_caps:
|
||||||
cmd.extend(["--cap-drop", cap])
|
cmd.extend(["--cap-drop", cap])
|
||||||
else:
|
else:
|
||||||
|
cmd.extend(["--cap-drop", "ALL"])
|
||||||
for cap in DEFAULT_CAPS:
|
for cap in DEFAULT_CAPS:
|
||||||
cmd.extend(["--cap-add", cap])
|
cmd.extend(["--cap-add", cap])
|
||||||
if env:
|
if effective_env:
|
||||||
for key, value in env.items():
|
for key, value in effective_env.items():
|
||||||
cmd.extend(["-e", f"{key}={value}"])
|
cmd.extend(["-e", f"{key}={value}"])
|
||||||
if extra_args:
|
if extra_args:
|
||||||
cmd.extend(extra_args)
|
cmd.extend(extra_args)
|
||||||
@@ -323,17 +531,24 @@ def _run_container(
|
|||||||
mounts_ls += f" {target}"
|
mounts_ls += f" {target}"
|
||||||
mounts_ls += " || true; echo '--- END MOUNTS ---'; \n"
|
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:
|
if wait_for_exit:
|
||||||
script = mounts_ls + "sh /entrypoint.sh"
|
script = mounts_ls + setup_script + "sh /entrypoint.sh"
|
||||||
else:
|
else:
|
||||||
script = "".join([
|
script = "".join([
|
||||||
mounts_ls,
|
mounts_ls,
|
||||||
|
setup_script,
|
||||||
"sh /entrypoint.sh & pid=$!; ",
|
"sh /entrypoint.sh & pid=$!; ",
|
||||||
f"sleep {sleep_seconds}; ",
|
f"sleep {sleep_seconds}; ",
|
||||||
"if kill -0 $pid >/dev/null 2>&1; then kill -TERM $pid >/dev/null 2>&1 || true; fi; ",
|
"if kill -0 $pid >/dev/null 2>&1; then kill -TERM $pid >/dev/null 2>&1 || true; fi; ",
|
||||||
"wait $pid; code=$?; if [ $code -eq 143 ]; then exit 0; fi; exit $code"
|
"wait $pid; code=$?; if [ $code -eq 143 ]; then exit 0; fi; exit $code"
|
||||||
])
|
])
|
||||||
cmd.extend(["--entrypoint", "/bin/sh", IMAGE, "-c", script])
|
cmd.extend(["--entrypoint", "/bin/sh", image, "-c", script])
|
||||||
|
|
||||||
# Print the full Docker command for debugging
|
# Print the full Docker command for debugging
|
||||||
print("\n--- DOCKER CMD ---\n", " ".join(cmd), "\n--- END CMD ---\n")
|
print("\n--- DOCKER CMD ---\n", " ".join(cmd), "\n--- END CMD ---\n")
|
||||||
@@ -342,7 +557,7 @@ def _run_container(
|
|||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=sleep_seconds + 30,
|
timeout=max(SUBPROCESS_TIMEOUT_SECONDS, sleep_seconds + 30),
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
# Combine and clean stdout and stderr
|
# Combine and clean stdout and stderr
|
||||||
@@ -509,23 +724,17 @@ 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..."
|
||||||
"""
|
"""
|
||||||
base = tmp_path / "missing_host_net_base"
|
vol = _fresh_named_volume("missing_host_network")
|
||||||
paths = _setup_fixed_mount_tree(base)
|
try:
|
||||||
# Ensure directories are writable and owned by netalertx user so container can operate
|
result = _run_container(
|
||||||
for key in ["data", "app_db", "app_config"]:
|
"missing-host-network",
|
||||||
paths[key].chmod(0o777)
|
volumes=None,
|
||||||
_chown_netalertx(paths[key])
|
volume_specs=[f"{vol}:/data"],
|
||||||
# Create a config file so the writable check passes
|
network_mode=None,
|
||||||
config_file = paths["app_config"] / "app.conf"
|
sleep_seconds=15,
|
||||||
config_file.write_text("test config")
|
)
|
||||||
config_file.chmod(0o666)
|
finally:
|
||||||
_chown_netalertx(config_file)
|
_docker_volume_rm(vol)
|
||||||
volumes = _build_volume_args_for_keys(paths, {"data"})
|
|
||||||
result = _run_container(
|
|
||||||
"missing-host-network",
|
|
||||||
volumes,
|
|
||||||
network_mode=None,
|
|
||||||
)
|
|
||||||
_assert_contains(result, "not running with --network=host", result.args)
|
_assert_contains(result, "not running with --network=host", result.args)
|
||||||
|
|
||||||
|
|
||||||
@@ -536,146 +745,146 @@ def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
|
|||||||
# top level.
|
# 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
|
def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None:
|
||||||
of netalertx user. Permission errors due to incorrect user context.
|
"""Test missing configuration file seeding - simulates corrupted/missing app.conf.
|
||||||
Expected: Permission errors, guidance to use correct user.
|
|
||||||
|
|
||||||
Check script: /entrypoint.d/60-user-netalertx.sh
|
9. Missing Configuration File: Simulates corrupted/missing app.conf.
|
||||||
Sample message: "⚠️ ATTENTION: NetAlertX is running as UID 1000:1000. Hardened permissions..."
|
Container automatically regenerates default configuration on startup.
|
||||||
"""
|
Expected: Automatic regeneration of default configuration.
|
||||||
paths = _setup_mount_tree(tmp_path, "run_as_1000")
|
|
||||||
volumes = _build_volume_args_for_keys(paths, {"data"})
|
Check script: /entrypoint.d/15-first-run-config.sh
|
||||||
|
Sample message: "Default configuration written to"
|
||||||
|
"""
|
||||||
|
vol = _fresh_named_volume("missing_app_conf")
|
||||||
|
try:
|
||||||
result = _run_container(
|
result = _run_container(
|
||||||
"run-as-1000",
|
"missing-app-conf",
|
||||||
volumes,
|
volumes=None,
|
||||||
user="1000:1000",
|
volume_specs=[f"{vol}:/data"],
|
||||||
|
sleep_seconds=15,
|
||||||
)
|
)
|
||||||
_assert_contains(result, "NetAlertX is running as UID 1000:1000", result.args)
|
finally:
|
||||||
|
_docker_volume_rm(vol)
|
||||||
|
_assert_contains(result, "Default configuration written to", result.args)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None:
|
|
||||||
"""Test missing configuration file seeding - simulates corrupted/missing app.conf.
|
|
||||||
|
|
||||||
9. Missing Configuration File: Simulates corrupted/missing app.conf.
|
def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None:
|
||||||
Container automatically regenerates default configuration on startup.
|
"""Test missing database file seeding - simulates corrupted/missing app.db.
|
||||||
Expected: Automatic regeneration of default configuration.
|
|
||||||
|
|
||||||
Check script: /entrypoint.d/15-first-run-config.sh
|
10. Missing Database File: Simulates corrupted/missing app.db.
|
||||||
Sample message: "Default configuration written to"
|
Container automatically creates initial database schema on startup.
|
||||||
"""
|
Expected: Automatic creation of initial database schema.
|
||||||
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:
|
Check script: /entrypoint.d/20-first-run-db.sh
|
||||||
"""Test missing database file seeding - simulates corrupted/missing app.db.
|
Sample message: "Building initial database schema"
|
||||||
|
"""
|
||||||
10. Missing Database File: Simulates corrupted/missing app.db.
|
vol = _fresh_named_volume("missing_app_db")
|
||||||
Container automatically creates initial database schema on startup.
|
try:
|
||||||
Expected: Automatic creation of initial database schema.
|
_ensure_volume_copy_up(vol)
|
||||||
|
# Seed only app.conf; leave app.db missing to trigger first-run DB schema creation.
|
||||||
Check script: /entrypoint.d/20-first-run-db.sh
|
_seed_volume_text_file(
|
||||||
Sample message: "Building initial database schema"
|
vol,
|
||||||
"""
|
"/data/config/app.conf",
|
||||||
base = tmp_path / "missing_app_db_base"
|
"TIMEZONE='UTC'\n",
|
||||||
paths = _setup_fixed_mount_tree(base)
|
chmod_mode="644",
|
||||||
_chown_netalertx(paths["app_db"])
|
user="20211:20211",
|
||||||
(paths["app_db"] / "testfile.txt").write_text("test")
|
)
|
||||||
volumes = _build_volume_args_for_keys(paths, {"data"})
|
|
||||||
result = _run_container(
|
result = _run_container(
|
||||||
"missing-app-db",
|
"missing-app-db",
|
||||||
volumes,
|
volumes=None,
|
||||||
|
volume_specs=[f"{vol}:/data"],
|
||||||
user="20211:20211",
|
user="20211:20211",
|
||||||
sleep_seconds=5,
|
sleep_seconds=20,
|
||||||
wait_for_exit=True,
|
|
||||||
)
|
)
|
||||||
_assert_contains(result, "Building initial database schema", result.args)
|
assert _volume_has_file(vol, "/data/db/app.db")
|
||||||
assert result.returncode != 0
|
finally:
|
||||||
|
_docker_volume_rm(vol)
|
||||||
|
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
|
def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None:
|
||||||
without mounting nginx config. Container starts but uses default address.
|
"""Test custom port configuration without writable nginx config mount.
|
||||||
Expected: Container starts but uses default address, warning about missing config mount.
|
|
||||||
|
|
||||||
Check script: check-nginx-config.sh
|
4. Custom Port Without Nginx Config Mount: Simulates setting custom LISTEN_ADDR/PORT
|
||||||
Sample messages: "⚠️ ATTENTION: Nginx configuration mount /tmp/nginx/active-config is missing."
|
without mounting nginx config. Container starts but uses default address.
|
||||||
"⚠️ ATTENTION: Unable to write to /tmp/nginx/active-config/netalertx.conf."
|
Expected: Container starts but uses default address, warning about missing config mount.
|
||||||
"""
|
|
||||||
paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf")
|
Check script: check-nginx-config.sh
|
||||||
for key in ["app_db", "app_config", "app_log", "app_api", "services_run"]:
|
Sample messages: "⚠️ ATTENTION: Nginx configuration mount /tmp/nginx/active-config is missing."
|
||||||
paths[key].chmod(0o777)
|
"⚠️ ATTENTION: Unable to write to /tmp/nginx/active-config/netalertx.conf."
|
||||||
paths["nginx_conf"].chmod(0o500)
|
"""
|
||||||
volumes = _build_volume_args_for_keys(
|
vol = _fresh_named_volume("custom_port_ro_conf")
|
||||||
paths,
|
extra_args = [
|
||||||
{"data", "app_log", "app_api", "services_run", "nginx_conf"},
|
"--tmpfs",
|
||||||
|
f"{VOLUME_MAP['nginx_conf']}:uid=20211,gid=20211,mode=500",
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
result = _run_container(
|
||||||
|
"custom-port-ro-conf",
|
||||||
|
volumes=None,
|
||||||
|
volume_specs=[f"{vol}:/data"],
|
||||||
|
env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"},
|
||||||
|
user="20211:20211",
|
||||||
|
extra_args=extra_args,
|
||||||
|
sleep_seconds=15,
|
||||||
)
|
)
|
||||||
try:
|
finally:
|
||||||
result = _run_container(
|
_docker_volume_rm(vol)
|
||||||
"custom-port-ro-conf",
|
_assert_contains(result, "Unable to write to", result.args)
|
||||||
volumes,
|
_assert_contains(
|
||||||
env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"},
|
result, f"{VOLUME_MAP['nginx_conf']}/netalertx.conf", result.args
|
||||||
user="20211:20211",
|
)
|
||||||
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:
|
|
||||||
paths["nginx_conf"].chmod(0o755)
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
11. Excessive Capabilities: Simulates container with capabilities beyond the required
|
11. Excessive Capabilities: Simulates container with capabilities beyond the required
|
||||||
NET_ADMIN, NET_RAW, and NET_BIND_SERVICE.
|
NET_ADMIN, NET_RAW, and NET_BIND_SERVICE.
|
||||||
Expected: Warning about excessive capabilities detected.
|
Expected: Warning about excessive capabilities detected.
|
||||||
|
|
||||||
Check script: 90-excessive-capabilities.sh
|
Check script: 90-excessive-capabilities.sh
|
||||||
Sample message: "Excessive capabilities detected"
|
Sample message: "Excessive capabilities detected"
|
||||||
"""
|
"""
|
||||||
paths = _setup_mount_tree(tmp_path, "excessive_caps")
|
vol = _fresh_named_volume("excessive_caps")
|
||||||
volumes = _build_volume_args_for_keys(paths, {"data"})
|
try:
|
||||||
result = _run_container(
|
result = _run_container(
|
||||||
"excessive-caps",
|
"excessive-caps",
|
||||||
volumes,
|
volumes=None,
|
||||||
|
volume_specs=[f"{vol}:/data"],
|
||||||
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=15,
|
||||||
)
|
)
|
||||||
_assert_contains(result, "Excessive capabilities detected", result.args)
|
finally:
|
||||||
_assert_contains(result, "bounding caps:", result.args)
|
_docker_volume_rm(vol)
|
||||||
|
_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:
|
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.
|
||||||
|
|
||||||
12. Appliance Integrity: Simulates running container with read-write root filesystem
|
12. Appliance Integrity: Simulates running container with read-write root filesystem
|
||||||
instead of read-only mode.
|
instead of read-only mode.
|
||||||
Expected: Warning about running in read-write mode instead of read-only.
|
Expected: Warning about running in read-write mode instead of read-only.
|
||||||
|
|
||||||
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"
|
||||||
"""
|
"""
|
||||||
paths = _setup_mount_tree(tmp_path, "appliance_integrity")
|
vol = _fresh_named_volume("appliance_integrity")
|
||||||
volumes = _build_volume_args_for_keys(paths, {"data"})
|
try:
|
||||||
result = _run_container("appliance-integrity", volumes, sleep_seconds=5)
|
result = _run_container(
|
||||||
_assert_contains(
|
"appliance-integrity",
|
||||||
result, "Container is running as read-write, not in read-only mode", result.args
|
volumes=None,
|
||||||
|
volume_specs=[f"{vol}:/data"],
|
||||||
|
sleep_seconds=15,
|
||||||
)
|
)
|
||||||
_assert_contains(result, "read-only: true", result.args)
|
finally:
|
||||||
|
_docker_volume_rm(vol)
|
||||||
|
_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:
|
def test_zero_permissions_app_db_dir(tmp_path: pathlib.Path) -> None:
|
||||||
@@ -769,19 +978,26 @@ def test_mandatory_folders_creation(tmp_path: pathlib.Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_writable_config_validation(tmp_path: pathlib.Path) -> None:
|
def test_writable_config_validation(tmp_path: pathlib.Path) -> None:
|
||||||
"""Test writable config validation - simulates read-only config file.
|
"""Test writable config validation - simulates invalid config file type.
|
||||||
|
|
||||||
3. Writable Config Validation: Simulates config file with read-only permissions.
|
3. Writable Config Validation: Simulates app.conf being a non-regular file (directory).
|
||||||
Container verifies it can read from and write to critical config and database files.
|
Container verifies it can read from and write to critical config and database files.
|
||||||
Expected: "Read permission denied" warning for config file.
|
Expected: "Path is not a regular file" warning for config file.
|
||||||
|
|
||||||
Check script: 30-writable-config.sh
|
Check script: 35-writable-config.sh
|
||||||
Sample message: "Read permission denied"
|
Sample message: "Path is not a regular file"
|
||||||
"""
|
"""
|
||||||
paths = _setup_mount_tree(tmp_path, "writable_config")
|
paths = _setup_mount_tree(tmp_path, "writable_config")
|
||||||
# Make config file read-only but keep directories writable so container gets past mounts.py
|
# Force a non-regular file for /data/config/app.conf to exercise the correct warning branch.
|
||||||
config_file = paths["app_config"] / "app.conf"
|
config_path = paths["app_config"] / "app.conf"
|
||||||
config_file.chmod(0o400) # Read-only for owner
|
if config_path.exists():
|
||||||
|
if config_path.is_dir():
|
||||||
|
shutil.rmtree(config_path)
|
||||||
|
else:
|
||||||
|
config_path.unlink()
|
||||||
|
config_path.mkdir(parents=False)
|
||||||
|
config_path.chmod(0o777)
|
||||||
|
_chown_netalertx(config_path)
|
||||||
|
|
||||||
# Ensure directories are writable and owned by netalertx user so container gets past mounts.py
|
# Ensure directories are writable and owned by netalertx user so container gets past mounts.py
|
||||||
for key in [
|
for key in [
|
||||||
@@ -799,7 +1015,7 @@ def test_writable_config_validation(tmp_path: pathlib.Path) -> None:
|
|||||||
result = _run_container(
|
result = _run_container(
|
||||||
"writable-config", volumes, user="20211:20211", sleep_seconds=5.0
|
"writable-config", volumes, user="20211:20211", sleep_seconds=5.0
|
||||||
)
|
)
|
||||||
_assert_contains(result, "Read permission denied", result.args)
|
_assert_contains(result, "ATTENTION: Path is not a regular file.", result.args)
|
||||||
|
|
||||||
|
|
||||||
def test_mount_analysis_ram_disk_performance(tmp_path: pathlib.Path) -> None:
|
def test_mount_analysis_ram_disk_performance(tmp_path: pathlib.Path) -> None:
|
||||||
@@ -904,3 +1120,92 @@ def test_mount_analysis_dataloss_risk(tmp_path: pathlib.Path) -> None:
|
|||||||
# Check that configuration issues are detected due to dataloss risk
|
# Check that configuration issues are detected due to dataloss risk
|
||||||
_assert_contains(result, "Configuration issues detected", result.args)
|
_assert_contains(result, "Configuration issues detected", result.args)
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
|
||||||
|
"""Test handling of restrictive permissions on bind mounts.
|
||||||
|
|
||||||
|
Simulates a user mounting a directory with restrictive permissions (e.g., 755 root:root).
|
||||||
|
The container should either fail gracefully or handle it if running as root (which triggers fix).
|
||||||
|
If running as non-root (default), it should fail to write if it doesn't have access.
|
||||||
|
"""
|
||||||
|
paths = _setup_mount_tree(tmp_path, "restrictive_perms")
|
||||||
|
|
||||||
|
# Helper to chown without userns host (workaround for potential devcontainer hang)
|
||||||
|
def _chown_root_safe(host_path: pathlib.Path) -> None:
|
||||||
|
cmd = [
|
||||||
|
"docker", "run", "--rm",
|
||||||
|
# "--userns", "host", # Removed to avoid hang
|
||||||
|
"--user", "0:0",
|
||||||
|
"--entrypoint", "/bin/chown",
|
||||||
|
"-v", f"{host_path}:/mnt",
|
||||||
|
IMAGE,
|
||||||
|
"-R", "0:0", "/mnt",
|
||||||
|
]
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up a restrictive directory (root owned, 755)
|
||||||
|
target_dir = paths["app_db"]
|
||||||
|
_chown_root_safe(target_dir)
|
||||||
|
target_dir.chmod(0o755)
|
||||||
|
|
||||||
|
# Mount ALL volumes to avoid 'find' errors in 0-storage-permission.sh
|
||||||
|
keys = {"data", "app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"}
|
||||||
|
volumes = _build_volume_args_for_keys(paths, keys)
|
||||||
|
|
||||||
|
# Case 1: Running as non-root (default) - Should fail to write
|
||||||
|
# We disable host network/userns to avoid potential hangs in devcontainer environment
|
||||||
|
result = _run_container(
|
||||||
|
"restrictive-perms-user",
|
||||||
|
volumes,
|
||||||
|
user="20211:20211",
|
||||||
|
sleep_seconds=5,
|
||||||
|
network_mode=None,
|
||||||
|
userns_mode=None
|
||||||
|
)
|
||||||
|
assert result.returncode != 0 or "Permission denied" in result.output or "Unable to write" in result.output
|
||||||
|
|
||||||
|
# Case 2: Running as root - Should trigger the fix script
|
||||||
|
result_root = _run_container(
|
||||||
|
"restrictive-perms-root",
|
||||||
|
volumes,
|
||||||
|
user="0:0",
|
||||||
|
sleep_seconds=5,
|
||||||
|
network_mode=None,
|
||||||
|
userns_mode=None
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_contains(result_root, "NetAlertX is running as ROOT", result_root.args)
|
||||||
|
_assert_contains(result_root, "Permissions fixed for read-write paths", result_root.args)
|
||||||
|
|
||||||
|
check_cmd = [
|
||||||
|
"docker", "run", "--rm",
|
||||||
|
"--entrypoint", "/bin/sh",
|
||||||
|
"--user", "20211:20211",
|
||||||
|
IMAGE,
|
||||||
|
"-c", "ls -ldn /data/db && touch /data/db/test_write_after_fix"
|
||||||
|
]
|
||||||
|
# Add all volumes to check_cmd too
|
||||||
|
for host_path, target, _readonly in volumes:
|
||||||
|
check_cmd.extend(["-v", f"{host_path}:{target}"])
|
||||||
|
|
||||||
|
check_result = subprocess.run(
|
||||||
|
check_cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
if check_result.returncode != 0:
|
||||||
|
print(f"Check command failed. Cmd: {check_cmd}")
|
||||||
|
print(f"Stderr: {check_result.stderr}")
|
||||||
|
print(f"Stdout: {check_result.stdout}")
|
||||||
|
|
||||||
|
assert check_result.returncode == 0, f"Should be able to write after root fix script runs. Stderr: {check_result.stderr}. Stdout: {check_result.stdout}"
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ def test_skip_tests_env_var():
|
|||||||
@pytest.mark.feature_complete
|
@pytest.mark.feature_complete
|
||||||
def test_app_conf_override_from_graphql_port():
|
def test_app_conf_override_from_graphql_port():
|
||||||
# If GRAPHQL_PORT is set and APP_CONF_OVERRIDE is not set, the entrypoint should set
|
# If GRAPHQL_PORT is set and APP_CONF_OVERRIDE is not set, the entrypoint should set
|
||||||
# APP_CONF_OVERRIDE to a JSON string containing the GRAPHQL_PORT value and print a message
|
# APP_CONF_OVERRIDE to a JSON string containing the GRAPHQL_PORT value.
|
||||||
# about it.
|
|
||||||
# The script should exit successfully.
|
# The script should exit successfully.
|
||||||
result = _run_entrypoint(env={"GRAPHQL_PORT": "20212", "SKIP_TESTS": "1"}, check_only=True)
|
result = _run_entrypoint(env={"GRAPHQL_PORT": "20212", "SKIP_TESTS": "1"}, check_only=True)
|
||||||
assert 'Setting APP_CONF_OVERRIDE to {"GRAPHQL_PORT":"20212"}' in result.stdout
|
assert 'Setting APP_CONF_OVERRIDE to' not in result.stdout
|
||||||
|
assert 'APP_CONF_OVERRIDE detected' in result.stderr
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ Pytest-based Mount Diagnostic Tests for NetAlertX
|
|||||||
Tests all possible mount configurations for each path to validate the diagnostic tool.
|
Tests all possible mount configurations for each path to validate the diagnostic tool.
|
||||||
Uses pytest framework for proper test discovery and execution.
|
Uses pytest framework for proper test discovery and execution.
|
||||||
|
|
||||||
|
TODO: Future Robustness & Compatibility Tests
|
||||||
|
1. Symlink Attacks: Verify behavior when a writable directory is mounted via a symlink.
|
||||||
|
Hypothesis: The tool might misidentify the mount status or path.
|
||||||
|
2. OverlayFS/Copy-up Scenarios: Investigate behavior on filesystems like Synology's OverlayFS.
|
||||||
|
Hypothesis: Files might appear writable but fail on specific operations (locking, mmap).
|
||||||
|
3. Text-based Output: Refactor output to support text-based status (e.g., [OK], [FAIL])
|
||||||
|
instead of emojis for better compatibility with terminals that don't support unicode.
|
||||||
|
|
||||||
All tests use the mounts table. For reference, the mounts table looks like this:
|
All tests use the mounts table. For reference, the mounts table looks like this:
|
||||||
|
|
||||||
Path | Writeable | Mount | RAMDisk | Performance | DataLoss
|
Path | Writeable | Mount | RAMDisk | Performance | DataLoss
|
||||||
@@ -604,3 +612,4 @@ def test_table_parsing():
|
|||||||
performance=True,
|
performance=True,
|
||||||
dataloss=True,
|
dataloss=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user