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::axum::{FrameOptions, SecurityHeadersConfig};

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

let app = FrameworkBuilder::new()
    .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 policy can authorize fixed inline scripts with hashes. Framework-generated scripts can contain response-specific state, so a fresh nonce per response is the convenient policy for RSJS islands, React islands, and out-of-order streaming:

src/main.rs
let app = FrameworkBuilder::new()
    .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.

Managed module tags registered with cx.head_mut().module(...) also receive the installed nonce automatically. Handwritten template tags need the explicit nonce described below.

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). A nonce on a <script> does not authorize inline event-handler attributes (onclick="…"); use typed RSJS handlers for application behavior.

Tuning directives

Customize with CspPolicy:

src/main.rs
use proa::axum::CspPolicy;

let app = FrameworkBuilder::new()
    .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