Skip to content

Per-Site security response headers (HSTS, and friends) #8

Description

@hugithordarson

Context

Modulo currently sets no security-related response headers on outgoing responses. Migrating from Apache exposes a real gap: existing Apache vhost configs typically include at minimum an HSTS header (Strict-Transport-Security), and modulo doesn't reproduce it. Sites that had HSTS yesterday don't have it today.

This issue is filed early — well before implementation — so that the shape of the per-Site config for security headers is decided before iteration 3 lands a config schema. Once the config format is in use by operators, changing it is expensive.

What we want to expose

HSTS is the headline. A few others are commonly bundled with it at the proxy layer. Most other security headers (CSP, Permissions-Policy, etc.) are app concerns and should NOT live in modulo.

HSTS — first-class

site:
  hsts:
    enabled: true              # default true
    max_age: 31536000          # 1 year, default
    include_subdomains: false  # default; operator opts in
    preload: false             # default; operator opts in

Notes:

  • Default max_age of 31536000 (1 year) matches current Mozilla/Google guidance.
  • includeSubDomains is opt-in because applying it to a site that doesn't control all subdomains is a foot-gun.
  • preload is opt-in because once you submit to the HSTS preload list, removal is hard.
  • Disabling HSTS per-Site needs to be possible (e.g. for a site in mid-migration that isn't yet always-HTTPS).

Other security headers — opt-in, simple values

These are useful enough at the proxy that operators reasonably want to set them centrally rather than rely on every app to do so. They should be optional and simple.

site:
  security_headers:
    x_content_type_options: nosniff   # opt-in
    x_frame_options: SAMEORIGIN       # opt-in; or DENY
    referrer_policy: strict-origin-when-cross-origin   # opt-in

No support for Content-Security-Policy at the proxy level — too tied to what the app actually serves, and bad defaults will silently break things. CSP belongs in the app.

Permissions-Policy likewise — app concern.

Explicit non-goals

  • Cipher suite configuration, SSLProtocol enable/disable, SSLHonorCipherOrder — modern JDK + Jetty defaults are already correct (TLS 1.2 + 1.3, AEAD-preferred ciphers, server cipher order). Exposing these as config invites operators to write outdated lists, which is worse than library defaults. The library is the right place to trust here. (This conclusion may need revisiting if FIPS-mode or formal compliance audits ever become a use case.)
  • OCSP stapling configuration. It's an implementation detail — either modulo does it when the cert's CA supports it, or it doesn't. Not a per-Site config knob. (Also: Let's Encrypt has sunsetted OCSP as of 2025, so for our deployments it's effectively moot.)

Shape implications for iteration 3

The Site config schema needs to accommodate a hsts sub-object (or equivalent — exact format depends on whether we end up with HOCON, YAML, TOML, etc.) with the fields above, and a security_headers sub-object. Implementation can come later; the shape needs to be settled when the schema is.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions