roadrunner_security_headers (roadrunner v0.10.5)

View Source

Security-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

HeaderDefault valueConfig key
x-content-type-optionsnosniffcontent_type_options
x-frame-optionsSAMEORIGINframe_options
referrer-policystrict-origin-when-cross-originreferrer_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 whole max-age, even after the header stops, and include_subdomains extends that to every subdomain, so a wrong value is a durable outage rather than a one-response mistake. It is emitted only over HTTPS (per roadrunner_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() (default true)
  • preload => boolean() (default false): 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, and loop), 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

The compiled header set produced by init/1 and consumed by call/3.

Types

config()

-type config() ::
          #{content_type_options => binary() | false,
            frame_options => binary() | false,
            referrer_policy => binary() | false,
            hsts => boolean() | hsts_config(),
            content_security_policy => binary() | false}.

hsts_config()

-type hsts_config() ::
          #{max_age => non_neg_integer(), include_subdomains => boolean(), preload => boolean()}.

state()

-type state() :: #sec{static :: roadrunner_http:headers(), hsts :: binary() | false}.

The compiled header set produced by init/1 and consumed by call/3.

Functions

call/3

init(Config)

-spec init(config()) -> state().