Files
crm-system/security-review-phase2.md
leocrm-bot 3cc0b2e1b4 phase2: architecture, task_graph v2.1.0, AGENTS.md, quality gates, security review
- architecture.md (2019 lines): 73/73 v1 features, RLS policies, CORS, auth rate limiting, v2 FKs removed
- task_graph.json v2.1.0: 14 tasks (7 v1 + 7 v2), 143 features, 298 ACs, all dict test_specs
- AGENTS.md: 14 tasks mapped, T07a/T07b split, v1/v2 phase plan
- Quality gate reviews: Round 1, 2, 3 (all passed)
- Security review: APPROVED_WITH_CONCERNS (0 critical, 7 major, 8 minor)
- Architecture feasibility review: FEASIBLE_WITH_RISKS (3 critical fixed, 5 major fixed)
- All 3 critical issues from feasibility review resolved
- All pre-implementation security items addressed
2026-06-28 23:07:29 +02:00

26 KiB

LeoCRM Phase 2 — Security & Data Risk Review

Reviewer: Security Data Engineer (A0 Orchestrator)
Date: 2026-06-28
Project: leocrm
Phase: Pre-Implementation (Phase 2 to Phase 3)
Files reviewed: architecture.md (1939 lines), task_graph.json (v2.0.0, 13 tasks), requirements.md (2142 lines, 143 features)
Scope: Security architecture, multi-tenant isolation, auth, data persistence, migration, backup/restore, plugin security, dependency risks


VERDICT: APPROVED_WITH_CONCERNS

The architecture is well-structured with strong fundamentals (session-based auth, CSRF protection, CSP headers, RBAC with field-level permissions, audit trail). However, 7 major risks and 8 minor risks must be addressed before or during implementation. No critical blocking issues found, but 3 major risks (RLS gap, rate limiting, CORS) should be resolved before Phase 3 start.

Severity Count
Critical 0
Major 7
Minor 8

1. AUTH SECURITY — Session-Based Auth + API Tokens

Design Summary

  • Session store: Redis (session:{id}, TTL=8h) — primary runtime store
  • Audit trail: PostgreSQL sessions table (immutable, retains all sessions ever created)
  • Cookie: leocrm_session=<id>; HttpOnly; Secure; SameSite=Strict; Path=/
  • Password hashing: bcrypt cost=12
  • API tokens: api_tokens table (SHA-256 hashed, scoped, expiring) — post-MVP but architecture-ready
  • Password reset: Token-based, 24h expiry, hashed storage, no user enumeration, session invalidation on reset

Assessment: GOOD with concerns

Positive:

  • ADR-05 decision is sound: server-side sessions avoid JWT pitfalls (token leakage, no revocation)
  • Immediate session invalidation via Redis key deletion
  • Forensic session audit trail in PostgreSQL (session validation flow checks Redis then PG audit then deny)
  • No user enumeration on login or password reset
  • Cookie flags correctly set (HttpOnly, Secure, SameSite=Strict)

MAJOR RISK M-01: No brute-force protection on auth endpoints

  • Finding: No account lockout, failed-attempt tracking, or rate limiting on POST /api/v1/auth/login or POST /api/v1/auth/password-reset/request
  • Impact: An attacker can perform unlimited password guessing attempts. bcrypt cost=12 slows each attempt (~250ms) but does not prevent distributed attacks.
  • Requirements reference: F-AUTH-01 has no lockout test scenario. Non-Goals section 18 explicitly excludes rate limiting from v1.
  • Recommendation: Add a minimal failed-attempt counter in Redis (login_failures:{email}, TTL=15min, threshold=10, lockout 15min) before Phase 3. This is distinct from general rate limiting and is scoped to auth only.

MINOR RISK m-01: LEOCRM_SECRET_KEY purpose and rotation undefined

  • Finding: LEOCRM_SECRET_KEY=<min-32-chars> is listed in env vars but its usage is not specified (session ID generation? cookie signing? CSRF token generation?). No rotation policy documented.
  • Recommendation: Document the secret's purpose in .env.example and define a rotation procedure in the admin guide. If used for signing session IDs, rotation invalidates all sessions (acceptable, document it).

MINOR RISK m-02: No 2FA in v1

  • Finding: Non-Goals section 13 explicitly excludes 2FA. Acceptable for v1 internal CRM, but should be prioritized post-MVP if exposed to internet.
  • Recommendation: Document as post-MVP roadmap item with priority based on exposure.

