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.