From 1e7d6c89413bc28b446763948588ec2c8eb27bfa Mon Sep 17 00:00:00 2001 From: Renn F Date: Wed, 21 Oct 2026 02:44:54 +0200 Subject: [PATCH 1/2] fix(docker): run the test image as a non-root user --- Dockerfile | 31 ++++++++++++++++--------------- compose.yml | 2 ++ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4aab3225..3e5f9c68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,34 @@ ARG PYTHON_VERSION=3.10 FROM python:${PYTHON_VERSION}-slim -RUN apt-get update && apt-get install -y \ - libsodium23 \ - wget \ - build-essential \ - libffi-dev \ - curl +COPY --from=ghcr.io/astral-sh/uv:0.13.4 /uv /uvx /bin/ + +RUN useradd --create-home --uid 10001 app \ + && mkdir /app \ + && chown app:app /app WORKDIR /app -ENV PIP_NO_CACHE_DIR=false \ - PIP_DISABLE_PIP_VERSION_CHECK=on \ +ENV PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DEFAULT_TIMEOUT=10 \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 -# Install uv -RUN pip install uv +USER 10001 -COPY pyproject.toml uv.lock* README.md /app/ +COPY --chown=app:app pyproject.toml uv.lock* README.md /app/ RUN uv sync --extra dev --frozen ENV PATH="/app/.venv/bin:$PATH" -COPY guard/ /app/guard/ -COPY tests/ /app/tests/ -COPY examples/ /app/examples/ -COPY docs/ /app/docs/ +COPY --chown=app:app guard/ /app/guard/ +COPY --chown=app:app tests/ /app/tests/ +COPY --chown=app:app examples/ /app/examples/ +COPY --chown=app:app docs/ /app/docs/ RUN mkdir -p /app/data/ipinfo + +# Probes the app service on port 8000. Any HTTP answer counts as alive; a refused connection or a timeout fails. +HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \ + CMD ["python", "-c", "import urllib.request, urllib.error\ntry:\n urllib.request.urlopen('http://127.0.0.1:8000/', timeout=3)\nexcept urllib.error.HTTPError:\n pass"] diff --git a/compose.yml b/compose.yml index 242a296c..17423bb3 100644 --- a/compose.yml +++ b/compose.yml @@ -27,6 +27,8 @@ services: PYTHON_VERSION: ${PYTHON_VERSION:-3.10} image: fastapi-guard-py${PYTHON_VERSION:-3.10} command: pytest -v --cov=. + healthcheck: + disable: true environment: - REDIS_URL=redis://redis:6379 - REDIS_PREFIX=${REDIS_PREFIX:-"test:fastapi_guard:"} From ccf09d686ed31cb1912af619cbdba9112a899665 Mon Sep 17 00:00:00 2001 From: Renn F Date: Wed, 21 Oct 2026 03:47:05 +0200 Subject: [PATCH 2/2] fix(docker): run the example and smoke images as a non-root user --- .github/workflows/docksec.yml | 2 ++ .github/workflows/live-smoke.yml | 4 ++++ examples/advanced_app/Dockerfile | 8 ++++++-- examples/simple_app/Dockerfile | 17 ++++++++++------- tests/live_smoke/stack/Dockerfile | 8 ++++++++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docksec.yml b/.github/workflows/docksec.yml index 3b4a2d35..6682e31f 100644 --- a/.github/workflows/docksec.yml +++ b/.github/workflows/docksec.yml @@ -5,10 +5,12 @@ on: branches: [master] paths: - 'Dockerfile' + - '**/Dockerfile' - '.github/workflows/docksec.yml' pull_request: paths: - 'Dockerfile' + - '**/Dockerfile' - '.github/workflows/docksec.yml' schedule: - cron: '0 6 * * 1' diff --git a/.github/workflows/live-smoke.yml b/.github/workflows/live-smoke.yml index 361c2146..ae7d82fa 100644 --- a/.github/workflows/live-smoke.yml +++ b/.github/workflows/live-smoke.yml @@ -43,6 +43,10 @@ jobs: run: | uv run python tests/live_smoke/patch_example_config.py + - name: Let the non-root container user write the scenario data + run: | + chmod a+rwx tests/live_smoke/stack/scenario_data + - name: Bring the live-smoke stack up run: | cd tests/live_smoke/stack diff --git a/examples/advanced_app/Dockerfile b/examples/advanced_app/Dockerfile index 2279a3f0..e36b7d55 100644 --- a/examples/advanced_app/Dockerfile +++ b/examples/advanced_app/Dockerfile @@ -18,7 +18,7 @@ RUN uv venv .venv && \ FROM python:3.12-slim RUN groupadd --system guard && \ - useradd --system --gid guard --create-home guard + useradd --system --uid 10001 --gid guard --create-home guard WORKDIR /app @@ -31,8 +31,12 @@ ENV PATH="/app/.venv/bin:$PATH" \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 -USER guard +USER 10001 EXPOSE 8000 +# Same probe as the compose services: /health must answer 2xx. +HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \ + CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)"] + CMD ["gunicorn", "-c", "gunicorn.conf.py", "app.main:app"] diff --git a/examples/simple_app/Dockerfile b/examples/simple_app/Dockerfile index 743eb8c4..e503b53a 100644 --- a/examples/simple_app/Dockerfile +++ b/examples/simple_app/Dockerfile @@ -1,12 +1,9 @@ ARG PYTHON_VERSION=3.10 FROM python:${PYTHON_VERSION}-slim -RUN apt-get update && apt-get install -y \ - libsodium23 \ - wget \ - build-essential \ - libffi-dev \ - curl +RUN useradd --create-home --uid 10001 app \ + && mkdir /app \ + && chown app:app /app WORKDIR /app @@ -22,10 +19,16 @@ COPY requirements.txt /app/ RUN pip install --no-cache-dir -r requirements.txt -COPY . /app/ +USER 10001 + +COPY --chown=app:app . /app/ RUN mkdir -p /app/data/ipinfo EXPOSE 8000 +# Probes port 8000. Any HTTP answer counts as alive; a refused connection or a timeout fails. +HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \ + CMD ["python", "-c", "import urllib.request, urllib.error\ntry:\n urllib.request.urlopen('http://127.0.0.1:8000/', timeout=3)\nexcept urllib.error.HTTPError:\n pass"] + CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--reload"] diff --git a/tests/live_smoke/stack/Dockerfile b/tests/live_smoke/stack/Dockerfile index cf34743b..8f543752 100644 --- a/tests/live_smoke/stack/Dockerfile +++ b/tests/live_smoke/stack/Dockerfile @@ -2,6 +2,8 @@ FROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +RUN useradd --create-home --uid 10001 app + WORKDIR /app COPY wheels/ /wheels/ @@ -19,6 +21,12 @@ ENV PATH="/app/.venv/bin:$PATH" \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 +USER 10001 + EXPOSE 8000 +# Same probe as the compose services: /health must answer 2xx. +HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=3 \ + CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)"] + CMD ["gunicorn", "-c", "gunicorn.conf.py", "app.main:app"]