github/secure_headers: Ruby security headers with safe defaults and per-request overrides
Manages application of security headers with many safe defaults
At a glance
- What is it?
- A Rack middleware and configuration library that applies CSP, HSTS, X-Frame-Options and cookie flags to Rails and Sinatra apps. The defaults are locked down; the work is in loosening them without breaking your own pages.
- Who is it for?
- Adopt secure_headers if you run a Ruby web app on Rack and want security headers applied by configuration rather than by hand in every response path. Do not adopt it if your stack is not Ruby or Rack, since the middleware and the configuration DSL are the whole product.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem secure_headers solves for Ruby apps
Security headers are easy to describe and tedious to keep correct. A Content Security Policy that works on the login page may break the dashboard; HSTS set with a short max-age does little; cookies set without SameSite leak across sites. Doing this by hand means touching every response path, and the failure mode is silent: nothing errors, the header is just missing or wrong.
secure_headers targets that gap. It is a Ruby library with a global config, per request overrides, and Rack middleware, and the README states it will automatically apply several headers related to security. The list is broad: Content Security Policy, HTTP Strict Transport Security, X-Frame-Options, X-XSS-Protection, X-Content-Type-Options, x-download-options, x-permitted-cross-domain-policies, referrer-policy, expect-ct and clear-site-data. It can also mark all HTTP cookies with the Secure, HttpOnly and SameSite attributes, which the README says is on by default and can be turned off with config.cookies = SecureHeaders::OPT_OUT.
The audience is narrow and obvious: teams running Rails, Sinatra or another Rack-based Ruby service who want headers applied centrally instead of per controller. If your application is not Rack, the middleware is not usable and the gem has little to offer.
How the configuration DSL turns Ruby hashes into headers
The mechanism is a two-layer configuration. A default block defines the baseline for the whole application, and per-request overrides adjust it for specific routes. The README points to a docs/named_overrides_and_appends.md file and a docs/per_action_configuration.md file for those two mechanisms, so the override path is a designed feature rather than an afterthought.
Inside the CSP block, values split into two kinds. The README calls the first kind meta values: they shape the header but are not included in it. preserve_schemes is one, described as removing schemes from host sources to save bytes and discourage mixed content. disable_nonce_backwards_compatibility is another, and the README explains that when it is false, unsafe-inline is added automatically when using nonces. The second kind are directive values, which translate directly into source directives: default_src, base_uri, child_src, connect_src, font_src, form_action, frame_ancestors, img_src, manifest_src, media_src, object_src, plugin_types, script_src, script_src_elem, script_src_attr, style_src, style_src_elem, style_src_attr, worker_src, upgrade_insecure_requests and report_uri.
The sandbox key is worth noting because it does not take a list. The README says true and [] will set a maximally restrictive setting. That is a deliberate choice: an empty array is not "no sandbox", it is the strictest sandbox, which is the opposite of how most configuration formats behave and a good place to lose an afternoon.
Two behaviours govern everything else. All nil values fall back to their default values, and SecureHeaders::OPT_OUT disables a header entirely. If no default configuration is supplied, the README states exceptions will be raised.
Installing the gem and getting a first header out
The README does not give installation steps beyond the gem itself. The repository ships a secure_headers.gemspec, a Gemfile and a Rakefile, so the gem is the distribution channel. Once it is in your bundle, the initializer is where the work happens. The README is explicit that omitting a default configuration raises exceptions, and that calling SecureHeaders::Configuration.default without any arguments or block gives you a default configuration which it describes as fairly locked down. Starting there and reading the emitted header is the fastest way to see what you are signing up for:
SecureHeaders::Configuration.defaultFor a real application you will replace that with a block. The README's own sample is not a default configuration, and it says so: it is a sample implementation, and you should read about the headers and decide what fits your requirements. The sample opens with the cookie attributes, the HSTS value and the simpler header toggles:
SecureHeaders::Configuration.default do |config|
config.cookies = {
secure: true, # mark all cookies as "Secure"
httponly: true, # mark all cookies as "HttpOnly"
samesite: {
lax: true # mark all cookies as SameSite=lax
}
}
# Add "; preload" and submit the site to hstspreload.org for best protection.
config.hsts = "max-age=#{1.week.to_i}"
config.x_frame_options = "DENY"
config.x_content_type_options = "nosniff"
config.x_xss_protection = "1; mode=block"
config.x_download_options = "noopen"
config.x_permitted_cross_domain_policies = "none"
config.referrer_policy = %w(origin-when-cross-origin strict-origin-when-cross-origin)
endThe sample continues with the csp hash, which is where the directives live. It sets default_src to 'none' and then names the sources each directive is allowed to use:
config.csp = {
# "meta" values. these will shape the header, but the values are not included in the header.
preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content.
disable_nonce_backwards_compatibility: true, # default: false. If false, `unsafe-inline` will be added automatically when using nonces. If true, it won't. See #403 for why you'd want this.
# directive values: these values will directly translate into source directives
default_src: %w('none'),
base_uri: %w('self'),
child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set.
connect_src: %w(wss:),
font_src: %w('self' data:),
form_action: %w('self' github.com),
frame_ancestors: %w('none'),
img_src: %w(mycdn.com data:),
manifest_src: %w('self'),
media_src: %w(utoob.com),
object_src: %w('self'),
sandbox: true, # true and [] will set a maximally restrictive setting
plugin_types: %w(application/x-shockwave-flash),
script_src: %w('self'),
script_src_elem: %w('self'),
script_src_attr: %w('self'),
style_src: %w('unsafe-inline'),
style_src_elem: %w('unsafe-inline'),
style_src_attr: %w('unsafe-inline'),
worker_src: %w('self'),
upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/
report_uri: %w(https://report-uri.io/example-csp)
}What you should see next is the header on responses. Load a page and inspect the response headers in your browser's network panel; the CSP, HSTS and X-Frame-Options values should match what the block set. If the strict default_src of 'none' from the locked-down default is still in place, scripts on your own pages will be blocked, which is the signal that you need to name your sources.
Reporting: report-uri, report-to and the Reporting-Endpoints header
CSP without reporting is guesswork. You set a policy, something breaks in production, and you find out from a user. secure_headers supports both reporting generations, and the README is unusually direct about which to use: it recommends using both report-uri and report-to.
The legacy directive is report-uri, which the README describes as widely supported but limited to POST requests with JSON payloads. It sits inside the csp hash as a list of URLs:
config.csp = {
default_src: %w('self'),
report_uri: %w(https://example.com/csp-report)
}The modern directive is report_to, which names an endpoint rather than giving a URL, and the endpoint itself is declared separately in config.reporting_endpoints, which produces the Reporting-Endpoints header. The README's example pairs a csp-endpoint name with a URL and then sets report_to to that same name:
config.csp = config.csp.merge({
report_to: "csp-endpoint"
})
config.reporting_endpoints = {
"csp-endpoint": "https://report-uri.io/example-csp",
"csp-report-only": "https://report-uri.io/example-csp-report-only"
}The indirection is the point and also the trap. A report_to value that does not match a key in reporting_endpoints produces a header that browsers cannot resolve, and nothing in the configuration will tell you. The report-only path exists too: the README notes config.csp_report_only is available from 3.5.0 and that earlier versions should use a report_only: true setting instead.
Where the locked-down default will bite you
The default configuration is described in the README as fairly locked down, and the sample CSP uses default_src: %w('none'). That is the correct starting posture and also the most common reason a first deployment breaks. Every page that loads its own JavaScript, stylesheet or font needs an explicit source, and the failure is a browser console error rather than a server exception, so it will not appear in your application logs.
The cookie defaults have a similar shape. Marking all cookies Secure, HttpOnly and SameSite=lax is the right default for a modern app, but the README offers only a single opt-out, config.cookies = SecureHeaders::OPT_OUT, which turns the behaviour off globally. There is no documented per-cookie exemption in the README itself. If your application sets a cookie that a third-party site must read, or a cookie that must travel over plain HTTP during a migration, the documented path is to disable cookie handling for the whole application and manage those attributes yourself.
There is also a version boundary to respect. The README states that the main branch represents the 7.x line, that bug fixes should go in the 6.x branch for now, and it links separate upgrade documents for 4.x, 5.x, 6.x and 7.x. That is a project carrying several supported lines at once, which matters when you are deciding whether to upgrade or to stay put.
secure_headers against a reverse proxy or a hand-rolled Rack middleware
The realistic alternative is not another Ruby gem. It is setting headers outside the application, in nginx, in a CDN configuration, or in a small piece of Rack middleware you write yourself.
The difference in approach is where the policy lives. A proxy or CDN sets one header for every response that passes through it, which is simple and covers static assets and error pages that never reach your application. secure_headers does the opposite: it works inside the request cycle, which is what makes per-action configuration and named overrides possible. A route that needs a different CSP can have one, because the policy is computed per request rather than at the edge.
The trade-off runs both ways. Proxy-level headers cannot vary by route without duplicating routing logic outside the application, and they cannot see the application's own knowledge of which assets a page needs. secure_headers cannot protect responses that never reach the Rack stack, and it does nothing for a static file served directly by the web server. Many production setups end up with both, and the README does not discuss how the two interact, so ordering and overwriting are things you will have to verify yourself.
Maintenance, upgrades and the MIT licence
The repository is not archived, and the last push was on 2026-09-23. Recent releases are v7.3.0 on 2026-06-03, v7.2.0 on 2026-02-20 and v7.1.0 on 2024-12-16. The gap between v7.1.0 and v7.2.0 is roughly fourteen months, so the release cadence is uneven, and the README's own note that bug fixes should go to the 6.x branch while main tracks 7.x confirms that maintenance is spread across branches rather than concentrated on one line.
The upgrade cost is documented rather than hidden. The README links dedicated upgrade documents for 4.x, 5.x, 6.x and 7.x, which means each major version has had breaking changes worth writing about. Budget for reading the relevant document before moving a major version, and check the CHANGELOG.md at the repository root for the detail the upgrade docs summarise.
The licence is MIT, which is permissive and places few obligations on how you redistribute or modify the gem. This is a description of the licence identifier, not legal advice; if your organisation has licence review requirements, the LICENSE file at the repository root is the authoritative text.
Editorial conclusion
Adopt secure_headers if you run a Ruby web app on Rack and want security headers applied by configuration rather than by hand in every response path. Do not adopt it if your stack is not Ruby or Rack, since the middleware and the configuration DSL are the whole product. Before rolling it out, call SecureHeaders::Configuration.default with no block and inspect the CSP header your app emits, because a strict default_src of 'none' will break pages that load their own scripts until you name the sources you actually need.
Frequently asked questions
What is secure_headers?
It is a Ruby library that applies security-related HTTP headers with safe defaults, and it ships a global config, per-request overrides and Rack middleware. The headers it manages include Content Security Policy, HSTS, X-Frame-Options, X-Content-Type-Options and referrer-policy, and it can also mark cookies with the Secure, HttpOnly and SameSite attributes.
How do I install the secure_headers gem?
The README gives no installation steps beyond the gem itself, which is distributed through the repository's secure_headers.gemspec. Once it is in your bundle, you create an initializer, and without a default configuration the README states that exceptions will be raised.
Does secure_headers turn on cookie flags by default?
Yes. The README says marking all HTTP cookies with Secure, HttpOnly and SameSite is on by default, and it can be turned off with config.cookies = SecureHeaders::OPT_OUT.
What is the difference between report-uri and report-to in secure_headers?
report-uri sends CSP violations to a URL endpoint and is described as widely supported but limited to POST requests with JSON payloads. report-to names an endpoint that is defined separately in config.reporting_endpoints, which produces the Reporting-Endpoints header. The README recommends using both.
What happens if a header value in secure_headers is nil?
The README states that all nil values fall back to their default values. To remove a header entirely rather than fall back, set it to SecureHeaders::OPT_OUT.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/github-secure-headers)