Skip to content

Commit d484744

Browse files
authored
Merge pull request #27 from MrChengLen/pr-audit-test-hardening
Pr audit test hardening
2 parents 698de4a + 24b8d7b commit d484744

9 files changed

Lines changed: 689 additions & 68 deletions

File tree

.env.example

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,141 @@
1-
# FileForge Configuration
2-
# Copy this file to .env and adjust values
1+
# FileMorph Configuration
2+
# Copy this file to .env and adjust values for your deployment.
3+
#
4+
# This file is structured top-down: required for every deployment first,
5+
# then Cloud-overlay (account / payment / email features), then Compliance-
6+
# Edition tunables, then operational knobs. A Community-Edition self-host
7+
# only needs the first section.
8+
#
9+
# ──────────────────────────────────────────────────────────────────────
10+
# === Required for every deployment ===
11+
# ──────────────────────────────────────────────────────────────────────
312

4-
# Server
13+
# Server bind. APP_HOST=0.0.0.0 makes the app listen on every interface
14+
# inside the container; the reverse proxy (Caddy/nginx) is what limits
15+
# external reach. APP_DEBUG=true enables FastAPI's interactive docs at
16+
# /docs and verbose tracebacks — leave off in production.
517
APP_HOST=0.0.0.0
618
APP_PORT=8000
719
APP_DEBUG=false
820

9-
# Security
10-
# Path to the JSON file storing hashed API keys
21+
# Path to the JSON file holding hashed API keys for anonymous-tier
22+
# clients. The file is created on first key generation; bind-mount the
23+
# parent directory into your container if you want keys to survive
24+
# restarts.
1125
API_KEYS_FILE=data/api_keys.json
1226

13-
# Limits
27+
# Maximum HTTP request body size accepted by the upload endpoints.
28+
# 100 MB is a sane default that fits typical convert/compress workloads
29+
# without inviting OOM on small hosts. Operators with bigger files raise
30+
# this; tier quotas (anonymous/free/pro/business) still apply on top via
31+
# app/core/quotas.py.
1432
MAX_UPLOAD_SIZE_MB=100
1533

16-
# CORS: comma-separated list of allowed origins (* for all)
34+
# CORS: comma-separated list of allowed origins. `*` is fine for dev or
35+
# a single-origin same-domain deployment. Production deployments behind
36+
# a real domain should list explicit origins (e.g.
37+
# `https://files.example.com,https://www.files.example.com`) so a
38+
# malicious page on a different domain can't talk to your API.
1739
CORS_ORIGINS=*
1840

19-
# Optional: route heavy upload POSTs (convert/compress, single + batch) through
20-
# a separate subdomain. Empty string = same-origin (default, simplest). Set
21-
# only when the main site sits behind a proxy that caps request bodies and
22-
# uploads must bypass it. See docs/self-hosting.md for CORS implications.
41+
# Public canonical URL of the deployment. Used by the canonical/og:url
42+
# meta tags, the sitemap, and the JSON-LD structured data. Localhost is
43+
# fine for dev; set to your domain for production so search engines
44+
# index the right URLs.
45+
APP_BASE_URL=http://localhost:8000
46+
47+
# Optional: route heavy upload POSTs (convert/compress, single + batch)
48+
# through a separate subdomain. Empty string = same-origin (default,
49+
# simplest). Set this only when the main site sits behind a proxy that
50+
# caps request bodies and uploads must bypass it via a tunnel
51+
# subdomain. See docs/self-hosting.md for CORS implications.
2352
API_BASE_URL=
2453

25-
# Public canonical URL for the deployment — used for canonical/og:url meta
26-
# tags, sitemap entries, and JSON-LD structured data. Default localhost is
27-
# fine for dev; set to your domain for prod (e.g. https://files.example.com).
28-
APP_BASE_URL=http://localhost:8000
54+
# ──────────────────────────────────────────────────────────────────────
55+
# === Cloud-overlay (optional — accounts, payments, transactional email) ===
56+
# ──────────────────────────────────────────────────────────────────────
57+
# A Community Edition deployment can leave everything below empty.
58+
# Setting any of these activates the corresponding sub-processor (see
59+
# docs/sub-processors.md). The application disables each feature
60+
# automatically when its primary key/url is empty — no further toggle
61+
# needed.
62+
63+
# JWT signing secret. Required for the user-account features
64+
# (registration, login, refresh, role checks). Must be at least 32
65+
# characters; rotate by changing this value (all sessions invalidate
66+
# on the next request).
67+
JWT_SECRET=dev-secret-change-me-min-32-chars-long
2968

