Security Headers
The security_headers_middleware automatically injects a set of security headers into every response, in addition to the CSP header. It runs unconditionally, on every response, with no builder call needed — .with_header_security(true) doesn't add anything, these headers already ship.
Injected headers
| Header | Value | Protection |
|---|---|---|
Content-Security-Policy | Dynamic (unique nonce per request) | Restricts allowed sources for scripts, styles, images, etc. |
X-Content-Type-Options | nosniff | Prevents the browser from guessing the MIME type — blocks MIME sniffing attacks |
X-Frame-Options | DENY | Prevents embedding the page in an iframe — protects against clickjacking |
X-XSS-Protection | 1; mode=block | Enables the XSS filter in legacy browsers (older IE/Edge) |
Referrer-Policy | strict-origin-when-cross-origin | Sends full referrer on same-origin, origin only on cross-origin, nothing on HTTP→HTTPS |
Permissions-Policy | Secure preset (see below) | Denies ~20 sensitive features (camera, microphone, geolocation, USB, Bluetooth, payment, sensors…); allows WebAuthn, fullscreen and picture-in-picture same-origin |
Cross-Origin-Embedder-Policy | require-corp | Requires cross-origin resources to be explicitly opted in (CORP) |
Cross-Origin-Opener-Policy | same-origin | Isolates the browsing context — prevents cross-origin attacks via window.opener |
Cross-Origin-Resource-Policy | same-origin | Prevents resources from being loaded by other origins |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Enforces HTTPS for 1 year, subdomains included (HSTS) |
Activation
These headers ship on every response with zero configuration. There is no "CSP only, without the other headers" variant — the two examples below produce the same result in practice; only the second customizes the nonce.
Default (nothing to do)
.middleware(|m| {
m.with_csp(|c| c)
})
Customize the nonce
.middleware(|m| {
m.with_csp(|c| {
c.with_nonce(true)
})
})
Full strict preset
.middleware(|m| {
m.with_csp(|c| {
c.policy(SecurityPolicy::strict())
})
})
Notes
Reverse proxy (Nginx, Caddy, Cloudflare…) — Runique sends all these headers on every dynamic response. A reverse proxy configured with proxy_hide_header or duplicate add_header directives can silently overwrite them. In production, do not declare these headers in Nginx — let them pass through from the application as-is.
For static files served directly by Nginx (assets, media), the headers do not go through Runique: they must be declared explicitly in the relevant location block:
location /media/ {
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "DENY" always;
}
HSTS (Strict-Transport-Security) — This header is emitted only when Runique actually serves HTTPS: either enforce_https or ACME enabled (should_emit_hsts()). Sending it over plain HTTP is useless (browsers ignore it) and risky (a one-year HTTPS lock-in on a domain that may not be ready). If your proxy (nginx, Caddy, Cloudflare…) terminates TLS without Runique knowing, declare the header on the proxy side.
The value is configurable (single source of truth, same settings everywhere: middleware, error pages):
| Env variable | Default | Purpose |
|---|---|---|
HSTS_MAX_AGE | 31536000 (1 year) | max-age duration in seconds |
HSTS_INCLUDE_SUBDOMAINS | true | Adds includeSubDomains — ⚠️ breaks any non-HTTPS subdomain |
HSTS_PRELOAD | false | Adds preload — opt-in: near-irreversible commitment (submission to the browser preload list). Requires includeSubDomains + max-age ≥ 1 year, otherwise a boot warning is logged and it is ignored for preload |
Static files no longer carry the header themselves: HSTS is host-scoped, so once a dynamic page emits it the browser applies it to the whole host (assets included).
Permissions-Policy — default preset (source: PermissionsPolicy::default). All of these features are denied (=()): accelerometer, ambient-light-sensor, bluetooth, camera, gyroscope, hid, magnetometer, microphone, midi, serial, usb, geolocation, idle-detection, display-capture, payment, interest-cohort, local-fonts, sync-xhr, xr-spatial-tracking, window-management. Allowed same-origin (=(self)): publickey-credentials-create, publickey-credentials-get (WebAuthn / passkeys), fullscreen, picture-in-picture. Customizable via .with_permissions_policy(|p| …).
COEP (Cross-Origin-Embedder-Policy: require-corp) — Required to use SharedArrayBuffer and certain high-performance APIs. It may block loading of cross-origin resources (images, scripts, fonts) that do not return the Cross-Origin-Resource-Policy header. If you load resources from third-party CDNs, verify their compatibility or disable COEP via a custom SecurityPolicy.
X-XSS-Protection — Legacy header, ignored by modern browsers (Chrome, Firefox). Kept for compatibility with older browsers.