Docs

Headers and CSP

Set frame policy, HSTS, and a nonce-based Content Security Policy.

Open Markdown

Framework responses get baseline hardening headers automatically, including a DENY frame policy. HSTS and CSP are configured here but off by default, because each deployment needs its own.

src/main.rs
use proa_framework_axum::{FrameOptions, SecurityHeadersConfig};

let security = SecurityHeadersConfig::new()
    .with_frame_options(FrameOptions::SameOrigin)
    .with_hsts_max_age_secs(63_072_000);

let app = FrameworkBuilder::new(manifest)
    .with_security_headers(security)
    .build();
SettingOptions
FrameOptionsDeny (default), SameOrigin, Disabled
HSTSOff by default. Enable only when TLS terminates before the app
CSPOff by default. Each app needs its own script, style, image, and connect policy

Use a nonce, not a static policy

A static Content-Security-Policy string works only when the response has no inline scripts. RSJS islands, React islands, and out-of-order streaming each need a fresh nonce per response, so reach for the strict policy instead:

src/main.rs
let app = FrameworkBuilder::new(manifest)
    .with_strict_csp()
    .build();

That mints a 128-bit nonce per request and sends:

base-uri 'self'; default-src 'self'; frame-ancestors 'none';
object-src 'none'; script-src 'nonce-…' 'strict-dynamic';
style-src 'self' 'unsafe-inline'

Directives are emitted in alphabetical order, so a captured header reads exactly like this.

Proa applies the same nonce to framework-emitted state, import-map, bootstrap, client-shell, and boundary-swap tags. So script-src needs no 'unsafe-inline' and no host allowlist.

Pairing the nonce with 'strict-dynamic' means a script loaded by an already-trusted script inherits trust. That is what makes bundler-emitted chunks work without enumerating CDN hosts.

Two things the layer cannot do for you. A raw <script> tag you write by hand needs an explicit nonce attribute, read from cx.csp_nonce() in a component or extracted with Extension<CspNonce> in a route handler (the layer stores the nonce in the request extensions). And inline event-handler attributes (onclick="…") never execute under any nonce policy, move them to an RSJS island or an addEventListener call inside a nonced script.

Tuning directives

Customize with CspPolicy:

src/main.rs
use proa_framework_axum::CspPolicy;

let app = FrameworkBuilder::new(manifest)
    .with_csp_policy(
        CspPolicy::strict()
            .with_directive("img-src", "'self' data: https://cdn.example")
            .with_directive("connect-src", "'self' https://api.example")
            .report_only(true),
    )
    .build();

report_only(true) sends Content-Security-Policy-Report-Only, which is how to roll a strict policy onto a live app. Violations are reported but nothing is blocked. Switch it off once the report stream is clean.

Style policy is separate from script policy. The default keeps style-src 'unsafe-inline' because inline style attributes are common and cannot execute code. Tighten it to 'self' once those styles live in classes.

Conflicts the builder warns about

The layer never overwrites a Content-Security-Policy header a handler already set, so a single route can opt out of the global policy.

Two combinations produce a startup warning:

Next steps

Search

Type at least 2 characters