# Architecture Hiding Plan: Sunaiva Technology Stack Concealment ## SECURITY PLAN | Priority: CRITICAL **Date**: 2026-02-16 **Author**: Genesis Security Team **Trigger**: Error message exposed "Telnyx" to end users -- unacceptable. **Scope**: All Sunaiva products (Talking Widget + AI Memory) --- ## Table of Contents 1. [Executive Summary](#1-executive-summary) 2. [Error Message Sanitization](#2-error-message-sanitization) 3. [HTTP Header Scrubbing](#3-http-header-scrubbing) 4. [Client-Side Code Obfuscation](#4-client-side-code-obfuscation) 5. [API Fingerprint Prevention](#5-api-fingerprint-prevention) 6. [Network-Level Protection](#6-network-level-protection) 7. [Stack Detection Tool Countermeasures](#7-stack-detection-tool-countermeasures) 8. [Implementation Priority Matrix](#8-implementation-priority-matrix) --- ## 1. Executive Summary A comprehensive audit of the Sunaiva codebase revealed **58+ references to "Telnyx"** in the client-side widget alone, plus multiple server-side leaks exposing internal service names, database IPs, provider identities, and infrastructure details. An attacker using Wappalyzer, BuiltWith, or manual inspection could trivially reconstruct our entire stack: FastAPI/uvicorn backend, Telnyx voice/WebRTC, Stripe billing, Gemini AI, PostgreSQL database on Elestio, and more. **Goal**: Make Sunaiva appear as a custom, proprietary platform with no identifiable third-party dependencies visible to any external observer. **Threat model**: Competitors, reverse engineers, security researchers, and automated scanners (Wappalyzer, BuiltWith, WhatRuns, Shodan) attempting to identify our technology stack. --- ## 2. Error Message Sanitization ### 2.1 Audit Findings #### CRITICAL: Webhook error response leaks internal exceptions **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/webhooks.py` **Line 787**: ```python result = {"status": "error", "event_type": event_type, "error": str(e)} ``` **Risk**: `str(e)` can contain Telnyx SDK error messages, database connection strings, module names, file paths, and stack trace fragments. **Fix**: ```python # BEFORE (LEAKS) result = {"status": "error", "event_type": event_type, "error": str(e)} # AFTER (SAFE) result = {"status": "error", "event_type": event_type} # Internal error details logged server-side only: logger.error("Webhook processing error: event_type=%s error=%s", event_type, e, exc_info=True) ``` #### CRITICAL: Billing tier validation exposes internal naming **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/billing.py` **Line 226**: ```python raise HTTPException(status_code=400, detail=f"Invalid tier: {tier}. Must be starter, pro, or growth.") ``` **Line 230**: ```python raise HTTPException(status_code=500, detail=f"Stripe price not configured for tier: {tier}") ``` **Risk**: Exposes tier names and reveals Stripe as the payment provider. **Fix**: ```python # Line 226 - Don't expose valid tier names in errors raise HTTPException(status_code=400, detail="Invalid subscription plan selected.") # Line 230 - Don't mention Stripe raise HTTPException(status_code=500, detail="Payment configuration error. Please contact support.") ``` #### CRITICAL: Auth defaults expose infrastructure IP **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/auth.py` **Lines 38-44**: ```python PG_CONFIG = { "host": os.environ.get("SUNAIVA_PG_HOST", "152.53.201.152"), "port": int(os.environ.get("SUNAIVA_PG_PORT", "5432")), "dbname": os.environ.get("SUNAIVA_PG_DB", "genesis_db"), "user": os.environ.get("SUNAIVA_PG_USER", "genesis_user"), "password": os.environ.get("SUNAIVA_PG_PASSWORD", ""), } ``` **Risk**: Default IP `152.53.201.152` is the Elestio/AIVA server. If env vars are missing, this IP is used and appears in error messages and logs. The database name `genesis_db` and user `genesis_user` leak the project name. **Fix**: ```python PG_CONFIG = { "host": os.environ.get("SUNAIVA_PG_HOST", "localhost"), "port": int(os.environ.get("SUNAIVA_PG_PORT", "5432")), "dbname": os.environ.get("SUNAIVA_PG_DB", "app_db"), "user": os.environ.get("SUNAIVA_PG_USER", "app_user"), "password": os.environ.get("SUNAIVA_PG_PASSWORD", ""), } ``` Also, the `.env` file in production MUST set all these values. The defaults should be safe localhost fallbacks, never real production IPs. #### CRITICAL: JWT secret has visible default **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/auth.py` **Line 34**: ```python JWT_SECRET = os.environ.get("JWT_SECRET", "sunaiva-widget-secret-change-in-production") ``` **Risk**: If `JWT_SECRET` env var is not set, this literal string becomes the signing key. Any attacker reading the source can forge tokens. **Fix**: ```python JWT_SECRET = os.environ.get("JWT_SECRET") if not JWT_SECRET: raise RuntimeError("JWT_SECRET environment variable is required. Refusing to start with default.") ``` #### HIGH: FastAPI validation errors expose framework identity FastAPI/Pydantic return validation errors in a distinctive format: ```json { "detail": [ { "loc": ["body", "email"], "msg": "field required", "type": "value_error.missing" } ] } ``` This format is a **unique fingerprint** for FastAPI. Any scanner recognizing this pattern instantly identifies the framework. **Fix** -- Add a global validation error handler in `app.py`: ```python from fastapi.exceptions import RequestValidationError @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Override FastAPI's default validation error format to prevent framework fingerprinting.""" # Extract just the human-readable messages, strip Pydantic internals errors = [] for error in exc.errors(): field = error.get("loc", [])[-1] if error.get("loc") else "unknown" msg = error.get("msg", "Invalid value") errors.append({"field": str(field), "message": msg}) return JSONResponse( status_code=422, content={"error": "Validation failed", "details": errors}, ) ``` #### HIGH: Generic exception handler needed **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/app.py` **Missing**: No global exception handler. Internal errors can propagate raw tracebacks. **Fix** -- Add to `app.py`: ```python from starlette.exceptions import HTTPException as StarletteHTTPException @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: StarletteHTTPException): """Sanitize HTTP error responses.""" return JSONResponse( status_code=exc.status_code, content={"error": exc.detail if isinstance(exc.detail, str) else "Request error"}, ) @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): """Catch-all: never expose internal errors to clients.""" logger.error("Unhandled exception on %s %s: %s", request.method, request.url.path, exc, exc_info=True) return JSONResponse( status_code=500, content={"error": "Internal server error"}, ) ``` #### MEDIUM: Webhook endpoint URL leaks provider name **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/webhooks.py` **Line 675**: `@router.post("/telnyx")` **Line 807**: `@router.get("/telnyx/health")` **Risk**: The URL `/webhooks/telnyx` is visible in Telnyx's dashboard config and in any network traffic logs. **Fix**: Rename to a generic path: ```python # BEFORE @router.post("/telnyx") @router.get("/telnyx/health") # AFTER @router.post("/voice-events") @router.get("/voice-events/health") ``` Update the Telnyx webhook URL in the Telnyx dashboard to point to `/webhooks/voice-events`. #### MEDIUM: Webhook health endpoint exposes subsystem name **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/webhooks.py` **Lines 814-819**: ```python return { "status": "healthy", "subsystem": "telnyx_webhooks", "signature_verification": "enabled" if TELNYX_PUBLIC_KEY else "disabled", "timestamp_tolerance_seconds": WEBHOOK_TIMESTAMP_TOLERANCE, } ``` **Fix**: ```python return { "status": "healthy", "subsystem": "voice_webhooks", "signature_verification": "enabled" if TELNYX_PUBLIC_KEY else "disabled", } # Remove timestamp_tolerance_seconds -- operational detail ``` #### LOW: Widget voice names expose Telnyx voice catalog **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/widgets.py` **Lines 48-50**: ```python ALLOWED_VOICES = { "eucalyptus", "marlu", "leda", "default", } ``` **Risk**: "eucalyptus", "marlu", "leda" are Telnyx NaturalHD voice names. Anyone familiar with Telnyx instantly identifies the provider. **Fix**: Use branded aliases: ```python # Map Sunaiva voice names to internal provider voices VOICE_ALIASES = { "aurora": "eucalyptus", # Australian female "harbor": "marlu", # Australian male "stellar": "leda", # Versatile natural "default": "default", } ALLOWED_VOICES = set(VOICE_ALIASES.keys()) ``` The API accepts `aurora`, `harbor`, `stellar`. Internally, the code maps to the real voice names when communicating with the voice provider. #### LOW: Leads module exposes AI model name **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/leads.py` **Line 43**: ```python GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash-exp") ``` **Risk**: Not directly user-facing, but if any error from the Gemini API propagates, it will contain the model name. **Fix**: Ensure all Gemini API calls are wrapped in try/except with sanitized error messages: ```python try: response = httpx.post(gemini_url, json=payload, timeout=30) response.raise_for_status() except Exception as e: logger.error("AI extraction failed: %s", e) return None # Never propagate the error ``` ### 2.2 AI Memory Server Leaks **File**: `/mnt/e/genesis-system/Sunaiva/ai-memory/server/main.py` **Lines 134-140**: FastAPI metadata exposes service identity (see Section 5.1 for fix). **Lines 449-456**: Health check exposes service name and version (see Section 5.2 for fix). ### 2.3 Enhance ContentFilter in security.py **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/security.py` Add provider name redaction to the existing `ContentFilter`: ```python # Add to ContentFilter.REDACT_PATTERNS list: # Voice/telecom provider names r"(?i)\btelnyx\b", r"(?i)\btwilio\b", r"(?i)\bvonage\b", r"(?i)\bplivo\b", # Payment provider names (when appearing in error context) r"(?i)stripe\s+(error|exception|api)", # AI model provider names r"(?i)\bgemini[\s-][\d.]+", r"(?i)\banthropic\b", r"(?i)\bopenai\b", # Infrastructure provider names r"(?i)\belestio\b", r"(?i)\bdigitalocean\b", r"(?i)\bhetzner\b", ``` Also add a **middleware** that runs ContentFilter on ALL API responses: ```python # In app.py, add AFTER SecurityHeadersMiddleware: class ResponseSanitizationMiddleware(BaseHTTPMiddleware): """Scan all JSON responses for leaked provider/infrastructure names.""" PROVIDER_PATTERNS = [ re.compile(r'(?i)\btelnyx\b'), re.compile(r'(?i)\bstripe\s+(error|api|exception)\b'), re.compile(r'(?i)\bgemini[\s-][\d.]+\b'), re.compile(r'(?i)\belestio\b'), re.compile(r'152\.53\.201\.152'), ] async def dispatch(self, request, call_next): response = await call_next(request) # Only scan JSON responses if response.headers.get("content-type", "").startswith("application/json"): body = b"" async for chunk in response.body_iterator: body += chunk if isinstance(chunk, bytes) else chunk.encode() body_str = body.decode("utf-8", errors="replace") for pattern in self.PROVIDER_PATTERNS: body_str = pattern.sub("[REDACTED]", body_str) return Response( content=body_str, status_code=response.status_code, headers=dict(response.headers), media_type=response.media_type, ) return response ``` **Note**: This middleware is a safety net. The primary fix is to prevent leaks at the source. --- ## 3. HTTP Header Scrubbing ### 3.1 Problem By default, uvicorn sets the `Server: uvicorn` header. FastAPI/Starlette may also leak framework identity through headers. ### 3.2 Current State The `SecurityHeadersMiddleware` in `app.py` (lines 95-115) adds security headers but does NOT remove identifying ones. ### 3.3 Fix: Strip Server Header in Middleware **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/app.py` Modify `SecurityHeadersMiddleware`: ```python class SecurityHeadersMiddleware(BaseHTTPMiddleware): """Add security headers and remove identifying headers from all responses.""" async def dispatch(self, request: StarletteRequest, call_next) -> StarletteResponse: response = await call_next(request) # ---- REMOVE identifying headers ---- # uvicorn sets "server: uvicorn" by default if "server" in response.headers: del response.headers["server"] # Remove any X-Powered-By that might be added by middleware if "x-powered-by" in response.headers: del response.headers["x-powered-by"] # ---- ADD security headers ---- response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" response.headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'" return response ``` ### 3.4 Fix: Override uvicorn's Server Header at Launch The uvicorn `Server` header is added at the ASGI server level, BEFORE middleware runs. The middleware approach above works for responses that flow through Starlette, but uvicorn may still emit its header for certain edge cases. **Option A**: Use `--header` flag (uvicorn 0.27+): ```bash uvicorn app:app --host 0.0.0.0 --port 8080 --header Server:Sunaiva ``` **Option B**: Use Gunicorn with uvicorn workers (recommended for production): ```bash gunicorn app:app -w 4 -k uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8080 \ --forwarded-allow-ips="*" ``` Gunicorn does not add a `Server` header by default. **Option C**: Cloudflare will strip the origin `Server` header when proxying (see Section 6). ### 3.5 Headers to Remove | Header | Source | Risk | |--------|--------|------| | `Server: uvicorn` | uvicorn ASGI server | Identifies Python ASGI framework | | `X-Powered-By` | Various middleware | Identifies framework | | `X-Process-Time` | Sometimes added by middleware | Reveals internal processing details | ### 3.6 Headers to Keep (Already Set) | Header | Value | Purpose | |--------|-------|---------| | `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing | | `X-Frame-Options` | `DENY` | Prevent clickjacking | | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Force HTTPS | | `Referrer-Policy` | `strict-origin-when-cross-origin` | Control referrer leaks | | `Content-Security-Policy` | `default-src 'none'; frame-ancestors 'none'` | Restrict resource loading | --- ## 4. Client-Side Code Obfuscation ### 4.1 Audit Findings **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/widget/v1/widget.js` This is the **highest priority** leak vector. The widget JS is served directly to end-user browsers, where anyone can `View Source`. **58 references to "Telnyx" or "telnyx"** found, including: | Line | Content | Severity | |------|---------|----------| | 5 | `Wraps the Telnyx AI Agent WebRTC SDK` (comment) | HIGH | | 36 | `TELNYX_WIDGET_URL = 'https://unpkg.com/@telnyx/ai-agent-widget@0.31.1'` | CRITICAL | | 610-611 | `.sw-telnyx-wrap` CSS class | MEDIUM | | 643-645 | `this.telnyxLoaded`, `this.telnyxEl`, `this.telnyxBtn` | HIGH | | 902 | `_buildTelnyxWrapper` function | HIGH | | 907 | `wrapper.id = NAMESPACE + '-telnyx'` | MEDIUM | | 909 | `document.createElement('telnyx-ai-agent')` | CRITICAL | | 966 | `_loadTelnyxSDK` function | HIGH | | 970 | `script[data-sunaiva-telnyx]` selector | MEDIUM | | 976 | `customElements.get('telnyx-ai-agent')` | CRITICAL | | 1046-1095 | `_triggerTelnyxCall`, `_findAndClickTelnyxButton` | HIGH | | 1157-1161 | `_bindTelnyxEvents` function | HIGH | ### 4.2 Phase 1: Source Code Renaming (IMMEDIATE) Before any minification, rename all Telnyx references in the source to generic names: ```javascript // BEFORE: var TELNYX_WIDGET_URL = 'https://unpkg.com/@telnyx/ai-agent-widget@0.31.1'; this.telnyxLoaded = false; this.telnyxEl = null; this.telnyxBtn = null; SunaivaWidget.prototype._buildTelnyxWrapper = function() { ... }; SunaivaWidget.prototype._loadTelnyxSDK = function() { ... }; SunaivaWidget.prototype._triggerTelnyxCall = function() { ... }; SunaivaWidget.prototype._findAndClickTelnyxButton = function() { ... }; SunaivaWidget.prototype._bindTelnyxEvents = function() { ... }; // AFTER: var _VC_SDK_URL = 'https://unpkg.com/@telnyx/ai-agent-widget@0.31.1'; // URL still needed this._vcReady = false; this._vcEl = null; this._vcBtn = null; SunaivaWidget.prototype._buildVoiceContainer = function() { ... }; SunaivaWidget.prototype._loadVoiceSDK = function() { ... }; SunaivaWidget.prototype._initiateVoiceCall = function() { ... }; SunaivaWidget.prototype._activateCallButton = function() { ... }; SunaivaWidget.prototype._bindVoiceEvents = function() { ... }; ``` CSS class rename: ```javascript // BEFORE: '.sw-telnyx-wrap' // AFTER: '.sw-vc-wrap' ``` DOM element attribute rename: ```javascript // BEFORE: script.setAttribute('data-sunaiva-telnyx', 'true'); document.querySelector('script[data-sunaiva-telnyx]'); // AFTER: script.setAttribute('data-sw-vc', 'true'); document.querySelector('script[data-sw-vc]'); ``` ID rename: ```javascript // BEFORE: wrapper.id = NAMESPACE + '-telnyx'; // AFTER: wrapper.id = NAMESPACE + '-vc'; ``` Comment removal: ```javascript // REMOVE line 5 entirely: // * Wraps the Telnyx AI Agent WebRTC SDK with Sunaiva branding, // REPLACE WITH: // * Self-contained voice AI widget with Sunaiva branding, ``` ### 4.3 Phase 2: Self-Hosted SDK (HIGH PRIORITY) The `@telnyx/ai-agent-widget` package is loaded from `unpkg.com`, which immediately reveals Telnyx. **This is the single biggest leak.** **Fix**: Self-host the Telnyx SDK: 1. Download the SDK bundle: ```bash curl -o sunaiva-voice-sdk.js 'https://unpkg.com/@telnyx/ai-agent-widget@0.31.1' ``` 2. Host it on our own CDN at: ``` https://widget.sunaiva.ai/v1/voice-sdk.js ``` 3. Update the widget: ```javascript // BEFORE: var _VC_SDK_URL = 'https://unpkg.com/@telnyx/ai-agent-widget@0.31.1'; // AFTER: var _VC_SDK_URL = 'https://widget.sunaiva.ai/v1/voice-sdk.js'; ``` 4. Strip or rename identifiers inside the SDK bundle using sed/Terser: ```bash # After downloading, rename the custom element registration # The SDK registers 'telnyx-ai-agent' as a custom element # This CANNOT be renamed without forking the SDK ``` **Important limitation**: The SDK registers a `` custom HTML element. The `customElements.define('telnyx-ai-agent', ...)` call inside the SDK binds to that name. Changing this requires forking the SDK repository. **Mitigation**: The custom element lives inside the Sunaiva widget's wrapper div with `display:none`. Standard DOM inspection by users is unlikely, but automated scanners checking `customElements` could find it. See Phase 3 for advanced mitigation. ### 4.4 Phase 3: Build Pipeline with Terser + Webpack (MEDIUM PRIORITY) Set up a proper build pipeline to minify, mangle, and obfuscate the widget: **package.json** (create in `Sunaiva/talking-widget/widget/`): ```json { "name": "sunaiva-widget-build", "version": "1.0.0", "private": true, "scripts": { "build": "webpack --mode production", "build:dev": "webpack --mode development" }, "devDependencies": { "webpack": "^5.90.0", "webpack-cli": "^5.1.4", "terser-webpack-plugin": "^5.3.10" } } ``` **webpack.config.js**: ```javascript const TerserPlugin = require('terser-webpack-plugin'); const path = require('path'); module.exports = { entry: './v1/widget.js', output: { filename: 'widget.min.js', path: path.resolve(__dirname, 'dist'), }, optimization: { minimize: true, minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: false, // Keep console.error for debugging passes: 2, }, mangle: { properties: { // Mangle internal property names that start with underscore regex: /^_[a-z]/, }, }, output: { comments: false, // Strip ALL comments (removes Telnyx references) beautify: false, }, }, }), ], }, }; ``` ### 4.5 Phase 4: Advanced -- Fork Telnyx SDK (FUTURE) For complete concealment, fork the `@telnyx/ai-agent-widget` npm package: 1. Clone the source from GitHub 2. Rename the custom element from `telnyx-ai-agent` to `sw-voice-agent` 3. Strip all Telnyx branding from the compiled output 4. Publish as a private package or bundle inline 5. This eliminates the final fingerprint **Effort**: 2-4 hours. **Priority**: Do after launch when revenue is flowing. --- ## 5. API Fingerprint Prevention ### 5.1 Disable OpenAPI Documentation in Production **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/app.py` **Lines 51-58** (CURRENT): ```python app = FastAPI( title="Sunaiva Talking Widget API", description="Backend API for Sunaiva AI Voice Widget platform. " "Handles customer auth, billing, widget management, and analytics.", version="1.0.0", docs_url="/docs", redoc_url="/redoc", ) ``` **Fix**: ```python # Disable docs in production, enable only in development _ENABLE_DOCS = os.environ.get("ENABLE_API_DOCS", "false").lower() == "true" app = FastAPI( title="API", # Generic title description="", # No description version="1.0.0", docs_url="/docs" if _ENABLE_DOCS else None, redoc_url="/redoc" if _ENABLE_DOCS else None, openapi_url="/openapi.json" if _ENABLE_DOCS else None, # CRITICAL: disable schema endpoint too ) ``` The `/openapi.json` endpoint is the most dangerous -- it contains the **entire API schema** including all routes, parameter names, and response models. Disabling `docs_url` alone is NOT sufficient because `/openapi.json` remains accessible. Do the same for AI Memory: **File**: `/mnt/e/genesis-system/Sunaiva/ai-memory/server/main.py` ```python _ENABLE_DOCS = os.environ.get("ENABLE_DOCS", "false").lower() == "true" app = FastAPI( title="API", description="", version="1.0.0", docs_url="/docs" if _ENABLE_DOCS else None, redoc_url=None, openapi_url="/openapi.json" if _ENABLE_DOCS else None, ) ``` ### 5.2 Sanitize Health Check Endpoints Health endpoints are the easiest reconnaissance targets. **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/app.py` **Lines 273-300** (CURRENT): ```python return { "status": "healthy", "service": "sunaiva-talking-widget", "version": "1.0.0", "database": pg_status, "stripe_configured": stripe_configured, "jwt_configured": jwt_configured, } ``` **Fix** -- Minimal public health + detailed internal health: ```python @app.get("/health") def health_check(): """Public health check -- minimal information only.""" return {"status": "ok"} @app.get("/health/detailed") def health_check_detailed(authorization: str = Header(None)): """Internal health check with full diagnostics. Requires admin JWT.""" # Require admin authentication if not authorization: raise HTTPException(status_code=401, detail="Unauthorized") # ... existing detailed health check logic, only for admins ... ``` Same for AI Memory health: ```python @app.get("/api/health") async def health(): """Public health check.""" return {"status": "ok"} ``` ### 5.3 Remove API Root Endpoint Map **File**: `/mnt/e/genesis-system/Sunaiva/talking-widget/backend/app.py` **Lines 344-422**: The root endpoint returns a complete map of ALL API endpoints. **Fix**: ```python @app.get("/") def root(): """API root.""" return {"status": "ok"} ``` Move the endpoint documentation to a private internal doc or the admin panel only. ### 5.4 Custom Error Responses Standard FastAPI/Starlette error formats are fingerprints: | Status | Default Format | Fingerprint Risk | |--------|---------------|-----------------| | 404 | `{"detail": "Not Found"}` | Matches FastAPI exactly | | 405 | `{"detail": "Method Not Allowed"}` | Matches FastAPI exactly | | 422 | `{"detail": [{"loc": [...], "msg": "...", "type": "..."}]}` | UNIQUE to FastAPI/Pydantic | | 500 | `Internal Server Error` (plain text) | Matches Starlette | **Fix** -- Override ALL default error handlers: ```python from starlette.exceptions import HTTPException as StarletteHTTPException from fastapi.exceptions import RequestValidationError @app.exception_handler(404) async def not_found_handler(request: Request, exc): return JSONResponse(status_code=404, content={"error": "Not found"}) @app.exception_handler(405) async def method_not_allowed_handler(request: Request, exc): return JSONResponse(status_code=405, content={"error": "Method not allowed"}) @app.exception_handler(RequestValidationError) async def validation_handler(request: Request, exc: RequestValidationError): errors = [] for e in exc.errors(): field = e.get("loc", [])[-1] if e.get("loc") else "unknown" errors.append({"field": str(field), "message": e.get("msg", "Invalid value")}) return JSONResponse(status_code=422, content={"error": "Validation failed", "details": errors}) @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: StarletteHTTPException): return JSONResponse( status_code=exc.status_code, content={"error": exc.detail if isinstance(exc.detail, str) else "Request error"}, ) @app.exception_handler(Exception) async def generic_handler(request: Request, exc: Exception): logger.error("Unhandled: %s %s: %s", request.method, request.url.path, exc, exc_info=True) return JSONResponse(status_code=500, content={"error": "Internal server error"}) ``` ### 5.5 Consistent Error Response Schema All errors MUST follow the same shape across both products: ```json { "error": "Human-readable message", "details": [...] // Optional, only for validation errors } ``` Never: - `{"detail": "..."}` (FastAPI default) - `{"message": "..."}` (Express default) - Raw HTML error pages - Stack traces --- ## 6. Network-Level Protection ### 6.1 Cloudflare Reverse Proxy (REQUIRED) Cloudflare is the **single most impactful** defense: 1. **Hides origin server IP**: Visitors see Cloudflare IPs, not `152.53.201.152` 2. **Overwrites `Server` header**: Cloudflare sets `Server: cloudflare` 3. **Adds DDoS protection**: Free tier includes basic protection 4. **Provides WAF**: Blocks common attack patterns 5. **Caches static assets**: Reduces origin fingerprinting opportunities 6. **SSL termination**: Prevents SSL certificate fingerprinting of origin **Setup for Sunaiva domains**: ``` sunaiva.ai → Cloudflare proxy → Elestio origin (152.53.201.152) api.sunaiva.ai → Cloudflare proxy → Elestio origin :8080 widget.sunaiva.ai → Cloudflare proxy → Static files (widget.js, SDK) sunaivadigital.com → Cloudflare proxy → Same origin ``` **Cloudflare settings**: ``` SSL/TLS: Full (Strict) Always Use HTTPS: On Minimum TLS: 1.2 Auto Minify: JS, CSS, HTML (further obfuscates widget.js) Rocket Loader: Off (can break widget loading) Under Attack Mode: Available (toggle during attacks) Bot Fight Mode: On ``` ### 6.2 Origin IP Protection Even with Cloudflare, the origin IP can leak through: 1. **DNS history**: Sites like SecurityTrails, ViewDNS show historical A records. **Mitigation**: Set up Cloudflare BEFORE the first DNS record points to the real IP. If already exposed, consider moving to a new IP behind Cloudflare. 2. **Direct IP access**: If someone hits `http://152.53.201.152:8080` directly. **Mitigation**: Configure the origin firewall to only accept connections from Cloudflare IP ranges: ```bash # On Elestio server (iptables example) # Allow Cloudflare IPv4 ranges for ip in $(curl -s https://www.cloudflare.com/ips-v4); do iptables -A INPUT -p tcp -s $ip --dport 8080 -j ACCEPT done # Block all other access to port 8080 iptables -A INPUT -p tcp --dport 8080 -j DROP ``` 3. **SSL certificate**: The origin SSL cert may be issued to `sunaiva.ai`, linking the IP. **Mitigation**: Use Cloudflare Origin Certificates (only valid between Cloudflare and origin). 4. **Email headers**: Outgoing emails from the server include the origin IP in headers. **Mitigation**: Use Resend (already configured) for all email. Never send email directly from the app server. ### 6.3 WebSocket/WebRTC Proxy for Telnyx The Telnyx AI Agent widget establishes WebRTC connections directly to Telnyx TURN/STUN servers. This is visible in the browser's Network tab: ``` wss://wss.telnyx.com/... stun:stun.telnyx.com turn:turn.telnyx.com ``` **Mitigation levels**: **Level 1 (Minimal -- do NOW)**: Accept that WebRTC STUN/TURN server addresses are visible in the browser's network tab. Most users never check. Focus on hiding Telnyx from source code and headers. **Level 2 (Moderate -- post-launch)**: Set up a TURN relay proxy: ``` Client WebRTC → turn.sunaiva.ai → Telnyx TURN servers ``` This requires a TURN server (coturn) that proxies to Telnyx's TURN servers. The Telnyx SDK would need configuration to use our custom TURN URL. **Level 3 (Maximum -- future)**: Fork the Telnyx SDK and hardcode `sunaiva.ai` domain endpoints for all WebSocket/WebRTC connections. Proxy all traffic through our infrastructure. **Recommended**: Level 1 now, Level 2 after first 50 customers. ### 6.4 Custom Domain for Webhook Ingress Currently, Telnyx sends webhooks to a URL containing our domain. This URL is configured in the Telnyx dashboard and is not visible to end users. However, for completeness: ``` Webhook URL: https://api.sunaiva.ai/webhooks/voice-events ``` This is already safe (our domain, generic path). The rename from `/webhooks/telnyx` to `/webhooks/voice-events` (Section 2) completes this. --- ## 7. Stack Detection Tool Countermeasures ### 7.1 Wappalyzer **What it detects**: HTTP headers, JavaScript global variables, HTML meta tags, cookies, DNS records, CSS class patterns. **How it identifies FastAPI/uvicorn**: - `Server: uvicorn` header - `/docs` and `/redoc` endpoints (OpenAPI) - `/openapi.json` endpoint - Pydantic validation error format **How it identifies Telnyx**: - `` custom element in DOM - Telnyx SDK script loaded from `unpkg.com/@telnyx/` - `wss://wss.telnyx.com` WebSocket connections **How it identifies Stripe**: - `stripe.js` script inclusion (only on checkout pages) - Stripe Checkout redirect URLs **Countermeasures already covered**: - [x] Remove `Server: uvicorn` header (Section 3) - [x] Disable `/docs`, `/redoc`, `/openapi.json` in production (Section 5.1) - [x] Override Pydantic error format (Section 5.4) - [x] Self-host Telnyx SDK (Section 4.3) - [x] Rename Telnyx references in widget.js (Section 4.2) **Additional Wappalyzer-specific countermeasures**: ```python # Remove any Python/uvicorn cookies if present # FastAPI's default session middleware uses "session" cookie -- don't use it # Use a custom cookie name if sessions are needed: app.add_middleware( SessionMiddleware, secret_key=os.environ["SESSION_SECRET"], session_cookie="sw_s", # Non-identifying name ) ``` ### 7.2 BuiltWith **What it detects**: HTTP headers, JavaScript libraries, CSS frameworks, analytics scripts, meta tags, DNS records, SSL certificates, CDN detection. **How it identifies our stack**: - `Server` header (uvicorn) - `X-Powered-By` header (if present) - CDN detection (unpkg.com for Telnyx SDK) - SSL certificate issuer (Let's Encrypt vs Cloudflare) - JavaScript library fingerprints (function signatures, global variables) **Additional countermeasures**: - Self-host ALL third-party scripts (no unpkg.com, no cdn.jsdelivr.net) - Use Cloudflare SSL (hides Let's Encrypt/origin cert from BuiltWith) - Remove any analytics scripts during initial launch (add them back via Cloudflare Workers to mask origin) ### 7.3 WhatRuns **What it detects**: Chrome extension that analyzes page resources, DOM elements, JavaScript globals, and network requests. **How it identifies our stack**: - JavaScript global variables (`window.Stripe`, Telnyx custom elements) - Network requests to known service domains (telnyx.com, stripe.com) - HTML element patterns **Countermeasures**: - Widget uses Shadow DOM (already implemented) -- WhatRuns has limited visibility into shadow roots - Self-host Telnyx SDK (prevents CDN fingerprinting) - Stripe.js is loaded only on checkout pages (minimal exposure) ### 7.4 Shodan / Censys **What they detect**: Direct IP scanning, open ports, SSL certificates, HTTP response headers, banner grabbing. **How they identify our stack**: - Port scan reveals 8080 (Python/uvicorn) - Banner grab shows `Server: uvicorn` - SSL certificate shows domain names **Countermeasures**: - Cloudflare (hides origin IP) -- primary defense - Origin firewall: only accept Cloudflare IPs (Section 6.2) - Change application port from 8080 (commonly associated with dev servers) to 443 behind Cloudflare ### 7.5 Manual Reconnaissance **Techniques attackers use**: 1. `curl -I https://api.sunaiva.ai/` -- check response headers 2. `curl https://api.sunaiva.ai/docs` -- check for OpenAPI 3. View Source on widget page -- find script tags 4. Browser DevTools Network tab -- watch WebSocket/XHR calls 5. `nslookup` / `dig` -- find origin IP from DNS history **Countermeasures** (all covered in this plan): 1. Headers scrubbed (Section 3) 2. Docs disabled (Section 5.1) 3. JS obfuscated, Telnyx references removed (Section 4) 4. Network calls minimized, self-hosted SDK (Section 4.3, 6.3) 5. Cloudflare proxy hides origin (Section 6.1) ### 7.6 Fingerprinting Verification Checklist After implementing all changes, run these tests to verify concealment: ```bash # 1. Header check curl -sI https://api.sunaiva.ai/ | grep -i "server\|powered\|x-process" # Expected: No uvicorn, no python, no fastapi # 2. OpenAPI check curl -s https://api.sunaiva.ai/docs curl -s https://api.sunaiva.ai/redoc curl -s https://api.sunaiva.ai/openapi.json # Expected: All return 404 # 3. Health check information leak curl -s https://api.sunaiva.ai/health # Expected: {"status": "ok"} -- nothing else # 4. Root endpoint curl -s https://api.sunaiva.ai/ # Expected: {"status": "ok"} -- no endpoint map # 5. Error format (send bad request) curl -s -X POST https://api.sunaiva.ai/auth/login -H "Content-Type: application/json" -d '{}' # Expected: {"error": "Validation failed", "details": [...]} -- NOT FastAPI default format # 6. Widget source check curl -s https://widget.sunaiva.ai/v1/widget.js | grep -i telnyx # Expected: No matches (after obfuscation) # 7. Wappalyzer browser extension # Install Wappalyzer, visit sunaiva.ai # Expected: No "uvicorn", "FastAPI", "Telnyx" detected # 8. Direct IP access curl -sI http://152.53.201.152:8080/ # Expected: Connection refused (after firewall rules) ``` --- ## 8. Implementation Priority Matrix ### P0 -- CRITICAL (Before Launch) | # | Task | File(s) | Effort | Impact | |---|------|---------|--------|--------| | 1 | Remove `str(e)` from webhook error response | `webhooks.py:787` | 5 min | Prevents runtime error leaks | | 2 | Remove hardcoded IP default in PG config | `auth.py:38-44` | 5 min | Prevents IP leak if env missing | | 3 | Remove default JWT secret (require env var) | `auth.py:34` | 5 min | Prevents token forgery | | 4 | Disable `/docs`, `/redoc`, `/openapi.json` in prod | `app.py:51-58` | 10 min | Removes full API schema | | 5 | Strip `Server: uvicorn` header | `app.py` middleware | 10 min | Hides ASGI framework | | 6 | Sanitize billing error messages | `billing.py:226,230` | 5 min | Hides provider names | | 7 | Add global exception handlers | `app.py` | 15 min | Prevents ALL stack trace leaks | | 8 | Override FastAPI validation error format | `app.py` | 10 min | Removes framework fingerprint | | 9 | Sanitize health check (minimal public response) | `app.py:273-300` | 10 min | Hides internal topology | | 10 | Remove API endpoint map from root | `app.py:344-422` | 5 min | Removes recon goldmine | **Total P0 effort: ~80 minutes** ### P1 -- HIGH (First Week Post-Launch) | # | Task | File(s) | Effort | Impact | |---|------|---------|--------|--------| | 11 | Rename all Telnyx references in widget.js | `widget.js` (58 occurrences) | 2 hours | Hides voice provider from source | | 12 | Self-host Telnyx SDK (no unpkg.com) | Widget build, CDN config | 1 hour | Hides SDK origin | | 13 | Rename webhook URL from `/telnyx` to `/voice-events` | `webhooks.py`, Telnyx dashboard | 15 min | Hides provider in URL path | | 14 | Sanitize webhook health endpoint | `webhooks.py:807-819` | 5 min | Hides subsystem name | | 15 | Brand voice names (aurora/harbor/stellar) | `widgets.py:48-50` | 30 min | Hides voice catalog identity | | 16 | Apply same fixes to AI Memory server | `ai-memory/server/main.py` | 30 min | Consistent across products | | 17 | Set up Cloudflare proxy for all domains | DNS, Cloudflare dashboard | 1 hour | Hides origin IP, scrubs headers | **Total P1 effort: ~5.5 hours** ### P2 -- MEDIUM (First Month) | # | Task | File(s) | Effort | Impact | |---|------|---------|--------|--------| | 18 | Webpack/Terser build pipeline for widget.js | New build config | 2 hours | Minification + mangling | | 19 | Origin firewall (Cloudflare IPs only) | Server iptables | 30 min | Prevents direct IP scanning | | 20 | Add ResponseSanitizationMiddleware | `app.py` | 1 hour | Safety net for all responses | | 21 | Add ContentFilter provider patterns | `security.py` | 30 min | Catches missed leaks | | 22 | Wappalyzer/BuiltWith verification test suite | Test scripts | 1 hour | Ongoing verification | **Total P2 effort: ~5 hours** ### P3 -- LOW (Future) | # | Task | Effort | Impact | |---|------|--------|--------| | 23 | Fork Telnyx SDK, rename custom element | 4 hours | Eliminates DOM fingerprint | | 24 | Custom TURN relay (hide WebRTC endpoints) | 8 hours | Hides voice traffic destination | | 25 | Move to Gunicorn in production | 1 hour | Eliminates uvicorn header at source | --- ## Appendix A: Complete File Leak Inventory | File | Leak Type | Details | Priority | |------|-----------|---------|----------| | `backend/app.py:51-58` | API metadata | Title, description, docs URLs | P0 | | `backend/app.py:273-300` | Health check | Service name, version, DB status, Stripe/JWT config status | P0 | | `backend/app.py:344-422` | API map | Complete endpoint listing | P0 | | `backend/auth.py:34` | JWT secret | Default value in source code | P0 | | `backend/auth.py:38-44` | Database IP | Hardcoded `152.53.201.152` as default | P0 | | `backend/billing.py:226` | Error detail | Exposes valid tier names | P0 | | `backend/billing.py:230` | Error detail | Mentions "Stripe" in error | P0 | | `backend/webhooks.py:675` | URL path | `/telnyx` in endpoint URL | P1 | | `backend/webhooks.py:787` | Error detail | Raw `str(e)` in response | P0 | | `backend/webhooks.py:807-819` | Health check | Exposes "telnyx_webhooks" subsystem | P1 | | `backend/widgets.py:48-50` | Voice names | Telnyx voice catalog names | P1 | | `backend/leads.py:43` | Config | Gemini model name in defaults | LOW | | `backend/rate_limiter.py:9` | Comment | "Telnyx bursts" (source only) | LOW | | `backend/admin_api.py:18` | Docstring | "Telnyx" in health description | LOW | | `backend/models.py:158,349` | Schema | `telnyx_assistant_id` field name | LOW | | `widget/v1/widget.js:36` | SDK URL | `unpkg.com/@telnyx/ai-agent-widget` | P1 | | `widget/v1/widget.js` (58 refs) | Source code | Variable names, functions, comments | P1 | | `ai-memory/server/main.py:134-140` | API metadata | Title, description | P1 | | `ai-memory/server/main.py:449-456` | Health check | Service name, version, testing_mode | P1 | | HTTP headers | Server header | `Server: uvicorn` | P0 | | HTTP responses | Error format | FastAPI/Pydantic validation format | P0 | ## Appendix B: Database Column Names These column names are internal and not exposed to users unless included in API responses. However, any admin/debug endpoint that returns raw DB rows would leak them: - `telnyx_assistant_id` (in `widgets` table) - `stripe_customer_id` (in `customers` table) - `stripe_subscription_id` (in `subscriptions` table) **Recommendation**: Never return raw DB rows. Always map through response models that use generic field names: - `telnyx_assistant_id` -> `voice_agent_id` (in API responses) - `stripe_customer_id` -> `billing_id` (in API responses) - `stripe_subscription_id` -> `subscription_id` (in API responses) **Database migration (future)**: Consider renaming the actual DB columns to generic names during a maintenance window. This is LOW priority since the DB schema is internal. --- **End of Plan** *This document should be treated as CONFIDENTIAL. Do not commit to any public repository.*