2. MULTI-TENANT ISOLATION — Row-Level Security

Design Summary

  • Every table has tenant_id (UUID, NOT NULL on core tables)
  • ORM auto-filtering via SQLAlchemy before_query event listener (do_orm_execute)
  • TenantMixin base class enforces tenant_id column on all models
  • Cross-tenant access returns 404 (not 403) to prevent information leakage
  • Plugin tables must include tenant_id (validator checks)
  • Tenant switch via POST /api/v1/auth/switch-tenant

Assessment: MODERATE RISK — needs DB-level enforcement

MAJOR RISK M-02: RLS claimed but only ORM-level filtering implemented

  • Finding: Architecture line 154 states "PostgreSQL 16 with Row-Level Security for tenant isolation" but the implementation (lines 1232-1240) is exclusively ORM-level filtering via SQLAlchemy event listener. No CREATE POLICY, ENABLE ROW LEVEL SECURITY, or SET app.current_tenant session variables are defined.
  • Impact: Any query that bypasses the ORM (raw SQL, session.execute(text(...)), stored procedures, Alembic migrations, ARQ worker jobs that don't set tenant context) will NOT be tenant-filtered. A single missed _tenant_filter_disabled flag or raw query can leak cross-tenant data.
  • Evidence: The before_query listener checks if not _tenant_filter_disabled — this flag must be managed carefully. Any code path that sets it without restoring is a data leak vector.
  • Recommendation:
    1. Implement PostgreSQL RLS policies as a defense-in-depth layer: CREATE POLICY tenant_isolation ON <table> USING (tenant_id = current_setting('app.current_tenant')::uuid)
    2. Set app.current_tenant at the beginning of each DB session/transaction from the authenticated session context
    3. Keep ORM filtering as the primary layer; RLS as the safety net
    4. This is a design change — should be approved before Phase 3 implementation

MAJOR RISK M-03: Tenant context propagation to ARQ workers not defined

  • Finding: Background jobs (exports, mail-sync, reminders, backups) run in a separate worker process. The architecture does not specify how current_tenant_id() is set in worker context. If a worker job operates on tenant-scoped data without setting the tenant context, the ORM filter may not apply or may apply incorrectly.
  • Impact: Cross-tenant data exposure in background job results (e.g., export contains data from all tenants).
  • Recommendation: Define and document tenant context propagation for ARQ workers: each job must carry tenant_id in its job context, and the worker must set current_tenant_id() before executing any DB queries.

MINOR RISK m-03: Tenant switch does not validate user-tenant membership

  • Finding: POST /api/v1/auth/switch-tenant updates the session's tenant_id. The architecture does not explicitly state that the endpoint validates the user's membership in the target tenant via user_tenants table.
  • Recommendation: Ensure the switch endpoint checks user_tenants membership before updating the session. Add a test case: user attempts to switch to a tenant they don't belong to then 403.

3. CSRF PROTECTION

Design Summary

  • SameSite=Strict cookie (browser-level protection)
  • Origin-Header-Validierung middleware (server-side check)
  • Only GET/HEAD/OPTIONS exempt from CSRF check
  • CSRF token stored per session in Redis and PostgreSQL audit table

Assessment: GOOD

Positive:

  • Two-layer CSRF protection (SameSite + Origin validation) is a solid approach
  • SameSite=Strict is the strongest browser-level CSRF defense
  • Origin validation is server-side and not bypassable by client tweaks
  • Test scenarios defined in task_graph.json: "CSRF: POST without Origin header then 403"

MINOR RISK m-04: CSRF token stored but not validated in requests

  • Finding: A csrf_token is generated and stored per session, but the architecture does not describe a mechanism where the frontend sends the token back (e.g., in a X-CSRF-Token header) and the backend validates it. The protection relies entirely on SameSite + Origin.
  • Impact: SameSite=Strict + Origin validation is sufficient for v1. The stored CSRF token appears unused.
  • Recommendation: Either (a) remove the csrf_token from the session model if SameSite+Origin is the chosen strategy, or (b) implement double-submit cookie pattern for defense-in-depth. Clarify in architecture.

4. INPUT VALIDATION

Design Summary

  • Pydantic schemas validate all API inputs (F-DATA-03)
  • XSS protection: server-side Pydantic validation + frontend DOMPurify/escaped rendering (F-SEC-02)
  • CSP headers prevent inline script execution
  • HTML in user inputs is escaped, not rendered

Assessment: GOOD

Positive:

  • Pydantic on all API inputs is FastAPI best practice
  • Server-side + client-side sanitization (defense in depth)
  • CSP header is well-configured: script-src 'self', object-src 'none', base-uri 'self'
  • XSS test scenarios defined in requirements

MINOR RISK m-05: No SQL injection prevention explicitly documented

  • Finding: While SQLAlchemy ORM with parameterized queries is the default, the architecture does not explicitly state a prohibition on raw SQL or string interpolation in queries.
  • Recommendation: Add an explicit coding guideline: no raw SQL with string interpolation; all raw queries must use parameterized text() with bind parameters.

5. SECRETS MANAGEMENT (F-ENV-01)

Design Summary

  • .env.example documents all environment variables with <secret> placeholders
  • Secrets never in Git repo (.gitignore includes .env)
  • Missing secret env var then app fails to start with clear error (F-ENV-01 test scenario 3)
  • Secrets: POSTGRES_PASSWORD, LEOCRM_SECRET_KEY, SMTP_PASS, MAIL_ENCRYPTION_KEY, S3_SECRET_KEY, AI_API_KEY
  • Pydantic Settings for env var loading (config.py)

Assessment: ADEQUATE for v1 with gaps

MAJOR RISK M-04: No secret rotation policy

  • Finding: No rotation procedure is defined for any secret (LEOCRM_SECRET_KEY, MAIL_ENCRYPTION_KEY, POSTGRES_PASSWORD, SMTP_PASS). F-ENV-01 only covers initial setup, not lifecycle.
  • Impact: If a secret is compromised, there is no documented procedure to rotate it. MAIL_ENCRYPTION_KEY rotation is especially critical — changing it without a re-encryption plan would make existing encrypted mail credentials unreadable.
  • Recommendation:
    1. Document rotation procedures for each secret in admin guide
    2. For MAIL_ENCRYPTION_KEY: implement key versioning (store key_id with encrypted data, support old + new key during rotation)
    3. For LEOCRM_SECRET_KEY: document that rotation invalidates all sessions (acceptable)
    4. For POSTGRES_PASSWORD: document procedure (change password, update env, restart)

MINOR RISK m-06: .env file approach for production

  • Finding: Docker Compose uses env_file: .env for all services including production on Coolify. This means secrets are stored in a plaintext file on the server.
  • Impact: If the host filesystem is compromised, all secrets are readable. Docker env vars are also visible via docker inspect.
  • Recommendation: For production on Coolify, use Coolify's secret/environment variable management (injects as container env vars without a file on disk). The .env file approach is fine for dev only. Document this split in the deployment guide.

6. DATA MIGRATION RISK (F-MIG-01)

Design Summary

  • F-MIG-01: CSV import with field mapping, per-row error reporting, auto-company-detection for contact imports
  • No legacy system data migration (no ETL from external CRM systems)
  • No schema migration risk (greenfield project with Alembic)

Assessment: LOW RISK — properly scoped

Positive:

  • CSV import is well-defined with field mapping and error handling
  • No complex legacy migration in v1 (correct scope decision)
  • Alembic for schema migrations is standard and reliable

MAJOR RISK M-05: CSV import has no file size limit or row count validation

  • Finding: F-MIG-01 test scenario imports 50 companies. No mention of maximum file size, maximum row count, or memory protection for large CSV files. A 500MB CSV with 1M rows could cause OOM or timeout.
  • Impact: Denial of service via large CSV upload; potential memory exhaustion.
  • Recommendation:
    1. Define max upload size (e.g., 10MB for CSV)
    2. Process CSV in streaming mode (not loading entire file into memory)
    3. Add row count limit (e.g., 50,000 rows per import)
    4. Run import as background job (ARQ) for files >1000 rows

7. BACKUP/RESTORE (F-INFRA-02)

Design Summary

  • pg_dump daily cron job to backup volume or S3
  • Storage volume backup (files)
  • Restore documented in docs/admin-guide.md
  • Backup failure triggers alert to admin

Assessment: MAJOR RISK — inadequate for multi-tenant production

MAJOR RISK M-06: Backup strategy insufficient for multi-tenant PostgreSQL

  • Finding: The backup design has multiple gaps:
    1. No backup encryption: pg_dump output is plaintext. Tenant data (companies, contacts, emails) is stored unencrypted in the backup volume/S3.
    2. No retention policy: No definition of how many backups to keep (7 days? 30 days?). Unlimited backups cause storage exhaustion; too few cause data loss.
    3. No tested restore procedure: F-INFRA-02 acceptance criterion says "Restore-Dokumentation vorhanden" but there is no test scenario that verifies an actual restore works.
    4. No point-in-time recovery: Only daily pg_dump snapshots. If a tenant accidentally deletes data at 14:00 and notices at 17:00, all data created between 00:00 and 14:00 that day is lost.
    5. Multi-tenant restore granularity: pg_dump is all-or-nothing. If one tenant needs restore, all tenants are affected. No mention of tenant-level export/restore.
    6. Redis not backed up: Session data is in Redis with TTL=8h. Redis is not included in backup strategy. If Redis is lost, all active sessions are invalidated (users must re-login). This is acceptable but should be documented.
  • Recommendation:
    1. Encrypt pg_dump output (gpg or S3 SSE-KMS)
    2. Define retention: 7 daily + 4 weekly + 12 monthly
    3. Add a restore test to the test suite (backup, restore, verify row count)
    4. Enable PostgreSQL WAL archiving for point-in-time recovery (PITR)
    5. Document that restore is all-tenant; consider tenant-level CSV export as a quick-recovery alternative
    6. Document Redis session loss behavior (acceptable: users re-login)

8. PLUGIN SECURITY (T03 Plugin Framework)

Design Summary

  • Built-in plugins only (no dynamic external loading in v1) — ADR-03
  • Plugin manifest schema (Pydantic)
  • Lifecycle hooks: install/activate/deactivate/uninstall
  • Plugin DB migration runner with plugin_migrations tracking
  • Migration validator checks tenant_id on all plugin tables
  • Service Container DI: plugins receive db, cache, event_bus, storage, notifications
  • Event Bus integration: plugins register/unregister event listeners

Assessment: MODERATE RISK — tenant isolation enforced, but no permission scoping

MAJOR RISK M-07: No plugin API permission scoping

  • Finding: Plugins receive injected services (db, cache, event_bus, storage, notifications) but there is no permission model restricting what a plugin can do. A plugin with access to the db session can query any table within the current tenant context. There is no "plugin A can only read companies, plugin B can only write to its own tables" model.
  • Impact: A malicious or buggy built-in plugin could access/modify data from other modules within the same tenant. Since all v1 plugins are built-in (shipped with code), this is lower risk, but the architecture should define the permission model for when external plugins are added post-MVP.
  • Recommendation:
    1. For v1: document that plugins are trusted (built-in only) and have full tenant-scoped access
    2. For post-MVP: define plugin permission scopes in the manifest (e.g., permissions: ["companies:read", "contacts:write"])
    3. Add a test: plugin cannot access data from a different tenant (already covered by tenant_id validator)

MINOR RISK m-07: Plugin event bus has no namespacing

  • Finding: Plugins register event listeners on a shared event bus. There is no mention of event namespacing to prevent event name collisions between plugins.
  • Recommendation: Use prefixed event names (e.g., dms.file.uploaded, calendar.event.created) to avoid collisions.

MINOR RISK m-08: Plugin uninstall with data removal has no confirmation audit

  • Finding: DELETE /api/v1/plugins/{name}?remove_data=true drops plugin tables. The architecture does not mention that this destructive action is logged in the audit log.
  • Recommendation: Log plugin uninstall with data removal to audit_log with actor, timestamp, plugin name, and table list.

9. DEPENDENCY RISKS

Design Summary

  • Backend: FastAPI, SQLAlchemy, Pydantic, ARQ, Redis-py, asyncpg/psycopg
  • Frontend: React 18, TanStack Query v5, Vite, Tailwind CSS
  • Database: PostgreSQL 16-alpine
  • Cache/Queue: Redis 7-alpine
  • Document editing: OnlyOffice Document Server

Assessment: LOW-MODERATE RISK

OnlyOffice :latest tag

  • Finding: Docker Compose uses onlyoffice/documentserver:latest. This tag is mutable and can introduce breaking changes or security vulnerabilities without notice.
  • Impact: Unpredictable updates; potential breaking changes; supply chain risk.
  • Recommendation: Pin to a specific version tag (e.g., onlyoffice/documentserver:8.2.2). Update deliberately after testing.

Other dependency notes:

  • FastAPI, React 18, PostgreSQL 16, Redis 7 are all current stable major versions with active security maintenance
  • No known critical CVEs in these major versions as of 2026-06
  • Recommendation: Pin all dependencies in requirements.txt / package.json with exact versions or minimum patches. Add pip-audit and npm audit to CI pipeline.
  • Recommendation: Use postgres:16-alpine and redis:7-alpine (already specified — good). Pin minor versions for reproducibility.

10. RATE LIMITING

Assessment: MAJOR RISK — explicitly excluded from v1

Finding: Non-Goals section 18: "Kein zentrales Rate-Limiting in v1." This means:

  • POST /api/v1/auth/login — no rate limit (brute-force possible, see M-01)
  • POST /api/v1/auth/password-reset/request — no rate limit (email bombing possible)
  • All API endpoints — no rate limit (DoS via excessive requests)
  • API tokens (post-MVP) — no rate limit per token

Impact:

  • Auth endpoints are brute-force vulnerable (mitigated partially by bcrypt cost=12, but not for distributed attacks)
  • Password reset endpoint can be abused to send unlimited emails (SMTP abuse, email bombing)
  • General API abuse (data scraping, DoS)

Recommendation:

  • Implement auth-scoped rate limiting (not general rate limiting) before Phase 3:
    • Login: 10 attempts per email per 15 min (Redis counter)
    • Password reset: 3 requests per email per hour
    • This is minimal effort and high security value
  • General API rate limiting can remain post-MVP if the app is internal-only, but document the decision

11. CORS

MAJOR RISK M-08: CORS configuration not specified

  • Finding: The frontend is served on port 80 (Nginx) and the API on port 8000 (FastAPI). In production, they may share a domain (reverse proxy) or be on separate ports. The architecture does not specify CORS headers.
  • Impact: If frontend and API are on different origins (e.g., dev environment: localhost:80 to localhost:8000), the browser will block requests without proper CORS headers. If CORS is set to wildcard, credentials (cookies) will not work.
  • Recommendation:
    • In production: serve frontend + API behind the same reverse proxy (same origin, no CORS needed)
    • In development: configure FastAPI CORS middleware with allow_origins=["http://localhost:80"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]
    • Never use allow_origins=["*"] with allow_credentials=True (browser rejects this)
    • Document CORS configuration in architecture.md

12. DOCKER/COMPOSE SECURITY

Findings

Issue Severity Detail
No non-root user Minor All containers run as root by default. Add user: directive or use images with non-root users.
No cap_drop: ALL Minor Containers retain all Linux capabilities. Drop all and add only needed ones.
No read-only filesystem Minor Add read_only: true with tmpfs for writable paths.
Redis without auth Major redis:7-alpine has no requirepass or ACL configured. Any container on the network can access Redis.
OnlyOffice :latest Minor Mutable tag; pin to specific version.
All ports exposed Minor ports: ["8000:8000"], ["80:80"], ["8080:80"] expose to host. In production, use internal network + reverse proxy only.
No health checks on all services Minor Only backend has a healthcheck. Add for postgres, redis, worker.
No resource limits Minor No mem_limit, cpus limits. A runaway process can consume all host resources.

Recommendation for Redis auth:

  • Add REDIS_PASSWORD env var
  • Configure Redis with requirepass or use ACL users
  • Update REDIS_URL to include password: redis://:<password>@redis:6379/0

13. FILE UPLOADS

Finding

DMS plugin (T05) handles file uploads via POST /api/v1/dms/files/upload (multipart). The architecture does not specify:

  • Maximum file size limit
  • Allowed file types / MIME type validation
  • File content verification (magic bytes, not just extension)
  • Malware / virus scanning
  • Filename sanitization (path traversal prevention)

Assessment: MAJOR RISK (deferred to plugin implementation)

  • Impact: Path traversal via malicious filenames, disk exhaustion via large files, stored XSS via uploaded HTML/SVG files, potential malware storage.
  • Recommendation: Define upload security in T05 task specification:
    1. Max file size: 50MB (configurable)
    2. Allowed MIME types whitelist (exclude text/html, image/svg+xml, application/javascript)
    3. Filename sanitization: strip path components, use UUID-based storage names
    4. Store files outside web root (already handled by storage service)
    5. Post-MVP: ClamAV integration for malware scanning

14. LOGGING SENSITIVE DATA

Assessment: GOOD

  • Structured JSON logs (F-INFRA-03): timestamp, level, event, method, path, status, duration, tenant_id, user_id
  • No password, token, or secret values in log format
  • Log level configurable via LOG_LEVEL env var
  • Recommendation: Add explicit log sanitization in the logging middleware: filter out password, new_password, token, Authorization header fields from request body logging if request body is ever logged.

15. DATA PERSISTENCE AND DATA LOSS RISK

Assessment: MODERATE RISK

  • Soft-delete with deleted_at column — good for accidental deletion recovery
  • DSGVO hard-delete with deletion_log — good for compliance
  • deletion_log mentioned in architecture but table schema not fully defined in the reviewed sections
  • Risk: Soft-deleted data is still in the database. If a tenant requests GDPR deletion, the hard-delete must also remove soft-deleted records.
  • Recommendation: Verify deletion_log table schema includes: tenant_id, entity_type, entity_id, deleted_by, deleted_at, data_summary (for audit).

SUMMARY TABLE

# Risk Severity Domain Action Before Phase 3?
M-01 No brute-force protection on auth Major Auth YES — add Redis-based attempt counter
M-02 RLS claimed but ORM-only filtering Major Multi-Tenant YES — add DB-level RLS as defense-in-depth
M-03 ARQ worker tenant context undefined Major Multi-Tenant YES — define in architecture
M-04 No secret rotation policy Major Secrets NO — document before deployment
M-05 CSV import no size/row limit Major Migration NO — add in T05/T07 implementation
M-06 Backup insufficient for multi-tenant Major Backup NO — resolve before deployment
M-07 No plugin API permission scoping Major Plugins NO — acceptable for v1 (built-in only)
M-08 CORS not specified Major Network YES — configure and document
m-01 LEOCRM_SECRET_KEY purpose/rotation undefined Minor Secrets NO
m-02 No 2FA in v1 Minor Auth NO (post-MVP)
m-03 Tenant switch membership validation Minor Multi-Tenant YES — add test case
m-04 CSRF token stored but unused Minor CSRF NO — clarify architecture
m-05 No SQL injection prevention guideline Minor Validation NO — add coding guideline
m-06 .env file for production Minor Secrets NO — use Coolify env management
m-07 Plugin event bus no namespacing Minor Plugins NO
m-08 Plugin uninstall no audit log Minor Plugins NO

TOP 3 RISKS

  1. M-02: RLS gap — Architecture claims PostgreSQL RLS but implements only ORM-level tenant filtering. Raw SQL, worker jobs, or filter bypass bugs can leak cross-tenant data. Must add DB-level RLS policies as defense-in-depth before implementation.

  2. M-01: No brute-force protection — Auth endpoints (login, password reset) have no rate limiting, lockout, or failed-attempt tracking. Combined with M-08 (no CORS config), the attack surface for credential attacks is significant. Must add minimal Redis-based auth rate limiting before Phase 3.

  3. M-06: Backup strategy inadequate — No encryption, no retention policy, no tested restore, no PITR for multi-tenant PostgreSQL. Data loss risk for production tenants. Must resolve before deployment phase.


RECOMMENDATION FOR PHASE 3 START

APPROVED_WITH_CONCERNS — Phase 3 may start after addressing the 3 pre-implementation items:

  1. M-02: Add PostgreSQL RLS policy definitions to architecture.md (defense-in-depth alongside ORM filtering)
  2. M-01 + Rate Limiting: Add auth-scoped rate limiting (login attempt counter + password reset throttle) to T01 task specification
  3. M-08: Add CORS configuration to architecture.md (same-origin in prod, explicit origins in dev)

Additionally, update T01 task to include:

  • ARQ worker tenant context propagation (M-03)
  • Tenant switch membership validation test (m-03)
  • Redis auth configuration (Docker Compose)

The remaining major risks (M-04, M-05, M-06, M-07) can be addressed during implementation or before deployment.


Review complete. No secrets, credentials, or live values were inspected. All findings based on architecture.md, task_graph.json, and requirements.md content only.