HSTS in Rails is one of those security wins that looks trivial right up until you break a domain for months.
If you run a Rails app over HTTPS, you should probably send Strict-Transport-Security. But the real question for a developer team isn’t “should we use HSTS?” It’s “which HSTS policy should we ship, and how safely can we get there?”
Rails makes this pretty easy, but the tradeoffs matter. A weak HSTS policy leaves downgrade risk on the table. An aggressive one can lock users and subdomains into HTTPS before your infrastructure is ready.
What HSTS does in Rails
HSTS tells the browser:
- always use HTTPS for this domain
- don’t let the user click through certificate errors for this site
- optionally apply the rule to subdomains too
The header looks like this:
Strict-Transport-Security: max-age=31536000; includeSubDomains
A browser that has seen that header will rewrite future http:// requests to https:// before making the request.
That matters because redirects alone are not enough. If a user types http://example.com, the first request is still plaintext unless the browser already knows the site is HTTPS-only. HSTS closes that gap after first contact.
Rails support: the easy path
Rails has built-in support through config.ssl_options and the force SSL stack.
A common setup looks like this:
# config/environments/production.rb
Rails.application.configure do
config.force_ssl = true
config.ssl_options = {
hsts: {
expires: 1.year,
subdomains: true,
preload: false
}
}
end
This does a few things:
- redirects HTTP to HTTPS
- marks secure cookies properly
- sends the HSTS header
Official docs live here:
If you want to inspect what your app is actually returning, run a header scan with HeaderTest.
The real comparison: HSTS policy choices
The biggest decision isn’t whether Rails can send HSTS. It’s which policy you choose.
Option 1: Short max-age for initial rollout
Example:
config.ssl_options = {
hsts: {
expires: 1.day,
subdomains: false,
preload: false
}
}
This is the cautious rollout.
Pros
- Safer for first deployment
- Limits damage if you misconfigured HTTPS
- Good for testing certificate renewal, redirects, and edge infrastructure
- Easier to back out from than a one-year policy
Cons
- Weak long-term protection
- Doesn’t protect subdomains
- Users are only pinned to HTTPS briefly
- Easy for teams to forget to increase it later
My opinion: this is the right starting point for teams that have legacy DNS, mystery subdomains, or multiple environments behind a shared parent domain. I’ve seen too many companies turn on long-lived HSTS and then discover an old support subdomain still serves broken TLS.
Option 2: Standard production HSTS
Example:
config.ssl_options = {
hsts: {
expires: 1.year,
subdomains: false,
preload: false
}
}
This is a practical default for a lot of Rails apps.
Pros
- Strong protection for the main app domain
- Long enough to be meaningful
- Lower risk than enabling subdomains immediately
- Easy to reason about if your app lives on a single host like
app.example.com
Cons
- Subdomains remain unprotected
- Can create inconsistent security across the organization
- Still painful to undo for users who already cached it
This is a good middle ground when your app is isolated and you don’t control every subdomain under the parent zone.
Option 3: HSTS with includeSubDomains
Example:
config.ssl_options = {
hsts: {
expires: 1.year,
subdomains: true,
preload: false
}
}
Now you’re making a promise for the entire domain tree.
Pros
- Stronger protection
- Prevents insecure access to all subdomains
- Reduces the chance of forgotten HTTP services hanging around
- Better fit for mature infrastructure
Cons
- Breaks any subdomain that doesn’t fully support HTTPS
- Can affect non-Rails systems under the same parent domain
- Forces coordination with ops, platform, marketing, support, and whoever owns random DNS records from 2017
This is where teams get burned. Rails may only serve one application, but HSTS with subdomains is a domain-level decision, not an app-level one.
If example.com sends includeSubDomains, that applies to api.example.com, help.example.com, old-admin.example.com, and every forgotten staging hostname someone left in DNS.
Option 4: HSTS preload-ready policy
Example:
config.ssl_options = {
hsts: {
expires: 2.years,
subdomains: true,
preload: true
}
}
That typically produces:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Pros
- Strongest practical browser enforcement
- Protects even on first visit in browsers using the preload list
- Great for high-value domains with mature HTTPS everywhere
Cons
- Hardest to reverse
- Requires full confidence in HTTPS across the domain
- Mistakes are slow and painful to unwind
- Not something I’d enable casually on a Friday afternoon
Preload is where HSTS stops being “just a header” and becomes a long-term operational commitment. If your team still has uncertainty around certificates, CDN behavior, or subdomain ownership, don’t do this yet.
Rails-specific pros of using built-in HSTS
Using Rails’ native config is usually better than hand-rolling headers in middleware.
Pros
- Clear, readable config
- Works nicely with
force_ssl - Less custom code to maintain
- Easier for new team members to understand
Example with force SSL disabled but HSTS manually controlled is possible, but I rarely like it:
config.force_ssl = false
config.action_dispatch.default_headers.merge!(
'Strict-Transport-Security' => 'max-age=31536000'
)
This works, but it’s easier to create inconsistent behavior. If you care enough to send HSTS, you almost certainly want enforced HTTPS redirects too.
Cons
- App-level config may not match edge behavior if a proxy or CDN rewrites headers
- Can be confusing in setups where TLS terminates before Rails
- Teams sometimes assume Rails config is active when the load balancer strips or replaces the header
Always verify the final response seen by clients, not just your Rails config file.
When not to enable aggressive HSTS yet
I’d hold back on includeSubDomains or preload if any of these are true:
- you still serve anything over plain HTTP
- you have unknown or unmanaged subdomains
- staging or internal tools share the same parent domain
- certificate renewal is flaky
- you recently migrated proxies, CDNs, or ingress controllers
- your app sometimes responds over HTTPS with certificate mismatches
HSTS amplifies confidence and also amplifies mistakes.
A safe rollout plan for Rails teams
This is the rollout pattern I trust:
Phase 1: enforce HTTPS
config.force_ssl = true
Make sure redirects, cookies, and certificates are clean.
Phase 2: short HSTS
config.ssl_options = {
hsts: {
expires: 1.day,
subdomains: false,
preload: false
}
}
Watch for errors. Check every important path.
Phase 3: long max-age
config.ssl_options = {
hsts: {
expires: 1.year,
subdomains: false,
preload: false
}
}
Now the main host gets meaningful protection.
Phase 4: add subdomains if the whole zone is ready
config.ssl_options = {
hsts: {
expires: 1.year,
subdomains: true,
preload: false
}
}
Only do this after auditing DNS and TLS everywhere.
Phase 5: consider preload
config.ssl_options = {
hsts: {
expires: 2.years,
subdomains: true,
preload: true
}
}
This is for teams that are very sure, not just pretty sure.
Pros and cons at a glance
Basic HSTS in Rails
Pros
- Easy to enable
- Stronger HTTPS posture
- Built into framework config
Cons
- Easy to underestimate rollout risk
- Browser caching makes mistakes linger
includeSubDomains
Pros
- Better coverage
- Reduces weak spots across subdomains
Cons
- Domain-wide blast radius
- Requires cross-team coordination
Preload
Pros
- Best protection against first-visit downgrade
- Great for mature production domains
Cons
- Hard to reverse
- Operationally unforgiving
My recommendation
For most Rails apps, I’d start with:
config.force_ssl = true
config.ssl_options = {
hsts: {
expires: 1.day,
subdomains: false,
preload: false
}
}
Then quickly move to:
config.ssl_options = {
hsts: {
expires: 1.year,
subdomains: false,
preload: false
}
}
I would only enable includeSubDomains after a deliberate audit. And I would only use preload when the domain is boring in the best possible way: no surprises, no legacy hosts, no mystery subdomains, no flaky certs.
That’s really the whole HSTS story in Rails. The code is easy. The domain ownership and operational discipline are the hard parts.
If you want to sanity-check your live headers before or after rollout, scan the site with HeaderTest.