Production-ready theory + step-by-step code flow (main.rs → middleware → handler → db)
Security headers are HTTP response headers that tell browsers how to behave. They reduce common attacks like XSS, clickjacking, MIME sniffing, and data leakage. In a Rust/Actix backend, the clean way is to apply these headers using middleware, so every response gets the same secure defaults without repeating code in each handler.
In this blog, you will build a custom Security Headers middleware and wire it into main.rs. You’ll also see the request flow: middleware runs, handler executes, DB query runs, then middleware adds headers to the response.
1) What are security headers?
What
Security headers are response headers that enforce browser security rules.
Why
They reduce risk from:
Cross-site scripting (XSS)
Clickjacking (UI redress attacks)
MIME-type sniffing (wrong content treated as executable)
Referrer leakage
Insecure resource loading
When
Use security headers on:
All API responses (safe defaults)
Any server that serves HTML pages or assets
Admin dashboards and user-facing sites
Even if you return JSON only, these headers are still beneficial.
2) Request flow (middleware → handler → DB → middleware)
A secure request looks like this:
Client calls GET /api/shops
Middleware receives request
Middleware forwards request to handler
Handler checks session and calls DB
Handler returns JSON response
Middleware adds security headers to response
Response goes back to client
This ensures every response is protected automatically.
3) Step 1: Decide which headers to set
Below is a good production default:
✅ X-Content-Type-Options: nosniff
Prevents MIME sniffing.
✅ X-Frame-Options: DENY
Blocks clickjacking (no iframe embedding).
✅ Referrer-Policy: no-referrer (or strict-origin-when-cross-origin)
Reduces referrer leakage.
✅ Permissions-Policy: ...
Disables browser features you don’t use (camera, mic, etc).
✅ Strict-Transport-Security (HSTS)
Force HTTPS, but only in production with real TLS.
✅ Content-Security-Policy (CSP)
Powerful header for XSS control (more relevant when serving HTML). For pure JSON APIs you can still set a safe CSP, but it matters most when you return pages.
4) Step 2: Create Security Headers middleware
Create file:
src/middleware/security_headers.rs
use actix_service::{Service, Transform};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
http::header::{HeaderName, HeaderValue},
Error,
};
use futures_util::future::{ready, LocalBoxFuture, Ready};
#[derive(Clone)]
pub struct SecurityHeaders {
// enable HSTS only in production
pub enable_hsts: bool,
// optional CSP if you serve HTML
pub csp: Option<String>,
}
impl SecurityHeaders {
pub fn new(enable_hsts: bool, csp: Option<String>) -> Self {
Self { enable_hsts, csp }
}
}
impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = SecurityHeadersMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(SecurityHeadersMiddleware {
service,
cfg: self.clone(),
}))
}
}
pub struct SecurityHeadersMiddleware<S> {
service: S,
cfg: SecurityHeaders,
}
impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn call(&self, req: ServiceRequest) -> Self::Future {
let cfg = self.cfg.clone();
let fut = self.service.call(req);
Box::pin(async move {
let mut res = fut.await?;
// 1) No MIME sniffing
res.headers_mut().insert(
HeaderName::from_static("x-content-type-options"),
HeaderValue::from_static("nosniff"),
);
// 2) Block iframe embedding (clickjacking)
res.headers_mut().insert(
HeaderName::from_static("x-frame-options"),
HeaderValue::from_static("deny"),
);
// 3) Reduce referrer leakage
res.headers_mut().insert(
HeaderName::from_static("referrer-policy"),
HeaderValue::from_static("strict-origin-when-cross-origin"),
);
// 4) Browser features control
res.headers_mut().insert(
HeaderName::from_static("permissions-policy"),
HeaderValue::from_static("camera=(), microphone=(), geolocation=(), payment=()"),
);
// 5) HSTS only for production HTTPS
if cfg.enable_hsts {
res.headers_mut().insert(
HeaderName::from_static("strict-transport-security"),
HeaderValue::from_static("max-age=31536000; includeSubDomains"),
);
}
// 6) Optional CSP (mainly for HTML pages)
if let Some(csp) = cfg.csp {
if let Ok(v) = HeaderValue::from_str(&csp) {
res.headers_mut().insert(
HeaderName::from_static("content-security-policy"),
v,
);
}
}
Ok(res)
})
}
}
2–3 simple lines about what this does
This middleware runs after your handler returns a response. It then injects security headers into every response automatically. This means you don’t need to add headers in each handler.
5) Step 3: Register middleware module
Create:
src/middleware/mod.rs
pub mod security_headers;
6) S*tep 4: Wire it into main.rs*
In main.rs:
mod middleware;
use middleware::security_headers::SecurityHeaders;
Enable HSTS only in production:
let is_production = std::env::var("APP_ENV").unwrap_or_default() == "production";
// If you serve only JSON, CSP can be optional.
// If you serve HTML templates, set a real CSP.
let csp = Some("default-src 'self'; frame-ancestors 'none'; base-uri 'self'".to_string());
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.wrap(cors)
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
.cookie_secure(is_production)
.cookie_same_site(SameSite::Lax)
.build(),
)
// ✅ Security headers middleware
.wrap(SecurityHeaders::new(is_production, csp.clone()))
.service(
web::scope("/api")
.route("/shops", web::get().to(handlers::list_shops_handler))
.route("/login", web::post().to(handlers::login))
)
.service(Files::new("/uploads", "./uploads"))
})
.bind(cfg.bind_addr)?
.run()
.await
Why .wrap(SecurityHeaders...) is placed here
Because it should apply to all responses under the app. Any handler that returns JSON will automatically include these security headers.
7) Step 5: Handler example that hits DB
handlers/shop.rs
use actix_session::Session;
use actix_web::{error, web, HttpResponse, Result};
use crate::db::{list_shops_by_vendor, AppState};
use super::common::{ensure_login_user_id, ApiResponse};
pub async fn list_shops_handler(
state: web::Data<AppState>,
session: Session,
) -> Result<HttpResponse> {
// Auth check
let user_id = ensure_login_user_id(&session)?;
let vendor_id = i32::try_from(user_id).map_err(|_| error::ErrorBadRequest("User id invalid"))?;
// DB call
let shops = list_shops_by_vendor(&state.pool, vendor_id)
.await
.map_err(error::ErrorInternalServerError)?;
// Response
Ok(HttpResponse::Ok().json(ApiResponse {
message: "Shops fetched".to_string(),
data: shops,
}))
}
DB function (example)
db/shop.rs
use sqlx::MySqlPool;
pub async fn list_shops_by_vendor(pool: &MySqlPool, vendor_id: i32) -> Result<Vec<Shop>, sqlx::Error> {
let rows = sqlx::query_as::<_, Shop>("SELECT * FROM shops WHERE vendor_id = ?")
.bind(vendor_id)
.fetch_all(pool)
.await?;
Ok(rows)
}
8) Confirm headers in browser/network
Call:
curl -i http://127.0.0.1:8080/api/shops
You should see headers like:
X-Content-Type-Options: nosniff
X-Frame-Options: deny
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains (only production)
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'self'
9) Production tips (important)
Use HSTS only when HTTPS is guaranteed
If you enable HSTS on a domain that sometimes serves HTTP, users may get locked out.
Be careful with CSP
If you serve frontend HTML, CSP needs to allow required scripts/styles. For API-only backend, CSP is optional.
Add Cache-Control: no-store for auth endpoints
For login/session sensitive endpoints:
res.headers_mut().insert(
HeaderName::from_static("cache-control"),
HeaderValue::from_static("no-store"),
);
Top comments (0)