Skip to content
Merged
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
4 changes: 4 additions & 0 deletions abct-docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ WORKDIR /app
COPY backend/ /app/backend/

# Copy frontend code
# nginx workers run as www-data and serve /app/frontend directly. COPY keeps the build context's
# file modes, so a checkout made under a restrictive umask (e.g. 027 -> 0640 files / 0750 dirs)
# gives 403 on /, /classic and every /static + /next/static asset. Normalise to world-readable.
COPY frontend/ /app/frontend/
RUN chmod -R a+rX /app/frontend

# Copy nginx configuration
# Remove default config and install ours
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_dockerfile_frontend_perms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Regression test for the 2026-09-29 403 incident.

The image was built from a checkout made under umask 027, COPY preserved the 0640/0750
modes, and nginx (worker user www-data) returned 403 for / and every static asset.
The Dockerfile must make /app/frontend world-readable after copying it.
"""
from pathlib import Path
import re

DOCKERFILE = Path(__file__).resolve().parents[2] / "abct-docker" / "Dockerfile"


def _instructions():
lines = DOCKERFILE.read_text().splitlines()
return [l.strip() for l in lines if l.strip() and not l.strip().startswith("#")]


def test_frontend_made_world_readable_after_copy():
ins = _instructions()
copy_idx = next(i for i, l in enumerate(ins) if re.match(r"COPY\s+frontend/\s+/app/frontend/?$", l))
chmod_ok = any(
re.search(r"chmod\s+-R\s+a\+rX\s+/app/frontend\b", l) for l in ins[copy_idx + 1:] if l.startswith("RUN")
) or re.search(r"--chmod=", ins[copy_idx])
assert chmod_ok, "frontend must be readable by nginx's www-data regardless of build-context umask"


def test_nginx_worker_user_still_needs_this():
# If nginx ever runs workers as root this test documents why the chmod exists; keep them paired.
conf = (DOCKERFILE.parent / "nginx.conf").read_text()
assert "root /app/frontend;" in conf
Loading