Skip to main content

Security Headers

What Your Agent Inherits

Every HTTP response from the agent’s endpoints automatically includes a full set of security headers: HSTS, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and Cache-Control. Your agent never has to set these headers manually. The SecurityHeadersMiddleware takes care of injecting them before the response reaches the client.


The Headers

The middleware attaches a conservative set of default headers to every HTTP response. Each one closes a specific browser-side attack vector:

HeaderDefault valuePurpose
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS for all future requests (only sent over HTTPS)
Content-Security-Policydefault-src 'none'; frame-ancestors 'none'Blocks all inline scripts, styles, and framing (XSS prevention)
X-Content-Type-OptionsnosniffPrevents MIME-type sniffing attacks
X-Frame-OptionsDENYPrevents clickjacking via iframe embedding
Referrer-Policyno-referrerStrips referrer information from outgoing requests
Permissions-Policygeolocation=(), camera=(), microphone=()Disables access to device APIs
Cache-Controlno-storePrevents caching of API responses

All headers get injected inside a single send_wrapper that intercepts the http.response.start ASGI event:

SecurityHeadersMiddleware View source
SecurityHeadersMiddleware
class SecurityHeadersMiddleware:
"""Attach conservative security headers to all HTTP responses."""
def __init__(
self,
app: ASGIApp,
*,
hsts_enabled: bool,
hsts_max_age_seconds: int,
referrer_policy: str,
permissions_policy: str,
content_security_policy: str,
trust_proxy_proto_header: bool,
trusted_proxies: list[str],
) -> None:
self.app = app
self.hsts_enabled = hsts_enabled
self.hsts_max_age_seconds = hsts_max_age_seconds
self.referrer_policy = referrer_policy
self.permissions_policy = permissions_policy
self.content_security_policy = content_security_policy
self.trust_proxy_proto_header = trust_proxy_proto_header
self.trusted_proxies = parse_trusted_proxies(trusted_proxies)

HSTS and proxy awareness: the HSTS header is only sent when the request arrived over HTTPS. Behind a reverse proxy, the middleware checks the X-Forwarded-Proto header, but only after it verifies that the client IP appears in the configured trusted_proxies list. Without that check, an untrusted client could inject a forwarded-proto header and trick the middleware into sending HSTS on a plain HTTP connection.

SecurityHeadersMiddleware.__call__ View source
SecurityHeadersMiddleware.__call__
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def send_wrapper(message: Message) -> None:
if message["type"] == "http.response.start":
headers = MutableHeaders(raw=message.setdefault("headers", []))
headers["X-Content-Type-Options"] = "nosniff"
headers["X-Frame-Options"] = "DENY"
headers["Referrer-Policy"] = self.referrer_policy
headers["Permissions-Policy"] = self.permissions_policy
headers["Cache-Control"] = "no-store"
if self.content_security_policy:
headers["Content-Security-Policy"] = self.content_security_policy
request_headers = Headers(raw=scope.get("headers", []))
client = scope.get("client")
client_host = client[0] if client else "unknown"
forwarded_proto = None
if self.trust_proxy_proto_header and is_trusted_proxy(
client_host, self.trusted_proxies
):
forwarded_proto = normalize_forwarded_proto(
request_headers.get("x-forwarded-proto")
)
scheme = forwarded_proto or scope.get("scheme", "http")
if self.hsts_enabled and scheme == "https":
headers["Strict-Transport-Security"] = (
f"max-age={self.hsts_max_age_seconds}; includeSubDomains"
)
await send(message)
await self.app(scope, receive, send_wrapper)

Automatic CSP Relaxation for API Docs

The strict production CSP (default-src 'none') blocks all external resources, which makes Swagger UI and ReDoc unusable. When either docs endpoint is enabled, the settings validator automatically extends the CSP with just the minimum directives needed for interactive documentation. The policy for regular API responses stays fully locked down:

_resolve_csp_for_docs View source
_resolve_csp_for_docs
def _resolve_csp_for_docs(settings: Settings) -> None:
"""Extend the default CSP when Swagger UI or ReDoc are enabled."""
if not (settings.docs_enabled or settings.redoc_enabled):
return
if settings.security_content_security_policy != _DEFAULT_CSP:
return
directives = [
"default-src 'none'",
"connect-src 'self' https://cdn.jsdelivr.net",
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
"img-src 'self' https://fastapi.tiangolo.com",
"frame-ancestors 'none'",
]
if settings.redoc_enabled:
directives[3] += " https://fonts.googleapis.com"
directives[4] += " data: https://cdn.redoc.ly"
directives.insert(-1, "font-src 'self' https://fonts.gstatic.com")
directives.insert(-1, "worker-src 'self' blob:")
settings.security_content_security_policy = "; ".join(directives)

If the operator has already provided a custom CSP, the resolver leaves it untouched. An explicit configuration always takes precedence.


Configuration

Every security header is configurable through environment variables, and the defaults are safe out of the box:

SettingDefaultDescription
APP_SECURITY_HEADERS_ENABLEDtrueMaster toggle for the middleware
APP_SECURITY_HSTS_ENABLEDfalseEnable HSTS (set true once HTTPS is confirmed)
APP_SECURITY_HSTS_MAX_AGE_SECONDS31536000HSTS max-age (1 year)
APP_SECURITY_REFERRER_POLICYno-referrerReferrer-Policy header value
APP_SECURITY_PERMISSIONS_POLICYgeolocation=(), camera=(), microphone=()Permissions-Policy
APP_SECURITY_CONTENT_SECURITY_POLICYdefault-src 'none'; frame-ancestors 'none'CSP (empty string disables)
APP_SECURITY_TRUST_PROXY_PROTO_HEADERfalseHonor X-Forwarded-Proto from trusted proxies
APP_SECURITY_TRUSTED_PROXIES[]CIDR list of trusted reverse proxies

Best Practices

  • Always start with a strict CSP (default-src 'none') and relax only as needed. A permissive default CSP provides no protection. Strict defaults with explicit allowlists are the only effective approach.
  • Never enable HSTS until you are certain HTTPS is working correctly. HSTS with a one-year max-age is effectively irreversible — browsers will refuse plain HTTP connections for the entire duration.
  • Always validate proxy trust before honoring X-Forwarded-Proto. Without IP-based validation, an untrusted client can inject a forwarded-proto header and manipulate HSTS behavior.
  • Prefer no-store over no-cache for API response Cache-Control. no-store prevents any caching of sensitive API responses, while no-cache still allows storage with revalidation.
  • Always set X-Frame-Options: DENY for API endpoints. APIs should never be rendered in iframes. This prevents clickjacking attacks with zero functional impact.

Further Reading


What the Agent Never Implements

The middleware layer handles all of the following concerns. Your agent’s generated code should never contain logic for:

  • Setting security headers. They are injected automatically on every response.
  • HSTS management. HTTPS detection and header injection live in the middleware.
  • CSP policy construction. Defaults are strict, and docs relaxation happens automatically.
  • Proxy protocol trust. The middleware validates the client IP against the trusted-proxies allowlist.
  • Cache-control on API responses. no-store is applied globally by the middleware.