CiteBase is architected as an enterprise-grade, multi-tenant RAG-as-a-Service (Retrieval-Augmented Generation) document intelligence platform. Because the system manages proprietary tenant documents, performs dense/sparse vector index lookups, and synthesizes answers via large language models, security is a first-class architectural concern.
This document outlines the threat model, authentication lifecycle, multi-tenant partition boundaries, credential storage specifications, and production deployment hardening guidelines.
flowchart TD
Client["Client / Untrusted Network (Web Browser / API Client)"]
Gateway["Reverse Proxy / API Gateway (TLS Termination, OAuth2 / OIDC)"]
API["CiteBase FastAPI Backend (appuser, UID 10001)"]
Postgres[("PostgreSQL 16 (Auth, Relational Metadata, Audit)")]
Chroma[("ChromaDB Vector Store (t_{tenant_id}_{collection})")]
Redis[("Redis 7 (Tenant-Scoped Cache & Rate Limiting)")]
LLM["External LLM Provider (Google Gemini API / TLS)"]
Client -->|Untrusted HTTPS Request| Gateway
Gateway -->|Authenticated / Scoped Request| API
API -->|SHA-256 Hash Lookup| Postgres
API -->|Tenant-Scoped Vector Queries| Chroma
API -->|Tenant-Keyed Cache & Token Bucket| Redis
API -->|Context-Constrained Prompt| LLM
- Tenant Documents & Ingested Content: Raw PDFs and extracted textual chunks.
- Dense & Sparse Vector Indices: 384-dimensional semantic embeddings in ChromaDB and BM25 inverted indices.
- Cached Query Responses: LLM answers and citation metadata cached in Redis.
- Relational Records & Audit Trails: Tenant metadata, API key hashes, ingestion tasks, and query execution logs in PostgreSQL.
- System Credentials: Master bootstrap keys, database credentials, and external LLM provider API tokens.
| Threat Vector | Potential Impact | CiteBase Architectural Mitigation |
|---|---|---|
| Cross-Tenant Data Leakage | Tenant B discovers or queries Tenant A documents. | Cryptographic tenant resolution from SHA-256 key hash; strict runtime namespacing of vector collections (t_{tenant_id[:12]}_{coll}), BM25 indices, and Redis cache keys. |
| API Key Brute-Force / Enumeration | Attacker guesses high-entropy API keys. | Keys generated with 256 bits of entropy (secrets.token_urlsafe(32)); constant-time SHA-256 lookup; Redis sliding-window rate limiting per key. |
| Plaintext Credential Compromise | Database backup leak exposes raw API keys. | Plaintext keys are never stored. Only irreversible SHA-256 hashes are persisted in PostgreSQL. |
| Application-Level XSS Exposure | Injected JavaScript reads long-lived browser credentials. | Frontend playground migrates away from persistent localStorage to ephemeral sessionStorage scoped strictly to the active tab. |
| Denial of Service via Heavy Ingestion / Queries | Resource exhaustion crashing server. | Asynchronous background processing (HTTP 202); file size limits (MAX_UPLOAD_BYTES = 50MB); query text length limits (max_length = 2000); per-tenant sliding-window rate limiting. |
| Prompt Injection / Grounding Bypass | Malicious document content overrides generation prompt. | Context passages strictly wrapped in structured fences (--- CONTEXT START ---); system prompt instructs model to answer exclusively from numbered citations or return explicit refusal. |
sequenceDiagram
autonumber
actor Client as Client / Playground
participant Auth as Auth Dependency (FastAPI)
participant DB as PostgreSQL (api_keys, tenants)
participant Engine as RAG Execution Engine
Client->>Auth: Request with 'X-API-Key' or 'Authorization: Bearer'
alt Missing Key
Auth-->>Client: HTTP 401 Unauthorized ("Missing API Key")
end
Auth->>Auth: Compute deterministic SHA-256 hash
Auth->>DB: SELECT * FROM api_keys WHERE key_hash = :hash AND is_active = true
alt Key Not Found / Revoked
Auth-->>Client: HTTP 401 Unauthorized ("Invalid or revoked API Key")
end
Auth->>DB: SELECT * FROM tenants WHERE id = :tenant_id AND is_active = true
alt Tenant Inactive / Suspended
Auth-->>Client: HTTP 403 Forbidden ("Tenant account is suspended")
end
Auth->>Engine: TenantContext(tenant_id, tenant_name, key_prefix, rate_limit_rpm)
Engine-->>Client: Scoped Execution Response
- Header Extraction: Fast extraction from either
X-API-KeyorAuthorization: Bearer <key>. - SHA-256 Hash Matching: Deterministic SHA-256 hash compared against database records.
- Dual Status Verification: Requests fail closed unless both the API key and its owning Tenant record are explicitly active (
is_active = True). - TenantContext Propagation: Downstream controllers receive a read-only, strongly typed
TenantContextcontaining the tenant ID, name, rate limit, and key prefix.
Every entity in PostgreSQL (DocumentRecord, IngestionTask, QueryLog, ApiKey) enforces foreign-key linkage to a specific tenant_id. Application queries strictly enforce tenant scoping:
SELECT * FROM document_records WHERE tenant_id = :tenant_id AND collection_name = :coll;To prevent cross-tenant vector contamination in ChromaDB, collections are dynamically prefixed using the tenant's identifier:
def get_scoped_collection_name(tenant_id: str, raw_name: str) -> str:
clean_name = raw_name.strip().lower()
tenant_prefix = tenant_id[:12]
# Guarantees total length <= 63 characters for ChromaDB compliance
return f"t_{tenant_prefix}_{clean_name[:45]}"Even if Tenant B submits an API request targeting a collection named financial_audit, the backend maps it to t_{tenant_b_id[:12]}_financial_audit, ensuring complete partition isolation from Tenant A's t_{tenant_a_id[:12]}_financial_audit.
BM25 sparse indices are saved atomically on disk using paths scoped strictly by the internal collection identifier:
data/bm25_indices/{scoped_collection_name}.json
Tenant collections cannot access, read, or overwrite index files belonging to other organizations.
All Redis response cache keys are strictly namespaced by tenant ID:
rag:cache:{tenant_id}:{query_sha256}
Even if Tenant A and Tenant B ask the exact same question against their own collections, Tenant B will never receive Tenant A's cached response.
Keys are generated using Python's cryptographically secure pseudo-random number generator (secrets.token_urlsafe(32)), producing 256 bits of entropy. Keys are formatted with standard prefixes:
sk_live_{token}
Plaintext API keys are never stored in persistent storage.
- At generation time, the plaintext token is displayed to the administrator exactly once.
- The database stores only
key_hash = hashlib.sha256(raw_key.encode('utf-8')).hexdigest(). - For operational logging and operator identification, an abbreviated 12-character prefix is recorded (
key_prefix = raw_key[:12], e.g.sk_live_a1b2...).
To rotate credentials without downtime:
- Issue an API call or administrative migration creating a new active
ApiKeyfor the tenant. - Update client configuration / API consumer systems with the new key.
- Validate that requests succeed under the new key prefix in audit logs.
- Revoke the old key by setting
is_active = False. The change takes effect immediately across all instances.
CiteBase utilizes Redis sliding-window token counters to enforce tenant quotas (rate_limit_rpm):
- Per-Key Scoping: Rate limits are tracked per active API key ID:
rate_limit:{api_key_id}:{current_minute}. - HTTP 429 Response: Exceeded quotas return HTTP 429 with informative response headers:
HTTP/1.1 429 Too Many Requests Retry-After: 60 X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0
- Fail-Open In-Memory Fallback: If Redis is temporarily unreachable, the rate limiter falls back to an in-memory dictionary counter with logged warnings, preventing service interruption while continuing to monitor burst anomalies.
Allowed origins are configured strictly via environment variables (ALLOWED_ORIGINS):
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
allow_headers=["*"],
)Wildcard * origins are strictly prohibited in production configurations.
- Developer Playground (Development / QA Mode):
- Designed for interactive exploration and multi-tenant testing.
- The browser frontend (
frontend/app.js) passesX-API-Keyheaders directly. - Credentials are stored strictly in
sessionStorage(isolated to the browser tab) and never persisted in long-livedlocalStorage.
- Production Architecture (B2B Consumer Mode):
- Browser applications never store or directly inject privileged master API keys.
- Requests terminate at an API Gateway / Reverse Proxy (e.g., Kong, Envoy, Cloudflare).
- Client authentication utilizes OAuth2/OIDC (JWT Bearer tokens or HTTP-only secure session cookies).
- The Gateway resolves user identity and injects verified tenant context headers downstream to CiteBase.
In production mode (ENV=production):
- CiteBase validates that
BOOTSTRAP_API_KEYis not empty and is not set to any disallowed default or template placeholder. - If an insecure or placeholder key is detected, the application halts startup immediately with an
EnvironmentError, refusing to expose the service. - Docker Compose files enforce mandatory variable presence (
${POSTGRES_PASSWORD:?Error...}), preventing silent fallbacks to known public passwords.
The production Docker container implements enterprise container security best practices:
- Non-Root Execution: Runs under dedicated unprivileged user
appuser(UID 10001, GID 10001). - Multi-Stage Build: Build tools (gcc, g++, make) are purged from the final runtime image.
- Automated Healthchecks: Healthchecks periodically verify
/healthendpoint, PostgreSQL connectivity, and Redis responsiveness.
Important
Historical Credential Notice:
In early development revisions (specifically ancestor commit 463affdfe750148aebfc098b70d17a7ebd759f20), a credential-shaped development placeholder was committed in configuration templates.
- Operational Status: This placeholder was strictly a local development template value and was never connected to or valid on any external production system.
- Eradication: The credential-shaped placeholder has been completely purged from all active tracked repository files (
.env.example,docker-compose.yml,frontend/app.js,frontend/index.html,README.md,tests/conftest.py). - Fail-Closed Block: The backend configuration in
backend/config.pyenforces fail-closed validation (is_insecure_bootstrap_key()), permanently blocking startup if development placeholders or template values are supplied whenENV=production. - Git History Notice: To maintain downstream Git clone integrity and preserve immutable commit SHAs on
mainwithout destructive force-pushes, historical commits are preserved under Option A. Full-history scanners (e.g. GitGuardian) will flag the historical ancestor commit, but active repository trees contain zero active credentials or literal tokens.
If you discover a potential security vulnerability within CiteBase, please report it responsibly:
- Email: security-reports@citebase.dev (or contact repository maintainer GurutejaReddy-04)
- Disclosure Policy: We request that you provide reasonable time for investigation and remediation before public disclosure. We commit to acknowledging receipt within 48 hours and providing regular remediation status updates.