Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 48 additions & 25 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
# The NetAlertX Dockerfile has 3 stages:
#
# Stage 1. Builder - NetAlertX Requires special tools and packages to build our virtual environment, but
# which are not needed in future stages. We build the builder and extract the venv for runner to use as
# which are not needed in future stages. We build the builder and extract the venv for runner to use as
# a base.
#
# Stage 2. Runner builds the bare minimum requirements to create an operational NetAlertX. The primary
# reason for breaking at this stage is it leaves the system in a proper state for devcontainer operation
# This image also provides a break-out point for uses who wish to execute the anti-pattern of using a
# This image also provides a break-out point for uses who wish to execute the anti-pattern of using a
# docker container as a VM for experimentation and various development patterns.
#
# Stage 3. Hardened removes root, sudoers, folders, permissions, and locks the system down into a read-only
# compatible image. While NetAlertX does require some read-write operations, this image can guarantee the
# compatible image. While NetAlertX does require some read-write operations, this image can guarantee the
# code pushed out by the project is the only code which will run on the system after each container restart.
# It reduces the chance of system hijacking and operates with all modern security protocols in place as is
# expected from a security appliance.
Expand All @@ -29,20 +29,36 @@ ENV PATH="/opt/venv/bin:$PATH"

# Install build dependencies
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

# Create virtual environment owned by root, but readable by everyone else. This makes it easy to copy
# into hardened stage without worrying about permissions and keeps image size small. Keeping the commands
# together makes for a slightly smaller image size.
RUN pip install --no-cache-dir -r /tmp/requirements.txt && \
# Upgrade pip/wheel/setuptools and install Python packages
RUN python -m pip install --upgrade pip setuptools wheel && \
pip install --prefer-binary --no-cache-dir -r /tmp/requirements.txt && \
chmod -R u-rwx,g-rwx /opt

# second stage is the main runtime stage with just the minimum required to run the application
# The runner is used for both devcontainer, and as a base for the hardened stage.
FROM alpine:3.22 AS runner

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
ENV NETALERTX_APP=${INSTALL_DIR}
Expand Down Expand Up @@ -98,11 +114,11 @@ ENV READ_WRITE_FOLDERS="${NETALERTX_DATA} ${NETALERTX_CONFIG} ${NETALERTX_DB} ${
${SYSTEM_SERVICES_ACTIVE_CONFIG}"

#Python environment
ENV PYTHONUNBUFFERED=1
ENV PYTHONUNBUFFERED=1
ENV VIRTUAL_ENV=/opt/venv
ENV VIRTUAL_ENV_BIN=/opt/venv/bin
ENV PYTHONPATH=${NETALERTX_APP}:${NETALERTX_SERVER}:${NETALERTX_PLUGINS}:${VIRTUAL_ENV}/lib/python3.12/site-packages
ENV PATH="${SYSTEM_SERVICES}:${VIRTUAL_ENV_BIN}:$PATH"
ENV PATH="${SYSTEM_SERVICES}:${VIRTUAL_ENV_BIN}:$PATH"

# App Environment
ENV LISTEN_ADDR=0.0.0.0
Expand All @@ -113,7 +129,7 @@ ENV VENDORSPATH_NEWEST=${SYSTEM_SERVICES_RUN_TMP}/ieee-oui.txt
ENV ENVIRONMENT=alpine
ENV READ_ONLY_USER=readonly READ_ONLY_GROUP=readonly
ENV NETALERTX_USER=netalertx NETALERTX_GROUP=netalertx
ENV LANG=C.UTF-8
ENV LANG=C.UTF-8


RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap \
Expand All @@ -122,8 +138,8 @@ RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 i
nginx supercronic shadow && \
rm -Rf /var/cache/apk/* && \
rm -Rf /etc/nginx && \
addgroup -g 20211 ${NETALERTX_GROUP} && \
adduser -u 20211 -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
addgroup -g ${NETALERTX_GID} ${NETALERTX_GROUP} && \
adduser -u ${NETALERTX_UID} -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
apk del shadow


Expand All @@ -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 --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 --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
# Copy the virtualenv from the builder stage (owned by readonly lock owner)
COPY --from=builder --chown=${READONLY_UID}:${READONLY_GID} ${VIRTUAL_ENV} ${VIRTUAL_ENV}


# 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
# although it may be quicker to do it before the copy, it keeps the image
# layers smaller to do it after.
RUN if [ -f '.VERSION' ]; then \
cp '.VERSION' "${NETALERTX_APP}/.VERSION"; \
else \
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/.VERSION"; \
fi && \
chown 20212:20212 "${NETALERTX_APP}/.VERSION" && \
RUN for vfile in .VERSION .VERSION_PREV; do \
if [ ! -f "${NETALERTX_APP}/${vfile}" ]; then \
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/${vfile}"; \
fi; \
chown ${READONLY_UID}:${READONLY_GID} "${NETALERTX_APP}/${vfile}"; \
done && \
apk add --no-cache libcap && \
setcap cap_net_raw+ep /bin/busybox && \
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
Expand All @@ -179,15 +196,21 @@ ENTRYPOINT ["/bin/sh","/entrypoint.sh"]
# This stage is separate from Runner stage so that devcontainer can use the Runner stage.
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

# Create readonly user and group with no shell access.
# Readonly user marks folders that are created by NetAlertX, but should not be modified.
# 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 the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
adduser -u 20212 -G "${READ_ONLY_GROUP}" -D -h /app "${READ_ONLY_USER}"
RUN addgroup -g ${READONLY_GID} "${READ_ONLY_GROUP}" && \
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
Expand Down Expand Up @@ -249,7 +272,7 @@ USER root

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 \
docker-cli-compose shellcheck
docker-cli-compose shellcheck py3-psutil

# Install hadolint (Dockerfile linter)
RUN curl -L https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint && \
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/resources/devcontainer-Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ USER root

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 \
docker-cli-compose shellcheck
docker-cli-compose shellcheck py3-psutil

# Install hadolint (Dockerfile linter)
RUN curl -L https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 -o /usr/local/bin/hadolint && \
Expand Down
26 changes: 19 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ RUN python -m pip install --upgrade pip setuptools wheel && \
FROM alpine:3.22 AS runner

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
ENV NETALERTX_APP=${INSTALL_DIR}
Expand Down Expand Up @@ -129,8 +135,8 @@ RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 i
nginx supercronic shadow && \
rm -Rf /var/cache/apk/* && \
rm -Rf /etc/nginx && \
addgroup -g 20211 ${NETALERTX_GROUP} && \
adduser -u 20211 -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
addgroup -g ${NETALERTX_GID} ${NETALERTX_GROUP} && \
adduser -u ${NETALERTX_UID} -D -h ${NETALERTX_APP} -G ${NETALERTX_GROUP} ${NETALERTX_USER} && \
apk del shadow


Expand All @@ -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_PREV

# Copy the virtualenv from the builder stage
COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
# Copy the virtualenv from the builder stage (owned by readonly lock owner)
COPY --from=builder --chown=${READONLY_UID}:${READONLY_GID} ${VIRTUAL_ENV} ${VIRTUAL_ENV}


# Initialize each service with the dockerfiles/init-*.sh scripts, once.
Expand All @@ -162,7 +168,7 @@ RUN for vfile in .VERSION .VERSION_PREV; do \
if [ ! -f "${NETALERTX_APP}/${vfile}" ]; then \
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/${vfile}"; \
fi; \
chown 20212:20212 "${NETALERTX_APP}/${vfile}"; \
chown ${READONLY_UID}:${READONLY_GID} "${NETALERTX_APP}/${vfile}"; \
done && \
apk add --no-cache libcap && \
setcap cap_net_raw+ep /bin/busybox && \
Expand All @@ -187,15 +193,21 @@ ENTRYPOINT ["/bin/sh","/entrypoint.sh"]
# This stage is separate from Runner stage so that devcontainer can use the Runner stage.
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

# Create readonly user and group with no shell access.
# Readonly user marks folders that are created by NetAlertX, but should not be modified.
# 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 the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
adduser -u 20212 -G "${READ_ONLY_GROUP}" -D -h /app "${READ_ONLY_USER}"
RUN addgroup -g ${READONLY_GID} "${READ_ONLY_GROUP}" && \
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
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ services:
image: netalertx:latest
container_name: netalertx # The name when you docker contiainer ls
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
- ALL
cap_add: # Add only the necessary capabilities
Expand Down Expand Up @@ -49,7 +51,7 @@ services:
# uid=20211 and gid=20211 is the netalertx user inside the container
# mode=1700 gives rwx------ permissions to the netalertx user only
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:
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces
PORT: ${PORT:-20211} # Application port
Expand Down
15 changes: 9 additions & 6 deletions docs/DOCKER_COMPOSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,18 @@ services:
# - path/on/host/to/dhcp.file:/resources/dhcp.file

# tmpfs mount consolidates writable state for a read-only container and improves performance
# uid=20211 and gid=20211 is the netalertx user inside the container
# mode=1700 grants rwx------ permissions to the netalertx user only
# uid/gid default to the service user (NETALERTX_UID/GID, default 20211)
# mode=1700 grants rwx------ permissions to the runtime user only
tmpfs:
# Comment out to retain logs between container restarts - this has a server performance impact.
- "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
- "/tmp:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"

# Retain logs - comment out tmpfs /tmp if you want to retain logs between container restarts
# Please note if you remove the /tmp mount, you must create and maintain sub-folder mounts.
# - /path/on/host/log:/tmp/log
# - "/tmp/api:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# - "/tmp/nginx:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# - "/tmp/run:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# - "/tmp/api:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# - "/tmp/nginx:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
# - "/tmp/run:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"

environment:
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} # Listen for connections on all interfaces
Expand Down Expand Up @@ -94,6 +94,9 @@ Run or re-run it:
docker compose up --force-recreate
```

> [!TIP]
> Runtime UID/GID: The image ships with a service user `netalertx` (UID/GID 20211) and a readonly lock owner also at 20211 for 004/005 immutability. If you override the runtime user (compose `user:` or `NETALERTX_UID/GID` vars), ensure your `/data` volume and tmpfs mounts use matching `uid/gid` so startup checks and writable paths succeed.

### Customize with Environmental Variables

You can override the default settings by passing environmental variables to the `docker compose up` command.
Expand Down
7 changes: 5 additions & 2 deletions docs/DOCKER_INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ Head to [https://netalertx.com/](https://netalertx.com/) for more gifs and scree
docker run -d --rm --network=host \
-v /local_data_dir:/data \
-v /etc/localtime:/etc/localtime \
--tmpfs /tmp:uid=20211,gid=20211,mode=1700 \
--tmpfs /tmp:uid=${NETALERTX_UID:-20211},gid=${NETALERTX_GID:-20211},mode=1700 \
-e PORT=20211 \
-e APP_CONF_OVERRIDE={"GRAPHQL_PORT":"20214"} \
ghcr.io/jokob-sk/netalertx:latest
```

> Runtime UID/GID: The image defaults to a service user `netalertx` (UID/GID 20211). A separate readonly lock owner also uses UID/GID 20211 for 004/005 immutability. You can override the runtime UID/GID at build (ARG) or run (`--user` / compose `user:`) but must align writable mounts (`/data`, `/tmp*`) and tmpfs `uid/gid` to that choice.

See alternative [docked-compose examples](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md).

### Default ports
Expand Down Expand Up @@ -83,7 +85,8 @@ data
If you are facing permissions issues run the following commands on your server. This will change the owner and assure sufficient access to the database and config files that are stored in the `/local_data_dir/db` and `/local_data_dir/config` folders (replace `local_data_dir` with the location where your `/db` and `/config` folders are located).

```bash
sudo chown -R 20211:20211 /local_data_dir
# Use the runtime UID/GID you intend to run with (default 20211:20211)
sudo chown -R ${NETALERTX_UID:-20211}:${NETALERTX_GID:-20211} /local_data_dir
sudo chmod -R a+rwx /local_data_dir
```

Expand Down
25 changes: 14 additions & 11 deletions docs/docker-troubleshooting/incorrect-user.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@

## Issue Description

NetAlertX is running as UID:GID other than the expected 20211:20211. This bypasses hardened permissions, file ownership, and runtime isolation safeguards.
NetAlertX is running as a UID:GID that does not match the runtime service user configured for this container (default 20211:20211). Hardened ownership on writable paths may block writes if the UID/GID do not align with mounted volumes and tmpfs settings.

## Security Ramifications

The application is designed with security hardening that depends on running under a dedicated, non-privileged service account. Using a different user account can silently fail future upgrades and removes crucial isolation between the container and host system.
The image uses a dedicated service user for writes and a readonly lock owner (UID 20211) for code/venv with 004/005 permissions. Running as an arbitrary UID is supported, but only when writable mounts (`/data`, `/tmp/*`) are owned by that UID. Misalignment can cause startup failures or unexpected permission escalation attempts.

## Why You're Seeing This Issue

This occurs when you override the container's default user with custom `user:` directives in docker-compose.yml or `--user` flags in docker run commands. The container expects to run as the netalertx user for proper security isolation.
- A `user:` override in docker-compose.yml or `--user` flag on `docker run` changes the runtime UID/GID without updating mount ownership.
- Tmpfs mounts still use `uid=20211,gid=20211` while the container runs as another UID.
- Host bind mounts (e.g., `/data`) are owned by a different UID.

## How to Correct the Issue

Restore the container to the default user:
Option A: Use defaults (recommended)
- Remove custom `user:` overrides and `--user` flags.
- Let the container run as the built-in service user (UID/GID 20211) and keep tmpfs at `uid=20211,gid=20211`.

- Remove any `user:` overrides from docker-compose.yml
- Avoid `--user` flags in docker run commands
- Allow the container to run with its default UID:GID 20211:20211
- Recreate the container so volume ownership is reset automatically
Option B: Run with a custom UID/GID
- Set `user:` (or `NETALERTX_UID/NETALERTX_GID`) to your desired UID/GID.
- Align mounts: ensure `/data` (and any `/tmp/*` tmpfs) use the same `uid=`/`gid=` and that host bind mounts are chowned to that UID/GID.
- Recreate the container so ownership is consistent.

## Additional Resources

Docker Compose setup can be complex. We recommend starting with the default docker-compose.yml as a base and modifying it incrementally.

For detailed Docker Compose configuration guidance, see: [DOCKER_COMPOSE.md](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md)
- 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)
Loading
Loading