twobotechnologies

Information security · 3 min read

Adding HSTS to PingFederate and Other Jetty-Based Servers

Short answer

Current PingFederate versions add HSTS and other headers through response-header-runtime-config.xml and response-header-admin-config.xml in the config store; on plain Jetty you can add a HeaderPatternRule with the rewrite module; and in front of any server, a reverse proxy such as nginx or Apache can set the header.

Key takeaways

  • HSTS (RFC 6797) tells browsers to use only HTTPS for a host for max-age seconds.
  • Start with a short max-age, then raise it to a year or more once everything works over HTTPS.
  • Only add preload when every subdomain serves HTTPS; removal from browser lists is slow.

A federation server's sign-in page is a prime target for SSL stripping: an attacker on the network intercepts the first plain-HTTP request and keeps the victim on HTTP while relaying to the real HTTPS site. HTTP Strict Transport Security stops that after the first visit. Older PingFederate versions had no setting for it, so administrators added it with Jetty rewrite rules. Today there are cleaner options.

What does HSTS do?

HSTS instructs the browser to connect to the host only over HTTPS, and to refuse certificate errors, for the number of seconds in max-age.

The header looks like this:

Strict-Transport-Security: max-age=31536000; includeSubDomains

Defined in RFC 6797 (2012), it has three parts: max-age in seconds, the optional includeSubDomains, and the non-standard but widely used preload token that signals consent to inclusion in browser preload lists. Browsers ignore the header when it arrives over plain HTTP.

How do I add HSTS in current PingFederate versions?

Edit response-header-runtime-config.xml for the runtime and response-header-admin-config.xml for the admin console, then restart.

Both files live in <pf_install>/pingfederate/server/default/data/config-store. In a cluster, make the change on the console node and replicate the configuration. Follow the file format in your version's documentation and keep a copy of your change for upgrades.

How do I do it on plain Jetty?

Use the rewrite module and a HeaderPatternRule matching all paths.

<Ref refid="Rewrite">
  <Call name="addRule">
    <Arg>
      <New class="org.eclipse.jetty.rewrite.handler.HeaderPatternRule">
        <Set name="pattern">/*</Set>
        <Set name="name">Strict-Transport-Security</Set>
        <Set name="value">max-age=31536000; includeSubDomains</Set>
      </New>
    </Arg>
  </Call>
</Ref>

This was the approach in older PingFederate releases too, since the product runs on embedded Jetty. Class names and module setup vary between Jetty major versions, so check the documentation for yours. The Security Header Builder generates this snippet with your chosen values.

How do I set it on a reverse proxy?

One line per server, on the HTTPS virtual host.

Server Directive
nginx add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache httpd Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" (mod_headers)
IIS 10 (1709+) Native HSTS settings per site, or a custom header in web.config
lighttpd setenv.add-response-header = ("Strict-Transport-Security" => "max-age=31536000; includeSubDomains") (mod_setenv)

In nginx, remember that add_header inside a location block replaces headers from the server level; repeat them where needed. Also redirect port 80 to HTTPS with a 301.

How do I check that the header is actually sent?

Request the sign-in page over HTTPS and inspect the response headers.

From a terminal, curl -sI https://sso.example.com/ shows the headers without a browser. In the browser, the Network panel shows them per request. Check several paths: the sign-in page, an error page and a static asset, because some servers only add headers to certain responses. Check through the public hostname as well as directly against the server, since a load balancer can strip or override headers. A missing header on error responses is common with nginx when always is left out.

How should I roll it out?

In steps: short max-age, then longer, then includeSubDomains, then preload if appropriate.

  1. Confirm every page and asset loads over HTTPS.
  2. Start with max-age=300 and watch for problems.
  3. Raise to a day, a week, a month, then a year (31536000) or two (63072000).
  4. Add includeSubDomains once every subdomain, including old internal ones, has valid HTTPS.
  5. Consider preload and submit at hstspreload.org. It requires at least one year of max-age plus includeSubDomains, and removal can take months.

Federation hostnames often sit under a company domain shared with many other services, so check includeSubDomains carefully with the owners of those services first. For other headers that belong on a sign-in page, see locking down a federation server.

Questions

Should the proxy or the federation server send HSTS?

Whichever terminates TLS for the browser. If a load balancer or proxy terminates TLS, set it there. Setting it in both places is harmless if the values match.

Does HSTS protect the very first visit?

No. A browser learns the policy from the first HTTPS response. Preloading the domain into browser lists closes that gap, but requires max-age of at least one year, includeSubDomains and the preload directive.

Sources

Tagged: PingFederate, SSL, TLS, Apache HTTP Server, nginx, IIS, lighttpd, Jetty, HSTS

The Twobo Technologies editors write practical explainers on identity and API security and maintain the free tools on this site. Spotted an error or an outdated detail? Email contact@twobotechnologies.com.

Related articles