Security Headers Set From a Rust Web Service Step by Step
Set the HTTP response headers that harden a web service: Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security, Referrer-Policy and more, applied with tower-http in Axum.

Security headers are the cheapest hardening available, and they are one line each. The difficulty is knowing which ones apply and not breaking the page with a policy that is too strict. This post lists the headers that earn their place, shows them added in Axum with tower-http, and notes the ones to skip. It belongs to the deployment controls in security for Rust developers.
Axum 0.8, tower-http 0.6, Rust 1.90.
A layer that sets them
tower-http = { version = "0.6", features = ["set-header"] }use axum::{http::{header, HeaderValue}, routing::get, Router};
use tower_http::set_header::SetResponseHeaderLayer;
fn app() -> Router {
Router::new()
.route("/", get(|| async { "ok" }))
.layer(SetResponseHeaderLayer::overriding(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
))
.layer(SetResponseHeaderLayer::overriding(
header::REFERRER_POLICY,
HeaderValue::from_static("strict-origin-when-cross-origin"),
))
.layer(SetResponseHeaderLayer::overriding(
header::STRICT_TRANSPORT_SECURITY,
HeaderValue::from_static("max-age=31536000; includeSubDomains"),
))
}Every response now carries the three headers. overriding replaces any value set by a handler, and if_not_present leaves handler values alone, which suits headers a handler may tune. Check with curl -I localhost:3000.
The headers
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | browser trusts your Content-Type, preventing script execution of uploads |
Strict-Transport-Security | max-age=31536000; includeSubDomains | HTTPS only for a year; send only over HTTPS |
Referrer-Policy | strict-origin-when-cross-origin | limits URL leakage to other sites |
Content-Security-Policy | see below | restricts script, style and frame sources |
X-Frame-Options or CSP frame-ancestors | DENY / frame-ancestors 'none' | blocks clickjacking by framing |
Permissions-Policy | camera=(), microphone=() | turns off browser features you do not use |
Cross-Origin-Opener-Policy | same-origin | isolates the browsing context |
Skip X-XSS-Protection: modern browsers removed the filter, and the header can introduce issues in old ones; set it to 0 or leave it out.
Content-Security-Policy
For an API that never serves HTML:
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'For a site serving pages, start in report-only mode and tighten:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'Watch the browser console or a report endpoint, fix what would break, then switch to the enforcing header. Inline scripts need a per-response nonce, not 'unsafe-inline'. A policy that is permissive everywhere protects nothing.
HSTS carefully
HSTS makes browsers refuse plain HTTP for the whole max-age. Roll out with a short value (max-age=300), confirm every subdomain supports HTTPS, then extend. Adding preload submits you to a list that is hard to leave. Do not send it over plain HTTP; browsers ignore it there.
Reverse proxies
If a proxy terminates TLS, HSTS should be added where you know the connection is HTTPS, typically at the proxy. Do not duplicate headers in two places; the browser may see two conflicting values.
Limits
Headers are defense in depth. They do not fix XSS in your templates, CSRF in your forms or broken authorization. Cookies need their own flags: Secure, HttpOnly, SameSite. Combined with CORS they define what a browser lets pages do. MDN's HTTP security headers documentation is the readable primary reference.


