Skip to content

Security: constk/harness-python-react

Security

docs/SECURITY.md

Security posture

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 model (scaffold)

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

Defence-in-depth map

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

Container hardening

Dockerfile ships a multi-stage build:

  • Builder — runs uv sync --frozen --no-dev. Has uv, pip cache, build tools.
  • Runtimepython: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/fooRead-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.
  • Authentication — the scaffold ships no auth; the right layer (OIDC, mTLS, API keys, sessions) is project-specific.
  • Secret manager integrationSettings reads from env / .env. A real deployment should fetch LLM_API_KEY from 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.

There aren't any published security advisories