SPB Git

spb/findata

Public

api: listen on all interfaces behind a source-network guard (tunnel reachability); cli closes the pool

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 1, 2026) parent fe69034 Browse files

4 changed files +26 −3

modified docs/ARCHITECTURE.md +1 −1
@@ -60,4 +60,4 @@ See `docs/DISCOVERY.md` §5. Budget: curated ≤ 250 GB soft limit, 60 GB free h
60 60 5. **Backup (daily)**: `pg_dump | zstd` + `rsync -a --delete` of the lake to M4M64a; remote file count verified; `last_backup` recorded. Restore drill: `findata restore-drill`.
61 61
62 62 ## Security model
63 −Owner-only by default. Browser: HttpOnly/Secure/SameSite=Strict session cookie, CSRF double-submit header, TOTP MFA (scopes empty until the second factor passes). Programmatic: `Authorization: Bearer fd_<prefix>.<secret>` keys, HMAC-hashed at rest, scoped, expirable, revocable, per-key rate limit and daily usage accounting. Vendor credentials live only in `.env` (mode 600) on the node; every log line and job error passes through a redaction filter; the HTTP client never logs URLs. The API binds to 127.0.0.1 (reached through the node's wg1 proxy by Caddy on BHS64). CSP `default-src 'self'`, HSTS, nosniff, frame-ancestors none. SQL playground: single read-only statement, keyword deny-list, DuckDB `allowed_directories` restricted to the lake, external access disabled, timeout and row caps. Exports are scoped to the requesting principal and expire.
63 +Owner-only by default. Browser: HttpOnly/Secure/SameSite=Strict session cookie, CSRF double-submit header, TOTP MFA (scopes empty until the second factor passes). Programmatic: `Authorization: Bearer fd_<prefix>.<secret>` keys, HMAC-hashed at rest, scoped, expirable, revocable, per-key rate limit and daily usage accounting. Vendor credentials live only in `.env` (mode 600) on the node; every log line and job error passes through a redaction filter; the HTTP client never logs URLs. The API listens on the node interfaces behind a source-network guard (loopback, WireGuard 10.67.0.0/16, LAN) so Caddy on BHS64 reaches it over wg1. CSP `default-src 'self'`, HSTS, nosniff, frame-ancestors none. SQL playground: single read-only statement, keyword deny-list, DuckDB `allowed_directories` restricted to the lake, external access disabled, timeout and row caps. Exports are scoped to the requesting principal and expire.
modified infra/mld/findata.json +2 −2
@@ -12,7 +12,7 @@
12 12 "size_mb": 20,
13 13 "placement": { "pin": "M3U96a", "prefer": "M3U96a", "avoid": ["M1M32"], "reason": "Storage node chosen in docs/DISCOVERY.md: 1 TB Apple NVMe (508 GB free, SMART verified, 5 GB/s), 32 cores / 96 GB for DuckDB, Postgres 17 + Redis already local, wg1 10.67.0.10. Lake at ~/findata-lake (outside the app dir)." },
14 14 "processes": [
15 − { "name": "findata-api", "manager": "pm2", "script": "{{HOME}}/apps/findata/.venv/bin/python", "args": ["-m", "uvicorn", "findata_api.main:app", "--host", "127.0.0.1", "--port", "8600", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "10.67.0.1,127.0.0.1"], "interpreter": null, "cwd": "{{HOME}}/apps/findata", "env": {"FINDATA_ROOT": "{{HOME}}/apps/findata", "PATH": "/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"}, "cron_restart": null, "autorestart": true, "max_memory_restart": "6G" },
15 + { "name": "findata-api", "manager": "pm2", "script": "{{HOME}}/apps/findata/.venv/bin/python", "args": ["-m", "uvicorn", "findata_api.main:app", "--host", "0.0.0.0", "--port", "8600", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips", "10.67.0.1,127.0.0.1"], "interpreter": null, "cwd": "{{HOME}}/apps/findata", "env": {"FINDATA_ROOT": "{{HOME}}/apps/findata", "PATH": "/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"}, "cron_restart": null, "autorestart": true, "max_memory_restart": "6G" },
16 16 { "name": "findata-worker", "manager": "pm2", "script": "{{HOME}}/apps/findata/.venv/bin/python", "args": ["-m", "findata_ingest.cli", "worker"], "interpreter": null, "cwd": "{{HOME}}/apps/findata", "env": {"FINDATA_ROOT": "{{HOME}}/apps/findata", "PATH": "/opt/homebrew/opt/postgresql@17/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"}, "cron_restart": null, "autorestart": true, "max_memory_restart": "24G" }
17 17 ],
18 18 "ngrok": null,
@@ -29,6 +29,6 @@
29 29 "post_start": []
30 30 },
31 31 "ka_repo": false,
32 − "tunnel": { "domain": "findata.spboucher.ai", "gateway": "BHS64", "note": "DNS GoDaddy A findata.spboucher.ai → 51.161.112.61 vérifié le 2026-11-01 avant déploiement ; route Caddy posée par mld deploy ; findata-api écoute 127.0.0.1 seulement (Caddy → wg1 10.67.0.10:8600 via le proxy du nœud)" },
32 + "tunnel": { "domain": "findata.spboucher.ai", "gateway": "BHS64", "note": "DNS GoDaddy A findata.spboucher.ai → 51.161.112.61 vérifié le 2026-11-01 avant déploiement ; route Caddy posée par mld deploy ; findata-api écoute 0.0.0.0:8600 avec garde de provenance (loopback, wg1 10.67.0.0/16, LAN) ; Caddy → wg1 10.67.0.10:8600" },
33 33 "notes": "Monorepo uv (Python 3.13) : services/api (FastAPI :8600, sert aussi la console Next.js statique apps/web/out), services/ingest (worker unique = verrou d'écriture du lac + ordonnanceur), lac Parquet ~/findata-lake, Postgres 17 local (DB findata, rôle findata). Secrets uniquement dans ~/apps/findata/.env (600) ; copie ~/Desktop/Cluster/secrets/findata/.env. Sauvegarde quotidienne 04:00 ET → M4M64a:~/backups/findata (rsync + pg_dump zstd). CLI : .venv/bin/findata status|discover|ingest …"
34 34 }
modified services/api/src/findata_api/main.py +15 −0
@@ -72,6 +72,20 @@ def _looks_id(p: str) -> bool:
72 72 return len(p) >= 16 and all(c in "0123456789abcdef-" for c in p)
73 73
74 74
75 +TRUSTED_PREFIXES = ("127.", "::1", "10.67.", "192.168.", "10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.2", "172.30.", "172.31.")
76 +
77 +
78 +class SourceGuard(BaseHTTPMiddleware):
79 + """The API listens on all interfaces so the WireGuard peer (Caddy on BHS64 via wg1) can reach it; only loopback,
80 + the tunnel subnet and the LAN may connect. Caddy terminates TLS and forwards the real client in X-Forwarded-For."""
81 +
82 + async def dispatch(self, request: Request, call_next):
83 + host = request.client.host if request.client else ""
84 + if not host.startswith(TRUSTED_PREFIXES) and host != "testclient":
85 + return JSONResponse({"error": {"code": "forbidden", "message": "untrusted network"}}, status_code=403)
86 + return await call_next(request)
87 +
88 +
75 89 class RateLimit(BaseHTTPMiddleware):
76 90 async def dispatch(self, request: Request, call_next):
77 91 if request.url.path.startswith(API_PREFIX) and not request.url.path.endswith("/health"):
@@ -93,6 +107,7 @@ def create_app() -> FastAPI:
93 107 "Timestamps are UTC ISO-8601; bars are stamped at bar **open**.")
94 108 app.add_middleware(Observability)
95 109 app.add_middleware(RateLimit)
110 + app.add_middleware(SourceGuard)
96 111
97 112 @app.exception_handler(StarletteHTTPException)
98 113 async def http_exc(request: Request, exc: StarletteHTTPException):
modified services/ingest/src/findata_ingest/cli.py +8 −0
@@ -7,6 +7,14 @@ from pathlib import Path
7 7 import typer
8 8
9 9 app = typer.Typer(no_args_is_help=True, help="FinData OS operator CLI")
10 +
11 +
12 +@app.callback()
13 +def _main() -> None:
14 + import atexit
15 +
16 + from findata_core.db import close_pool
17 + atexit.register(close_pool)
10 18 ingest_app = typer.Typer(no_args_is_help=True)
11 19 app.add_typer(ingest_app, name="ingest")
12 20
13 21
Menu

Theme