Security & Data Handling¶
This service is designed for a trusted internal network. It ships with no authentication and assumes the documents it serves are non-sensitive manufacturing knowledge. This page documents the boundaries it does enforce, the ones it doesn't, and how to harden a real deployment.
Threat model in one line¶
The app trusts its callers but not its inputs: uploaded files and scan paths are treated as hostile (bounded, sanitized), while who is calling is assumed to be already authorized by an upstream gateway.
What the app enforces¶
Server-side scan boundary¶
POST /api/v1/ingest/scan resolves folder_path under ingest.scan_root (default
data). A path that escapes that root (../, absolute paths) is rejected with 400 —
a caller cannot use the scan endpoint to read arbitrary host files.
Upload path sanitization¶
Uploaded filenames are sanitized before they touch the filesystem: traversal sequences are
stripped and the destination is forced to stay inside ingest.upload_dir
(data/uploads). This is covered by tests/unit/test_import_security.py
(test_safe_upload_path_strips_traversal, …keeps_plain_name_inside_dir).
Resource bounds (DoS guards)¶
Pathological files can't exhaust memory during extraction — each bound rejects with a clear error instead of risking an OOM that takes the process down:
| Bound | Setting | Default |
|---|---|---|
| Per-file size | ingest.max_file_size_mb |
50 MB |
| Allowed types | ingest.allowed_extensions |
pdf, xlsx, xls, csv, pptx, docx |
| PDF pages | ingest.pdf_max_pages |
2000 |
| XLSX cells | ingest.xlsx_max_cells |
2 000 000 |
Request payloads are bounded too: search keywords/error-codes are length- and
count-capped (src/kb/models/search.py), and chat is capped at 200 messages × 20 000
chars each (src/kb/api/chat.py) so a caller can't drive unbounded memory or LLM token
cost. Import sessions are TTL-evicted (session_ttl_minutes / session_hard_ttl_minutes)
so abandoned previews can't pin memory.
Taxonomy as an allow-list¶
project/equipment are validated against config/taxonomy.yaml at index time, and
LLM-extracted values not in the taxonomy are dropped rather than used as filters. This
keeps both stored data and queries within a known vocabulary.
Zero-fabrication as a safety property¶
Search results are verbatim documents or nothing — the LLM never injects generated text
into results. This is an architectural guarantee (see
Architecture → constraints), and it means the system cannot
"make up" a procedure or a parameter value: a wrong answer can only ever be a wrong
retrieval, which is auditable via the stored request_id.
What the app does NOT provide¶
| Gap | Implication | Mitigation |
|---|---|---|
| Authentication / authorization | Any caller who can reach the port can search, ingest, and delete | Deploy behind an API gateway / reverse proxy that authenticates and authorizes; never expose :8000 publicly |
| Multi-tenancy / per-user scoping | All documents are visible to all callers | Segregate at the network layer, or run separate instances per tenant |
| Encryption at rest | ES data on the es-data volume is unencrypted by default |
Use encrypted storage / an ES security tier |
| Admin-endpoint gating | /api/v1/admin/* and DELETE are unauthenticated like everything else |
Restrict these paths at the gateway |
| Rate limiting | No built-in throttle | Enforce at the gateway/proxy |
Secrets¶
The only secrets are the upstream API keys and ES credentials:
KB_LLM__API_KEY,KB_EMBEDDING__API_KEY,KB_ES__PASSWORD.- Supply them via
.env(git-ignored) or your orchestrator's secret store — never bake them into the image or commit them..env.exampleis the committed template. - Missing keys degrade features rather than crash (no LLM → 503 on AI endpoints; no embedding key → BM25-only), so it's safe to run without them in a locked-down tier.
Transport & ES auth¶
For ES over an untrusted network:
KB_ES__URL=https://es.internal:9200
KB_ES__VERIFY_CERTS=true
KB_ES__SSL_FINGERPRINT=<sha256-from-es-startup> # for a self-signed node
KB_ES__USERNAME=kb_app
KB_ES__PASSWORD=<from-secret-store>
Use a least-privilege ES user scoped to the kb_* indices. See
Deployment → Hardening for the full production checklist.
Auditing¶
Every request carries a request_id propagated into logs and onto search-feedback records,
so a result (including a 👎) can be traced back to the exact query, extracted params, and
upstream calls that produced it — see Observability → Logging.
There is no built-in access log of who read which document (no identity layer); capture
that at the gateway if you need it.