30-
# Whether to expose /pricing as a commercial offer surface. Self-hosted
31-
# Community deployments leave this off — there's no commercial offer to
32-
# advertise. Set to `true` only on a SaaS deployment that has (or will
33-
# soon have) Stripe configured. The page renders a "Coming Soon" banner
34-
# automatically when STRIPE_SECRET_KEY is empty.
69+
# PostgreSQL DSN for the Cloud overlay (users, api_keys, file_jobs,
70+
# audit_events tables). Use the asyncpg driver. Empty string disables
71+
# the database completely — registration/login routes return 503.
72+
# DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/filemorph
73+
74+
# Stripe (leave empty to disable billing). When present, the /pricing
75+
# page shows live upgrade buttons gated behind the BGB §356 (5)
76+
# withdrawal-waiver checkbox; without these values the page renders a
77+
# "Coming Soon" banner.
78+
STRIPE_SECRET_KEY=
79+
STRIPE_WEBHOOK_SECRET=
80+
STRIPE_PRO_PRICE_ID=
81+
STRIPE_BUSINESS_PRICE_ID=
82+
83+
# Whether to expose /pricing as a commercial offer surface at all.
84+
# Self-hosted Community deployments leave this off — there is no
85+
# commercial offer to advertise. Set to `true` only on a SaaS
86+
# deployment that has (or will soon have) Stripe configured.
3587
PRICING_PAGE_ENABLED=false
3688

37-
# S10-lite analytics — per-day counters for page views, conversions,
38-
# registrations, and failures. Visible at /cockpit (admin-only). Counters
39-
# are aggregates, not personal data: no cookie banner needed. Default on;
40-
# set to `false` if you don't want the daily_metrics table populated at
41-
# all (the cockpit Analytics tab then shows an empty-state notice).
42-
METRICS_ENABLED=true
89+
# Transactional email (password-reset, billing receipts, account
90+
# verification). Empty SMTP_HOST disables outgoing mail; the routes
91+
# that need it (forgot-password, register) return a graceful 503.
92+
# SMTP_HOST=smtp.zoho.eu
93+
# SMTP_PORT=587
94+
# SMTP_USER=no-reply@example.com
95+
# SMTP_PASSWORD=
96+
# SMTP_FROM=no-reply@example.com
97+
98+
# ──────────────────────────────────────────────────────────────────────
99+
# === Compliance-Edition tunables (audit log, retention, output integrity) ===
100+
# ──────────────────────────────────────────────────────────────────────
101+
# These knobs target operators in regulated environments (DACH
102+
# Behörden, healthcare, legal). The defaults below are Cloud-edition
103+
# safe (fire-and-forget audit, no retention beyond the request);
104+
# Compliance customers tighten them per their privacy/audit policy.
43105

44-
# NEU-B.1: Compliance-Edition tamper-evident audit log. Default off
45-
# (Cloud-edition fire-and-forget — failures are logged at WARNING and
46-
# never break the request). Set to `true` for ISO 27001 A.12.4.1 /
47-
# BORA §50 / BeurkG §39a compliance — the convert/compress route
48-
# refuses to serve a result it could not log to audit_events. Requires
49-
# DATABASE_URL.
106+
# NEU-B.1Compliance-Edition tamper-evident audit log gate. Default
107+
# off (Cloud-edition: failures are logged at WARNING and never break
108+
# the request). Set to `true` for ISO 27001 A.12.4.1 / BORA §50 /
109+
# BeurkG §39a compliance — the convert/compress route then refuses
110+
# to serve a result it could not log to audit_events. Requires
111+
# DATABASE_URL above.
50112
AUDIT_FAIL_CLOSED=false
51113

52-
# NEU-B.2 retention policy. The Cloud edition is zero-retention by
53-
# design (every conversion flushes its temp dir on completion or
54-
# failure; there is no S3/R2 storage layer active). RETENTION_HOURS
55-
# is an informational knob for self-hosters running a future
56-
# storage-key-backed pipeline (FileJob.expires_at). Compliance-edition
57-
# operators with an eDiscovery / GoBD retention requirement set this
58-
# to the value their privacy policy declares; Cloud / Community keep
59-
# it at 0.
114+
# NEU-B.2 — Retention policy in hours. Cloud edition is zero-retention
115+
# by design (every conversion flushes its temp dir on completion or
116+
# failure; no S3/R2 storage layer is active). This knob is informational
117+
# for self-hosters running a future storage-key-backed pipeline
118+
# (FileJob.expires_at). Compliance-edition operators with an
119+
# eDiscovery / GoBD retention requirement set this to the value their
120+
# privacy policy declares; Cloud / Community keep it at 0.
60121
RETENTION_HOURS=0
61122

