Public Surface Security Checklist
This document drives the "baseline security checklist for externally exposed interfaces" under 11 / T5 security-baseline-for-operations.
Scope:
/public/*- Anonymous websocket / realtime connection entry points
- Anonymous authentication entry points such as
/api/v1/auth/login,/api/v1/auth/register,/api/v1/auth/refresh,/api/v1/auth/2fa/verify,/api/v1/auth/oidc/* - Public file access entry points such as
/uploads/* - Public APIs that may later be opened to third parties or end users
What Must Be Confirmed Before Adding an Exposed Interface
- Does this interface really need anonymous access?
- Can it be turned into a
management surfaceorservice surfaceinstead? - Would it leak internal identifiers such as tenant, workspace, agent, customer?
- Does it need pagination, filtering, or result trimming to prevent bulk enumeration?
- Does it need dedicated path-level rate limiting rather than relying only on the global limit?
- Would it return provider, infrastructure, or internal topology information?
- Do access events or anomalous traffic need auditing?
Minimum Checklist
Input
- Accept only parameters with an explicit schema
- Apply length and format limits to query, path, and header
- Do not trust client-supplied tenant / workspace scope
Output
- Never return secrets, tokens, internal configuration, or provider credentials
- Do not expose unnecessary internal IDs, database primary keys, or filesystem paths
- Error messages must not echo internal stacks, SQL, or third-party provider details
Traffic Control
- Must assess whether dedicated path-level rate limiting is needed
- Use stricter quotas on interfaces that can trigger expensive computation, retrieval, or uploads
- If websocket / streaming is supported, cap concurrent connections or connections per unit time
Browser Exposure
- Check that CORS allows only the necessary origins
- With a multi-origin allowlist, the server echoes back the matched Origin per request (with
Vary: Origin), and anything outside the allowlist gets noAccess-Control-Allow-Originat all; a multi-valued ACAO header must never be emitted (fixed in the first cut of 2026-09 P2-5: the previousstrings.Joinemitted an illegal multi-valued header that browsers universally reject) - If the interface can be embedded by pages or called from scripts, confirm whether extra origin / referer restrictions are needed
Observability
- Set up basic monitoring for anomalous traffic,
429,401,403 - Be able to trace issues via
X-Request-IDor an equivalent request identifier
Recommendations for Current Interfaces
/public/portal/config
- Return only branding and locale information
- Do not include internal tenant configuration, provider parameters, or feature flag details
- No dedicated rate limit is configured for this prefix: the endpoint is covered by the general
/public/prefix rate limit
/public/kb/*
- Expose only knowledge content that has been publicly published
- Result sets must support pagination / row limits
- Watch for scraping, enumeration, and full-text crawling risks
- The runtime baseline requires a dedicated path-level rate limit for
/public/kb/
/public/csat/*
- Avoid enumerating other people's satisfaction data via predictable IDs
- Apply a separate limit on submission frequency
- The runtime baseline requires a dedicated path-level rate limit for
/public/csat/
/public/suggestions/*
- Return only
is_publicknowledge document titles; no ticket / other-customer information - Exposure attribution writes one row to
suggestion_exposure_logsper successful response (a write-amplification surface), so a dedicated path-level rate limit is mandatory (P2-0 RQ-5; staging 45rpm / production 30rpm baseline) - The dedicated prefix must be listed before
/public/inrate_limiting.paths(the middleware takes the limit from the first matching prefix)
Anonymous /api/v1/auth/* entry points
/login,/register,/refreshmust have dedicated path-level rate limits/2fa/verify(the anonymous OTP challenge step) and/oidc/start|callback|statusbelong to this surface as well: the 2FA verification endpoint focuses on preventing OTP brute-force enumeration (the auth-prefix rate limit is 25rpm in dev, 30rpm in staging, and 20rpm in production), while the OIDC callback focuses on preventing open redirects and state forgery- Do not leak account existence, internal state, or policy details in error responses
- Set up alerts for brute-force attempts, refresh abuse, and bulk registration
- The global
security.max_body_bytesreturns a direct 413 for oversized requests (16MB in the production template)
/api/v1/ws
- The anonymous connection entry point must control connection frequency and concurrency
- Internal state, scheduling information, or tenant-sensitive metadata must not be exposed during the handshake
- The runtime baseline requires a dedicated path-level rate limit for
/api/v1/ws - When
security.websocket_allowed_originsis non-empty, any connection carrying an Origin header outside the allowlist gets a 403; an empty list stays permissive (anonymous visitor origins such as native WebViews cannot be enumerated); non-browser clients without an Origin header are let through (caught later by token/tenant validation)
/api/v1/ws/voice
- The voice translation channel (Phase 2 cut 2b-2): an anonymous connection surface of the same kind as
/api/v1/ws, in the same visitor token trust domain (same-origin check whenguest_token.requiredis enabled) - Wiring-layer gate: routes are not registered when
ai.asris not configured (the hub handshake 503 is the directly-constructed fallback), so the surface does not exist when the deployment has no voice capability - The handshake returns only missing
session_id(400) / invalid speaker (400) / token failure (401) / channel not wired (503), never internal state or provider details; stream-level errors are delivered via thevoice-errorcode vocabulary (disabled/asr_unavailable/stream_broken) and then the line is closed - Shares the same upgrader as
/api/v1/ws; thesecurity.websocket_allowed_originsallowlist (and the no-Origin-header pass-through) convention is fully identical - Path-level rate limiting reuses the
/api/v1/wsprefix entry (that prefix matches by prefix and covers/api/v1/ws/voice; no separate entry is created to avoid the same connection being counted twice)
/uploads/*
- Expose only uploaded assets with a genuine public need
- Do not echo real disk paths or internal storage key rules
- The runtime baseline requires a dedicated path-level rate limit for
/uploads/ - Local storage mode only allows hits on concrete files: directory listing, missing paths, and the storage root path all return 404 (gin Static is no longer mounted since the first cut of 2026-09 P2-5)
Runtime Security Baseline (Landed in the First Cut of 2026-09 P2-5)
The following capabilities have landed along with the security.headers / security.max_body_bytes / security.websocket_allowed_origins config sections, with real-run acceptance (scripts/test-public-surface-acceptance.sh, provider=public-surface):
- Unified security response headers:
X-Content-Type-Options: nosniff,X-Frame-Options,Referrer-Policy, optional CSP, optional HSTS (off by default: TLS is usually terminated at a reverse proxy, and HSTS is injected by the proxy). The security-header middleware is mounted before rate limiting / the body cap, so aborted responses such as429/413also carry the security headers. - Global request body cap
security.max_body_bytes: a Content-Length over the cap returns a direct 413, and chunked transfer is caught at read time byhttp.MaxBytesReader. - CORS multi-origin allowlist echoes per request +
Vary: Origin. - WS connection Origin allowlist.
- Local
/uploads/*serves only concrete files; directories and misses all return 404.
The production and staging templates (config.production.secure.example.yml / config.staging.example.yml) explicitly enable the baseline above; the development template config.yml keeps it off by default to preserve existing deployment behavior.
Relation to Existing Documentation
- Route surface classification: auth-surface-policy.md
- Runtime security baseline: security-baseline-operations.md
- token / key rotation: token-lifecycle-and-key-rotation.md
- Runtime acceptance:
make public-surface-acceptance(evidence archived underscripts/test-results/public-surface/, manifest provider=public-surface)