From 578aa03c266764cfdc26abf7a437426835c97ac5 Mon Sep 17 00:00:00 2001 From: "const.koutsakis@aurecongroup.com" Date: Thu, 30 Apr 2026 01:53:54 +1000 Subject: [PATCH] =?UTF-8?q?chore:=20container=20hardening=20=E2=80=94=20re?= =?UTF-8?q?ad-only=20root=20FS=20+=20tmpfs=20/tmp=20+=20Python=20env=20(#1?= =?UTF-8?q?19,=20#120,=20#170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 9 ++++++++- docker-compose.yml | 9 +++++++++ docs/SECURITY.md | 8 ++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d5135d8..0bdf8f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,14 @@ COPY --chown=app:app src/ src/ USER app # Put the venv on PATH so `uvicorn` resolves without `uv run` indirection. -ENV PATH="/app/.venv/bin:${PATH}" +# PYTHONDONTWRITEBYTECODE=1 stops Python from attempting `.pyc` writes under +# `__pycache__/` on cold start — they would EROFS-fail under the read-only +# root FS configured in docker-compose.yml. PYTHONUNBUFFERED=1 keeps +# uvicorn's stdout from being held behind line-buffering when running under +# non-TTY container stdio. +ENV PATH="/app/.venv/bin:${PATH}" \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 EXPOSE 8000 diff --git a/docker-compose.yml b/docker-compose.yml index 55fb5ce..9224151 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,15 @@ services: - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317 - OTEL_EXPORTER_OTLP_PROTOCOL=grpc - OTEL_SERVICE_NAME=harness-python-react + # Container hardening. The root FS is read-only at the kernel level so a + # post-exploit shell can't modify /app, persist binaries, or fill disk + # under the `app` user's ownership. /tmp is the only writable path — + # tmpfs-mounted with a 64 MB ceiling so it can't be abused as unbounded + # storage. Verified: `touch /app/foo` → EROFS; `touch /tmp/foo` succeeds; + # healthcheck reports healthy. See docs/SECURITY.md "Container Security". + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 frontend: build: ./frontend diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 416d7f4..e88a93b 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -61,8 +61,16 @@ Tag v*.*.* ──► release.yml: build image, push to ghcr.io, generate - **Builder** — runs `uv sync --frozen --no-dev`. Has uv, pip cache, build tools. - **Runtime** — `python:3.14-slim`, copies only `.venv` + `src/` from the builder, runs as non-root user `app`. No uv, no pip cache, no build tools, no dev deps. +Runtime stage env: `PYTHONDONTWRITEBYTECODE=1` (no `.pyc` writes — would EROFS-fail under the read-only root FS) and `PYTHONUNBUFFERED=1` (uvicorn stdout flushed immediately). + +`docker-compose.yml`'s `app` service runs with `read_only: true` and a `tmpfs: /tmp:size=64m,mode=1777` mount. The kernel rejects writes to every path except the 64 MB tmpfs, so a post-exploit shell under the `app` user cannot modify `/app`, persist binaries, or fill the host's disk under `app`'s ownership. Verified locally: `touch /app/foo` → `Read-only file system`; `touch /tmp/foo` succeeds; healthcheck reports `healthy`. + Healthcheck uses stdlib `urllib.request` so curl isn't in the image. +### Distroless evaluation — deferred + +`gcr.io/distroless/python3-debian12` ships Python at `/usr/bin/python3` while the current builder stage materialises a venv whose `pyvenv.cfg` and interpreter symlinks reference `/usr/local/bin/python3.14` (Dockerfile comment makes this constraint explicit). Migrating requires either matching Python paths between stages (no distroless variant matches slim's `/usr/local`) or rebuilding the venv inside the runtime stage (distroless has no `pip` / `uv`). Either route adds engineering risk and operational friction (no `docker exec ... sh`) that outweighs the marginal attack-surface reduction now that read-only-FS + non-root + no-build-tools + trivy-scanning are all in place. Revisit when distroless ships a `/usr/local` variant or when the venv-in-runtime cost shrinks. + ## What's intentionally out of scope (scaffold) - **WAF / DDoS** — deployment-environment concerns, not template concerns.