roadrunner_security_headers (roadrunner v0.12.0)
View SourceSecurity-headers middleware: a default-safe set of response headers, the same handful every browser-facing service hand-rolls, added to every HTTP response and leaving any header the handler already set untouched.
Wire it into a listener's (or route's) middlewares opt, bare for the
defaults or {roadrunner_security_headers, Config} to tune them:
roadrunner_listener:start_link(my_app, #{
port => 8080,
routes => Routes,
middlewares => [roadrunner_security_headers]
}).Default set
| Header | Default value | Config key |
|---|---|---|
x-content-type-options | nosniff | content_type_options |
x-frame-options | SAMEORIGIN | frame_options |
referrer-policy | strict-origin-when-cross-origin | referrer_policy |
Each key takes a binary to override the value, or false to drop the header.
Opt-in headers
Two headers carry consequences beyond the current response, so they stay off until enabled explicitly:
content-security-policy(content_security_policy => <<"...">>): a default policy is too application-specific to be safe.strict-transport-security(hsts => true | Config): once a browser sees HSTS it refuses plain HTTP to the host for the wholemax-age, even after the header stops, andinclude_subdomainsextends that to every subdomain, so a wrong value is a durable outage rather than a one-response mistake. It is emitted only over HTTPS (perroadrunner_req:scheme/1); a browser ignores HSTS received over plain HTTP (RFC 6797 §8.1).
hsts => true emits the recommended max-age=31536000; includeSubDomains.
hsts => Config tunes it:
max_age => non_neg_integer()(default 31536000, one year)include_subdomains => boolean()(defaulttrue)preload => boolean()(defaultfalse): opt-in, it commits the host to the browser preload lists, which is slow to reverse
Precedence and scope
A header the handler already set wins: each default is added only when
absent. Unlike roadrunner_compress, this middleware adds no Vary (its
output conditions on no request header; HSTS keys off the connection scheme,
not a header). It decorates every response shape that carries headers
(buffered, stream, sendfile, loop, and send, whose final response gets
them), so a file served via sendfile gets nosniff too. A {websocket, ...} upgrade passes through: its 101 carries
only the handshake headers plus any the handler set, and these defaults do not
belong on a protocol switch.
Summary
Types
-type hsts_config() :: #{max_age => non_neg_integer(), include_subdomains => boolean(), preload => boolean()}.
-type state() :: #sec{static :: roadrunner_http:headers(), hsts :: binary() | false}.
The compiled header set produced by init/1 and consumed by call/3.
Functions
-spec call(roadrunner_req:request(), roadrunner_middleware:next(), state()) -> roadrunner_handler:result().