The template targets the OWASP top-10 surface for a small Python+React service plus the LLM-specific risks that come with hosting an agent. Each layer below is independent — defence in depth, not a single chokepoint.
| Threat | Where it can land | Defence |
|---|---|---|
| Secret in repo (AWS key, OpenAI key, PEM) | Commit | (1) .claude/hooks/pretooluse_bash.py scans staged diff; (2) pre-commit gitleaks; (3) CI gitleaks |
| Vulnerable Python dep | uv.lock |
pip-audit --strict (security.yml) — fails on any CVE; per-CVE ignore list at .github/security/pip-audit-ignore.txt with sunset notes |
| Vulnerable npm dep | package-lock.json |
npm audit --audit-level=high (security.yml) |
| Vulnerable container CVE | Built image | Trivy scan in security.yml — blocks merge on fixable HIGH/CRITICAL |
| Agent prompt injection | /api/v1/... body |
Output sanitisation: render LLM responses as plain text or pre-formatted blocks; never dangerouslySetInnerHTML |
| API contract drift | Pydantic models | StrictModel (extra="forbid") raises at construction — typos and renamed fields fail at the seam |
| Required-check drift | .github/branch-protection/*.json |
Branch-protection contexts sync meta-gate fails CI when JSON contexts disagree with workflow jobs on disk |
| Commit-type drift | Commitizen ↔ pr-title.yml | Commit-type sync meta-gate compares the two allowlists |
| Released image tampering | GHCR | release.yml ships a CycloneDX SBOM attached to the GitHub Release; image is built once per tag with reproducible deps via uv sync --frozen --no-dev |
| Force-push to main | Default access | Branch protection: allow_force_pushes: false, allow_deletions: false, require_code_owner_reviews: true, required status checks |
LLM coder edits ──► PreToolUse hook (forbidden flags + secret scan + audit log)
│
▼
Local commit ──► pre-commit (ruff, gitleaks, commitizen, mypy, hygiene)
│
▼
git push ──► CI:
• Lint & Format (ruff)
• Type Check (mypy --strict)
• Architecture (import-linter)
• Unit tests + Coverage ≥ 75 %
• Pre-commit (re-run, no-bypass)
• Frontend Build + Frontend Quality
• Branch-protection contexts sync
• Commit-type sync
• Lint PR title (conventional commits)
• Secret scan (gitleaks)
• Python deps (pip-audit --strict)
• Frontend deps (npm audit --audit-level=high)
• Container image scan (trivy)
│
▼
PR review ──► Code owner approval (CODEOWNERS)
│
▼
Merge to develop ──► develop branch protection (15 required contexts, strict: false)
│
▼
Release PR ──► develop → main; main branch protection (15 required, strict: true)
│
▼
Tag v*.*.* ──► release.yml: build image, push to ghcr.io, generate SBOM, publish Release
Dockerfile ships a multi-stage build:
- 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 userapp. 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.
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.
- WAF / DDoS — deployment-environment concerns, not template concerns.
- Authentication — the scaffold ships no auth; the right layer (OIDC, mTLS, API keys, sessions) is project-specific.
- Secret manager integration —
Settingsreads from env /.env. A real deployment should fetchLLM_API_KEYfrom a vault and inject it as env, but the wiring is environment-specific. - Rate limiting — same — depends on infrastructure.
Each of these is a slot to fill once your domain is decided. The harness doesn't try to pretend any of them exist out of the box.