Security Review - Dedicated Download Service for Critical Run Artifacts
Security artifact for the dedicated download service package. Implementation findings are resolved; production rollout still requires live smoke evidence.
Metadata
- Package:
docs/work-packages/20260619_dedicated_download_service/ - Reviewer: Codex implementation review
- Date: 2026-06-19
- Scope reviewed: Dedicated archive download service, browse auth/path reuse, Docker Compose service wiring, Caddy route changes, focused tests, and service documentation.
- Commit/branch context: local
masterworktree during implementation. - Related artifacts:
- Code review: no separate code-review artifact was requested for this pass; implementation findings are dispositioned through the QA and security reviews.
- QA review:
docs/work-packages/20260619_dedicated_download_service/artifacts/20260619_qa_review.md
Security Triage Decision
- Security impact level: high
- Dedicated security review required: yes
- Triage rationale: The package introduces a new externally reachable file-serving service and changes proxy routing for run-scoped artifacts. Auth, path traversal, route shadowing, logging privacy, and operational rollback must be reviewed explicitly.
- Threat model assumptions:
- Attackers may know or guess public route shapes and may try traversal, symlink, encoded-path, or range-header abuse.
- Some runs are public, while private and root-only paths must remain protected by the canonical browse auth contract.
- Reverse proxy routing mistakes can expose unintended files or bypass existing browse checks even if service code is correct.
- Logs may be accessible to operators and must not contain secrets, raw JWTs, raw query filters, or sensitive full filesystem paths.
Findings
| ID | Severity | Surface | Description | Evidence | Required action | Status |
|---|---|---|---|---|---|---|
| SEC-01 | High | Auth/path extraction | New service could drift from canonical browse auth and path-boundary behavior. | wepppy/microservices/download/app.py imports browse.auth and browse.security; tests/microservices/test_dedicated_download_service.py covers public, private, bearer, traversal, hidden, non-zip, and range behavior. |
Reuse or faithfully extract existing helpers; add regression tests for private, public, traversal, missing/non-archive, and path-boundary cases. | Resolved |
| SEC-02 | High | Caddy routing | Exact archive matcher could accidentally capture non-archive downloads or route broader /download/* traffic to incomplete service behavior. |
docker/caddy/Caddyfile and docker/caddy/Caddyfile.wepp1 add archive_download_proxy before browse_proxy; tests/docker/unit/test_download_service_routing.py proves archive-only matching and non-archive fallback. |
Add precise route matcher before broad browse matcher; add negative route probes for browse/schema and non-archive downloads. | Resolved |
| SEC-03 | Medium | Logging privacy | Enhanced observability could log raw tokens, raw query strings, or full sensitive filesystem paths. | download.complete logs sanitized category/basename and client fields only; test_archive_download_logs_range_completion_without_full_path_or_token verifies full run root and Authorization are absent. |
Use sanitized path category and basename only; explicitly exclude Authorization, cookies, raw JWTs, raw filters, and full absolute paths from logs. | Resolved |
| SEC-04 | Medium | Range handling | Malformed or extreme range headers could trigger resource waste or incorrect responses. | _parse_range_header handles single closed/open/suffix ranges and rejects invalid/multi-range requests with 416; focused tests cover valid and unsatisfiable ranges. |
Parse single ranges defensively; reject invalid ranges with 416; test open-ended, suffix, and unsatisfiable ranges. |
Resolved |
| SEC-05 | Low | Route consistency | Lowercase-only Caddy ZIP matching could route uppercase .ZIP archives through browse instead of the dedicated service, reducing isolation and audit consistency. |
QA review found Caddy regex \.zip$ while the service accepted ZIP extensions case-insensitively. |
Updated both Caddyfiles to [Zz][Ii][Pp] and added routing test coverage for uppercase .ZIP. |
Resolved |
Risk acceptance authority: Accepted-risk requires security reviewer recommendation plus explicit package owner acknowledgment in Sign-off.
Verdict
- Gate status: pass for local implementation; production cutover remains conditional on live smoke validation.
- Unresolved findings:
- High: 0
- Medium: 0
- Low: 0
- Release recommendation: ship with conditions. Before considering production complete, start the service on wepp1, reload Caddy, and capture
HEAD, fullGET, rangedGET, and log evidence for a representative archive.
Surface Checks
1) Auth, Session, and Authorization
- Entry points enforce expected authn/authz checks for changed routes/services.
- Role checks and scope checks are explicit, least-privilege, and regression-tested.
- Session/JWT token validation paths preserve canonical contracts.
- CSRF protections are preserved for browser session mutation paths; the new route is read-only.
- Cross-service auth token mint/verify flows are not widened unintentionally.
- Error paths do not disclose token contents or auth internals.
2) Secrets and Credential Handling
- No new plaintext secrets in repository files, env defaults, or docs examples.
-
*_FILEsecret-file contract is preserved where applicable. - No secrets passed in argv, query params, or logs.
- Added/changed services mount only required secrets.
- Rotation and rollback behavior are documented for new secret dependencies; no new secret dependency was introduced.
- Changed code avoids fallback wrappers that silently skip missing secrets.
3) Input Validation and Output Safety
- Untrusted input is validated at boundaries (types, ranges, enum membership).
- File/path inputs block traversal and out-of-scope path access.
- Rendered output paths avoid unsafe HTML/markdown/script injection; the route does not render user content.
- URL fetch/download flows enforce allowlist or explicit safety constraints.
- Unsafe deserialization and shell interpolation patterns are absent.
- Failing validation returns explicit contract-compliant errors.
4) File System and Run-Tree Boundaries
- Writes remain inside intended run roots (
/wc1/runs/...) and approved paths; the new service is read-only. - No new path joins allow escaping run scope via symlink or relative path tricks.
- Export/download paths avoid leaking unrelated files.
- Temporary files and artifacts are cleaned up or intentionally retained with policy; the service creates none.
- Permissions for generated files/directories are least-privilege; the service generates none.
5) Queue, Worker, and Subprocess Surfaces
- Enqueue sites and dependency edges remain intentional and documented; no queue wiring changed.
- Worker task inputs are validated before shell/subprocess/file operations; no worker/subprocess path was added.
- Subprocess invocation avoids shell injection and unbounded command composition; no subprocess call was added.
- Queue cancellation/retry paths cannot bypass auth or data ownership boundaries; no queue path was added.
-
wctl check-rq-graphhas been run when queue wiring changed; not applicable because no queue wiring changed. - Failure handling preserves canonical response/error contracts.
6) Agentic Tooling and MCP Surfaces
- Agent/tool calls do not grant broader permissions than the parent request needs.
- No path for implicit privilege escalation through subagents or helper tools introduced by scaffold-only docs.
- MCP/tool tokens and credentials are scoped and not leaked in logs/artifacts.
- Tool execution constraints are explicit when running commands or file writes.
- Guardrails block unauthorized network egress or public artifact publication.
7) Network and External Integrations
- New outbound calls are justified, constrained, and observable; no outbound calls were added.
- Timeouts/retries avoid denial-of-service amplification and unsafe fallback loops.
- Internal-only endpoints are not exposed through new proxy/route changes.
- Rate limits/throttles are considered for high-cost or abuse-prone endpoints; this package isolates worker/timeouts but does not add rate limiting.
- External dependency trust assumptions are documented; no new dependency was added.
8) CI/CD and Supply Chain
- Self-hosted runner access scope is unchanged or tightened.
- Workflow token permissions are minimal for changed jobs; no workflow changed.
- Build/test scripts avoid exposing credentials in logs.
- New third-party dependencies passed precedent and risk checks; no new dependency was added.
- Pinned versions/digests are used where policy requires.
9) Data Integrity, Locking, and Concurrency
- NoDb lock/dump contracts are preserved (
dump_and_unlock, lock scope); no NoDb mutation path changed. - Cross-process shared state updates remain atomic and auditable; no shared-state mutation path was added.
- Redis keyspaces and TTL behavior are unchanged unless explicitly planned.
- Concurrent mutation paths include regression coverage for race conditions; no mutation path was added.
- Recovery after partial failure leaves state consistent and diagnosable.
10) Logging, Monitoring, and Incident Readiness
- Logs include enough context for incident triage without exposing secrets.
- Security-relevant events have observable signals (auth failures, denials, exfil attempts).
- New error handlers do not swallow exceptions silently.
- Alerting/operational checks are updated for new high-risk surfaces; health check and service logs are available, production alerting remains operator-owned.
- Rollback and containment steps are documented for the changed scope.
Validation Evidence
- Automated checks run:
wctl run-pytest tests/microservices/test_dedicated_download_service.py -q- 14 passed.wctl run-pytest tests/docker/unit/test_download_service_routing.py -q- 5 passed.wctl run-pytest tests/microservices/test_dedicated_download_service.py tests/docker/unit/test_download_service_routing.py -q- 19 passed.wctl run-pytest tests/microservices/test_dedicated_download_service.py tests/docker/unit/test_download_service_routing.py -q- 19 passed after QA dispositions.wctl run-pytest tests/microservices/test_download.py tests/microservices/test_browse_auth_routes.py::test_private_download_redirects_only_for_navigation tests/microservices/test_browse_auth_routes.py::test_private_download_uses_bearer_when_cookie_run_scope_mismatch tests/microservices/test_browse_auth_routes.py::test_run_download_root_only_path_uses_bearer_when_cookie_lacks_root_role -q- 9 passed.wctl run-pytest tests/microservices/test_dedicated_download_service.py tests/microservices/test_download.py tests/microservices/test_browse_auth_routes.py tests/microservices/test_browse_security.py tests/microservices/test_browse_routes.py tests/docker/unit/test_download_service_routing.py -q- 140 passed.PYTHONPATH=/home/workdir/wepppy .venv/bin/pytest tests/microservices/test_dedicated_download_service.py tests/microservices/test_download.py tests/microservices/test_browse_auth_routes.py tests/microservices/test_browse_security.py tests/microservices/test_browse_routes.py tests/docker/unit/test_download_service_routing.py -q- 140 passed after QA/security dispositions.wctl run-pytest tests/microservices/test_dedicated_download_service.py tests/microservices/test_download.py tests/microservices/test_browse_auth_routes.py tests/microservices/test_browse_security.py tests/microservices/test_browse_routes.py tests/docker/unit/test_download_service_routing.py -q- 140 passed after the local stack was restarted.docker compose --env-file docker/.env -f docker/docker-compose.dev.yml config --quiet- passed.docker compose --env-file docker/.env -f docker/docker-compose.prod.yml config --quiet- passed.docker compose --env-file docker/.env -f docker/docker-compose.prod.yml -f docker/docker-compose.prod.wepp1.yml config --quiet- passed.python3 tools/check_broad_exceptions.py --enforce-changed --base-ref origin/master- passed for tracked changed Python files; new service files were manually inspected for broad exception handlers.
- Manual checks run:
docker run --rm -v /workdir/wepppy/docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile- valid configuration with preexisting warnings.docker run --rm -v /workdir/wepppy/docker/caddy/Caddyfile.wepp1:/etc/caddy/Caddyfile:ro caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile- valid configuration with preexisting warnings.- Local Caddy
HEADsmoke againsthoneyed-marathoner/disturbed9002returned200,Accept-Ranges: bytes,Content-Length: 2516876934,Server: uvicorn,Via: 1.1 Caddy, and request id43dc51062fa54ab3a3a61bc0bc3836ec. - Local Caddy full
GETsmoke returned200and downloaded2516876934bytes in12.207687seconds at reported curl speed206171483bytes/s. - Local Caddy
Range: bytes=0-1048575smoke returned206,Content-Range: bytes 0-1048575/2516876934, and downloaded1048576bytes. - Local Caddy sparse resume smoke from byte
2515828358returned206,Content-Range: bytes 2515828358-2516876933/2516876934, and completed by downloading only the final1048576bytes. - Download service logs showed matching
download.completeevents forHEAD, fullGET, and both ranged probes with sanitized artifact identity and no tokens or absolute run-root paths.
Residual Risk
- Accepted residual risks:
- Client disconnect classification is best-effort from the Starlette streaming generator. Operators should correlate app logs with Caddy access logs for edge-level disconnect detail.
- NFS remains a shared backend dependency. The service split removes browse worker/proxy common-cause vectors but does not guarantee recovery from NFS stalls.
- Follow-up packages/issues:
- Potential follow-up for non-NFS/object-storage archive publication if service telemetry shows NFS remains the main reliability limit.
- Potential follow-up to migrate other exact non-transforming downloads after archive ZIP telemetry is clean.
Sign-off
- Security reviewer: Codex implementation review, 2026-06-19
- Package owner: WEPPcloud operators, pending production rollout acknowledgment