123+
# ──────────────────────────────────────────────────────────────────────
124+
# === Operational knobs (sweep cadence, concurrency, metrics) ===
125+
# ──────────────────────────────────────────────────────────────────────
126+
127+
# S10-lite analytics — per-day counters for page views, conversions,
128+
# registrations, and failures. Visible at /cockpit (admin-only).
129+
# Counters are aggregates, not personal data — no cookie banner needed.
130+
# Default on; set to `false` to leave the daily_metrics table empty
131+
# (the cockpit Analytics tab then shows an empty-state notice).
132+
METRICS_ENABLED=true
133+
62134
# Background sweep that removes orphaned `fm_*` temp dirs left behind
63135
# by crashes mid-conversion. The request path always cleans its own
64136
# temp dir in a `finally` block, so this only catches crash-recovery
65137
# cases. Set to 0 to disable the periodic sweep (the startup sweep
66-
# still runs).
138+
# still runs once on boot regardless).
67139
TEMP_SWEEP_INTERVAL_MINUTES=60
68140

69141
# How old (minutes) an `fm_*` temp dir must be before the sweep
@@ -72,10 +144,10 @@ TEMP_SWEEP_INTERVAL_MINUTES=60
72144
# Operators with very long batch pipelines raise this to match.
73145
TEMP_SWEEP_MAX_AGE_MINUTES=10
74146

75-
# NEU-D.1 capacity guard. Total parallel conversions across all
76-
# callers. Default 4 is sized for a 4 GB host with the existing
77-
# per-tier output caps; raise to ~CPU-count on a bigger box.
78-
# Past the cap → 503 + Retry-After.
147+
# NEU-D.1 — global concurrency cap across all callers. Default 4 is
148+
# sized for a 4 GB host with the existing per-tier output caps; raise
149+
# to roughly CPU-count on a bigger box. Past the cap, requests get
150+
# 503 + Retry-After.
79151
MAX_GLOBAL_CONCURRENCY=4
80152

81153
# How long a request waits for a free slot before giving up. Small

.githooks/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ FAIL=0
1313
# locale/ catalogs are mechanically extracted from the impressum/privacy/terms templates above —
1414
# they cannot avoid carrying the same address/email strings. Treating them as public is consistent
1515
# with the source templates being public.
16-
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo))$'
16+
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo)|\.env\.example)$'
1717

1818
# Personal/operational identifiers that should never land in public code.
1919
PATTERNS='lennart\.seidel@icloud\.com|lennart@filemorph\.io|Reetwerder|21029 Hamburg'

.githooks/pre-push

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ set -e
1515
ZERO=0000000000000000000000000000000000000000
1616

1717
# Same patterns as pre-commit — keep in sync.
18-
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo))$'
18+
ALLOW_RE='^(app/templates/(impressum|privacy|terms)\.html|COMMERCIAL-LICENSE\.md|docs/gdpr-account-deletion-design\.md|docs/api-usage-guide\.md|docs/self-hosting\.md|docs/dpa-template\.md|docs-internal/.*|\.githooks/.*|\.github/workflows/scope-guard\.yml|CHANGELOG\.md|locale/.*\.(po|pot|mo)|\.env\.example)$'
1919
PATTERNS='lennart\.seidel@icloud\.com|lennart@filemorph\.io|Reetwerder|21029 Hamburg'
2020
OPS_PATTERNS='/opt/filemorph(/|$|[[:space:]])|/var/log/filemorph|/home/deploy([[:space:]]|/)|Hetzner CX|HETZNER_HOST|HETZNER_SSH_USER|HETZNER_SSH_KEY|OPS_REPO_DISPATCH_PAT|GHCR_PAT|appleboy/ssh-action'
2121
SECRET_ASSIGN='(JWT_SECRET|SMTP_PASSWORD|STRIPE_SECRET_KEY|STRIPE_WEBHOOK_SECRET|DATABASE_URL|API_KEY|POSTGRES_PASSWORD|GHCR_PAT|OPS_REPO_DISPATCH_PAT|HETZNER_SSH_KEY)[[:space:]]*=[[:space:]]*[^[:space:]$]'

app/core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Settings(BaseSettings):
1717

1818
api_keys_file: str = "" # resolved below if empty
1919

20-
max_upload_size_mb: int = 2000
20+
max_upload_size_mb: int = 100
2121

2222
cors_origins: str = "http://localhost:8000"
2323
jwt_secret: str = "dev-secret-change-me-min-32-chars-long"

0 commit comments

Comments
 (0)