From 985253422da0e2eb6bd02b38bddb7faef1f7c665 Mon Sep 17 00:00:00 2001 From: thethreemagi Date: Tue, 1 Sep 2026 13:38:47 -0400 Subject: [PATCH] harden: run unprivileged, read-only rootfs, no capabilities The app is the only process on this box accepting unauthenticated input from the internet and it was running as root in a writable container. Now uid 10001, read_only with a tmpfs /tmp, cap_drop ALL and no-new-privileges. Host-side /srv/scout-website/data and the service account key are chowned to 10001. Verified on a throwaway container: every route, the admin API, spam rejection, and a real submission writing to both the jsonl and SQLite. --- app/Dockerfile | 5 ++++- docker-compose.yml | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/Dockerfile b/app/Dockerfile index ee89588..08607cb 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -2,5 +2,8 @@ FROM python:3.12-slim RUN pip install --no-cache-dir fastapi "uvicorn[standard]" python-multipart gspread WORKDIR /app COPY . /app -RUN mkdir -p /app/static/img +RUN mkdir -p /app/static/img && useradd -r -u 10001 app +# Unprivileged: nothing here needs root, and the process is the one thing on +# this box that takes unauthenticated input from the internet. +USER 10001 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml index 59c2473..91d747e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,13 @@ services: build: context: ./app container_name: scout-website + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true restart: unless-stopped logging: driver: json-file