- 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
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
sessionstable (immutable, retains all sessions ever created) - Cookie:
leocrm_session=<id>; HttpOnly; Secure; SameSite=Strict; Path=/ - Password hashing: bcrypt cost=12
- API tokens:
api_tokenstable (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/loginorPOST /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.exampleand 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_queryevent listener (do_orm_execute) TenantMixinbase class enforcestenant_idcolumn 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, orSET app.current_tenantsession 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_disabledflag or raw query can leak cross-tenant data. - Evidence: The
before_querylistener checksif not _tenant_filter_disabled— this flag must be managed carefully. Any code path that sets it without restoring is a data leak vector. - Recommendation:
- 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) - Set
app.current_tenantat the beginning of each DB session/transaction from the authenticated session context - Keep ORM filtering as the primary layer; RLS as the safety net
- This is a design change — should be approved before Phase 3 implementation
- Implement PostgreSQL RLS policies as a defense-in-depth layer:
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_idin its job context, and the worker must setcurrent_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-tenantupdates the session'stenant_id. The architecture does not explicitly state that the endpoint validates the user's membership in the target tenant viauser_tenantstable. - Recommendation: Ensure the switch endpoint checks
user_tenantsmembership 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_tokenis generated and stored per session, but the architecture does not describe a mechanism where the frontend sends the token back (e.g., in aX-CSRF-Tokenheader) 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.exampledocuments all environment variables with<secret>placeholders- Secrets never in Git repo (
.gitignoreincludes.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:
- Document rotation procedures for each secret in admin guide
- For MAIL_ENCRYPTION_KEY: implement key versioning (store key_id with encrypted data, support old + new key during rotation)
- For LEOCRM_SECRET_KEY: document that rotation invalidates all sessions (acceptable)
- 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: .envfor 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
.envfile 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:
- Define max upload size (e.g., 10MB for CSV)
- Process CSV in streaming mode (not loading entire file into memory)
- Add row count limit (e.g., 50,000 rows per import)
- Run import as background job (ARQ) for files >1000 rows
7. BACKUP/RESTORE (F-INFRA-02)
Design Summary
pg_dumpdaily 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:
- No backup encryption:
pg_dumpoutput is plaintext. Tenant data (companies, contacts, emails) is stored unencrypted in the backup volume/S3. - 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.
- 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.
- No point-in-time recovery: Only daily
pg_dumpsnapshots. 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. - Multi-tenant restore granularity:
pg_dumpis all-or-nothing. If one tenant needs restore, all tenants are affected. No mention of tenant-level export/restore. - 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.
- No backup encryption:
- Recommendation:
- Encrypt pg_dump output (gpg or S3 SSE-KMS)
- Define retention: 7 daily + 4 weekly + 12 monthly
- Add a restore test to the test suite (backup, restore, verify row count)
- Enable PostgreSQL WAL archiving for point-in-time recovery (PITR)
- Document that restore is all-tenant; consider tenant-level CSV export as a quick-recovery alternative
- 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_migrationstracking - Migration validator checks
tenant_idon 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
dbsession 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:
- For v1: document that plugins are trusted (built-in only) and have full tenant-scoped access
- For post-MVP: define plugin permission scopes in the manifest (e.g.,
permissions: ["companies:read", "contacts:write"]) - 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=truedrops 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_logwith 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.jsonwith exact versions or minimum patches. Addpip-auditandnpm auditto CI pipeline. - Recommendation: Use
postgres:16-alpineandredis: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:80tolocalhost: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=["*"]withallow_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_PASSWORDenv var - Configure Redis with
requirepassor use ACL users - Update
REDIS_URLto 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:
- Max file size: 50MB (configurable)
- Allowed MIME types whitelist (exclude
text/html,image/svg+xml,application/javascript) - Filename sanitization: strip path components, use UUID-based storage names
- Store files outside web root (already handled by storage service)
- 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_LEVELenv var - Recommendation: Add explicit log sanitization in the logging middleware: filter out
password,new_password,token,Authorizationheader 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_atcolumn — good for accidental deletion recovery - DSGVO hard-delete with
deletion_log— good for compliance deletion_logmentioned 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_logtable 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
-
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.
-
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.
-
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:
- M-02: Add PostgreSQL RLS policy definitions to architecture.md (defense-in-depth alongside ORM filtering)
- M-01 + Rate Limiting: Add auth-scoped rate limiting (login attempt counter + password reset throttle) to T01 task specification
- 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.