Files
Ten31-Portal/backend/ten31portal/config.py
T
Jonathan Kirkwood 3a4b7dfd8b 0.2.46: security hardening (external-fund scope, role guards, forced password rotation, headers)
Pre-release audit fixes:
- External Administrators can only act on investors already in their own funds
  (grant_access, create_statement, set_partner_exited, import match), closing a
  cross-fund account-takeover chain.
- Role and is_active changes are gated: no self-escalation, non-approvers cannot
  mint or alter approver accounts, and the Service Admin is protected.
- must_change_password is enforced server-side, not just in the client.
- Session cookie is Secure with a 12h max-age and SameSite; CSP and standard
  security headers on every response; the app refuses to boot on a known or short
  session secret; uvicorn runs with proxy-headers so login throttling keys on the
  real client IP behind the platform proxy.
- Import preview no longer confirms cross-fund investors; no-store on document
  downloads.
- Adds backend/tests/test_security_hardening.py (15 tests); full suite 76 passing.
- SECURITY-AUDIT.md records the swarm audit, findings, and remediation status.
2026-08-28 09:53:16 -05:00

37 lines
2.2 KiB
Python

"""Application configuration via environment variables."""
import os
DB_PATH: str = os.getenv("TEN31_DB_PATH", "/data/ten31portal/portal.db")
SESSION_SECRET: str = os.getenv("TEN31_SESSION_SECRET", "change-me-in-production")
# Session secrets that must never reach production. The app refuses to start on any of these
# (see main.lifespan) so a packaging slip cannot silently boot on a source-visible key that
# would let anyone forge a session cookie.
INSECURE_SESSION_SECRETS: frozenset[str] = frozenset({
"", "change-me", "change-me-in-production",
})
# Send the session cookie only over HTTPS. Off by default so local dev and the test client
# (both plain HTTP) work; the container turns it on via TEN31_SESSION_COOKIE_SECURE=true.
SESSION_COOKIE_SECURE: bool = os.getenv(
"TEN31_SESSION_COOKIE_SECURE", "false"
).lower() in ("1", "true", "yes")
# Session lifetime. Default 12 hours (was the Starlette 14-day default), so a captured cookie
# ages out in a workday rather than a fortnight.
SESSION_MAX_AGE: int = int(os.getenv("TEN31_SESSION_MAX_AGE", str(12 * 60 * 60)))
DOCS_DIR: str = os.getenv("TEN31_DOCS_DIR", "/data/ten31portal/documents")
# Cap on a single uploaded document. The docs dir shares the data volume with the DB, so an
# unbounded upload could fill the disk and take the portal down. Default 50 MB. The same cap
# also bounds spreadsheet imports (which must be read fully into memory to parse).
MAX_UPLOAD_SIZE: int = int(os.getenv("TEN31_MAX_UPLOAD_SIZE", str(50 * 1024 * 1024)))
# Investor accounts created by the eNAV import (and existing no-login accounts converted via
# the enable-investor-logins CLI/action) start with this password so the admin can hand out
# credentials easily; each investor changes it via the portal's own Change password.
DEFAULT_INVESTOR_PASSWORD: str = os.getenv("TEN31_DEFAULT_INVESTOR_PASSWORD", "Ten31Portal")
# Where start.sh records the randomly-generated initial admin password on first boot, so the
# operator can retrieve it once (via the "Show Initial Admin Password" service action) and then
# change it. Lives next to the DB on the 0600 data volume; removed once the password is reset.
ADMIN_PASSWORD_FILE: str = os.path.join(os.path.dirname(DB_PATH) or ".", ".admin-password")