Ramshankar
on systems, software & life

Configuring HTTP Security Headers

While there’s no such thing as perfect security, we should strive to secure our websites as best we can. Even a partially secured website is better than one with no security at all. Even if your site has no sensitive information today, implementing these security measures prevent accidents in the future. If you’re still not convinced, please read my earlier article on why your site needs to be HTTPS.

Unfortunately, merely serving content over HTTPS isn’t sufficient and we need to go the extra distance if we care about security. Now, assuming you’ve already obtained an SSL certificate and enabled HTTPS for your site, it’s highly recommended that you configure certain HTTP security headers as well.⁠[1]

What are HTTP security headers

Security headers are directives sent by web servers to clients (typically web browsers) to mitigate vulnerabilities that compromise privacy or security. Think of them as additional security hints or instructions as part of a server’s response to a client.

While some headers may not be applicable to every site (due to the nature of content being published), it’s a good practice for sites to implement at least the most important headers.

You can inspect HTTP response headers of sites in your browser using its inspection tools as illustrated in the following screenshot.

Inspecting HTTP response headers using Firefox
Inspecting the HTTP response header of Wikipedia using Firefox

Alternatively, from the command-line you can use the curl tool to fetch and dump HTTP response headers. curl is open-source software that’s available on all major operating systems. To my surprise, curl now even ships as part of Windows 10.⁠[2]

Using curl to inspect HTTP response headers (listing truncated for tidiness)
$ curl -IL https://wikipedia.com

HTTP/2 200
cache-control: s-maxage=86400, must-revalidate, max-age=3600
server: ATS/8.0.8
etag: W/"11a72-5b0ea6bab90ea"
content-type: text/html
vary: Accept-Encoding
x-cache: cp3062 miss, cp3054 hit/967979
x-cache-status: hit-front
server-timing: cache;desc="hit-front"
strict-transport-security: max-age=106384710; includeSubDomains; preload
...

As shown above, servers emit several HTTP headers (like cache-control, server etc.) while communicating with a client. While the full list of HTTP headers is rather extensive, we are only interested in the handful of important security headers to configure.

How to configure HTTP security headers

As of October 2020, the following are the most critical security headers. These are also the most commonly verified headers among security-scoring sites.

Ideally, servers must emit these security headers for all HTTP response codes and not just for standard responses like 200 (success), 301 (moved permanently) etc. For this reason, you should include the always keyword (for both Apache and Nginx) while configuring security headers for your site.

In Nginx, using add_header in a location block does not inherit any “parent” add_header directives! I urge you to place these security headers in a separate .conf file and include it in each relevant location block.

Let’s look at each of the above security headers in detail with examples on how to configure them.

Strict-Transport-Security

HTTP Strict-Transport-Security (HSTS) is a header that instructs browsers to connect to the site only using HTTPS, even if HTTP was chosen. Browsers will transparently upgrade all requests to HTTPS for sites that provide HSTS. Furthermore, it instructs browsers to disable user’s ability to by-pass certificate related errors while accessing your site.

Nginx e.g.
add_header
Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
Apache e.g.
header always set
Strict-Transport-Security "max-age=63072000; includeSubDomains"
max-age (mandatory)

This specifies the time (in seconds) browsers should remember that the site is only to be accessed using HTTPS. A value of 63072000 seconds (2 years) is recommended.

includeSubDomains (optional)

This specifies that HSTS applies to all subdomains. It’s recommended to enable this unless you have subdomains that cannot be served over HTTPS. In which case, you should consider migrating all your subdomains to support HTTPS as soon as possible.

preload (optional, non-standard)

HSTS preloading is an initiative by Google to hardcode a list of HSTS-enabled sites directly into browsers. Specifying this parameter gives your consent to include your domain to this list. Users are not protected by HSTS until after their first successful secure connection to your site.⁠[3] HSTS preloading helps by ensuring even the very first connection to your domain uses HTTPS. While this is not part of the official specification, it’s well supported on modern browsers.⁠[4]

X-Frame-Options

X-Frame-Options (XFO) is a header that instructs the browser whether or not it can render a page in a frame. A frame includes <frame>, <iframe>, <embed> or <object> elements. This header is primarily intended to protect your site from click-jacking attacks.

Browsers that support Content-Security-Policy (CSP) header’s frame-ancestors directive would ignore the XFO header. However, I recommend keeping the XFO header to protect users on older browser or browsers that don’t support frame-ancestors.

Nginx e.g.
add_header X-Frame-Options deny always;
Apache e.g.
Header always set X-Frame-Options deny
deny or sameorigin

deny disables rendering of pages inside a frame, even when the embedded page is from your own domain. Use this if you don’t use frames on the page. sameorigin allows pages to be rendered in a frame on the same origin as the page itself. Use this on pages where your site uses a frame. Specify either deny or sameorigin but never both.

X-Content-Type-Options

This header instructs browsers that MIME types specified in Content-Type headers by your server is accurate and should not be changed. Sometimes, browsers might read parts of a file served by your server — called MIME-type sniffing — to determine what type of file it is. This header mitigates vulnerabilities in browsers while reading the file by opting-out of MIME-type sniffing.

This header also enables Cross-Origin Read Blocking (CORB) protection for certain MIME-types.⁠[5]

All modern web servers are pre-configured to correctly advertise MIME types for most common file formats. Nginx has a configurable MIME-type list in /etc/nginx/mime.types, which you should ensure contains valid MIME types for all files served by your site.

Nginx e.g.
add_header X-Content-Type-Options nosniff always;
Apache e.g.
Header always set X-Content-Type-Options nosniff

X-XSS-Protection

This header was introduced by Internet Explorer, Chrome and Safari to prevent browsers from loading the page when they detect a cross-site scripting attack. This header has since been deprecated and replaced by the CSP header. However, it is still a good practice to include this header to support users on older browsers that do not support CSP.

Nginx e.g.
add_header X-XSS-Protection "1; mode=block" always;
Apache e.g.
Header always set X-XSS-Protection "1; mode=block"

Since this security header is deprecated and blocking XSS attacks is the most suitable option for most users, I won’t explain the other options here. If you’re interested, please read Mozilla’s documentation on XSS filtering to know more.

Content-Security-Policy

Content Security Policy (CSP) is a header that helps browsers detect and mitigate certain attacks like Cross-Site Scripting (XSS) and data injection attacks. This is a complicated security header to set up as it has many options, some of which are may require difficult and tedious changes to your site.

Most of the policies involve specifying the server origin and resource endpoints. CSP allows fine-grained control of what resources the user’s browsers is allowed to load for the page. Sites that have complicated resource requirements can customize their CSP on a per-page basis. I’ll cover the basic syntax and some of the important directives.

CSP header syntax
Content-Security-Policy <directive> <value>[; <directive> <value>][...]

Each <directive> <value> pair is a policy. You can specify multiple policies in a single header. You can also provide multiple CSP headers (including specifying some directives using the <meta> tag in pages). But to keep things simple, I recommend using a single CSP header as far as possible. If for some reason, you need to use multiple CSP headers, remember that CSP headers can specify new policies but can’t undo or ease restrictions imposed by policies from previous CSP headers.

One size does not fit all especially when it comes to CSP. Modify the examples given to suit your site’s needs.
Nginx e.g.
add_header
Content-Security-Policy "default-src 'self'; upgrade-insecure-requests" always;
Apache e.g.
Header always set
Content-Security-Policy "default-src 'self'; upgrade-insecure-requests"
Nginx strict CSP example

The following is secure CSP header that is suitable for static websites that don’t use scripts, self-hosts resources (images, fonts etc.) and don’t use frames. I use Nginx variables to keep the length of lines manageable.

set $DEFAULT     "default-src 'none'";
set $BASEURI     "base-uri 'self'";
set $STYLE       "style-src 'self'";
set $SCRIPT      "script-src 'none'";
set $IMG         "img-src 'self'";
set $FONT        "font-src 'self'";
set $MEDIA       "media-src 'self'";
set $OBJECT      "object-src 'none'";
set $FRAMEANS    "frame-ancestors 'none'";
set $FORM        "form-action 'none'";
set $INSECUREURI "block-all-mixed-content";

add_header
Content-Security-Policy
"$DEFAULT; $BASEURI; $STYLE; $IMG; $FONT; $MEDIA; $OBJECT; $FRAMEANS; $SCRIPT; $FORM; $INSECUREURI" always;
default-src (mandatory)

This serves as the fallback for any fetch directives that are not specified in CSP headers. 'self' (includes single quotes) specifies the origin from which the page is being served, including the same URL scheme and port number.

The CSP example above starts with a default-src policy of 'none', which refuses loading any fetch directives that are unspecified in the CSP header. Following this, it explicitly specifies policies for required resources or directives that doesn’t have a default-src fallback. I recommend doing it this way as it starts with a disabled-by-default approach and forces you to think and specify each policy.

base-uri (recommended)

This restricts what can be used in the page’s <base> tag. If you don’t specify base-uri, the value of default-src will not be used a fallback. For this reason, consider always specifying this directive.

style-src, script-src

These specify valid sources for loading stylesheets and scripts on the page. If you are self-hosting styles and scripts on the page, specify 'self'.

Inline scripts and styles (using <script> or <style> elements) as well as inline script and style attributes, are deemed less secure compared to loading them using a separate script or stylesheet. Using 'self' is not sufficient to allow inline scripts and styles. Using 'unsafe-inline' will allow loading of inline scripts and styles, but it defeats the purpose of using a CSP and is not recommended. One alternative is to use a cryptographic nonce or hash for the inline script or style. If you can avoid using inline scripts and styles altogether, that would be ideal.

Personally, I think that browsers treating style attributes as insecure is rather extreme. Apparently, I’m not alone on this, since a style-src-attr directive has now been introduced. Unfortunately, it isn’t supported on Firefox and Safari yet.⁠[6]

img-src, font-src

These specify valid sources for loading images and fonts on the page. If you are self-hosting images and fonts used in the page, specify 'self'. Otherwise, specify the IPs or names of allowed sources for images and fonts.

media-src, object-src

These specify valid sources for media (<audio> and <video> elements) and objects (<object>, <embed> and <applet> elements). If the page doesn’t use any of these elements, specify 'none'. If you are self-hosting media and objects used in the page, specify 'self'. Otherwise, specify the IPs or names of allowed sources for media and objects.

frame-ancestors (no fallback)

This specifies valid parents that may embed a page using <frame>, <iframe>, <object>, <embed>, or <applet> elements. If the page shouldn’t be embedded, specify 'none' (which is equivalent to setting X-Frame-Options: deny). Otherwise, specify the IPs or names of allowed sources for frame ancestors.

form-action (no fallback)

This specifies URLs which can be used as the target for form submissions on a page. If you’re not using forms on the page, specify 'none'. Otherwise, specify the IPs or names of allowed target URLs for form actions.

block-all-mixed-content (recommended)

This instructs browsers to prevent loading any resources using HTTP when the page is loaded using HTTPS. An alternative is using upgrade-insecure-requests which instructs browsers to transparently treat all insecure URLs (HTTP) of your site as though they are HTTPS. It’s intended for sites with many insecure, legacy URLs that need to be secured.

Explaining all CSP policies is beyond the scope of this article. Please refer to Mozilla’s CSP documentation for the complete list of policies.

Referrer-Policy

HTTP requests often include an optional header called Referer, which contains the origin (URL) the request was made from. The Referrer-Policy security header restricts the data exposed via the Referer header. It’s useful to prevent exposing sensitive data (such as usernames, session IDs, email address etc.) from URLs on your site to unintended destinations.

The Referer header was misspelled (should’ve been “Referrer”) in the original HTTP RFC.⁠[7] By the time it was noticed, it was too late to change as HTTP had already become a widely adopted standard. However, the Referrer-Policy security header has the correct spelling.

When no Referrer-Policy is specified, the default behavior of the browser is used. It’s therefore advisable to explicitly specify the policy since defaults can vary from one browser to the next.

Nginx e.g.
add_header Referrer-Policy strict-origin-when-cross-origin always;
Apache e.g.
Header always set Referrer-Policy strict-origin-when-cross-origin

A URL is composed of the scheme, origin, path and query string. For instance, for the URL https://example.com/some/dir?user=john, the scheme is HTTPS, the origin is example.com, the path is some/dir and the query string is user=john.

The scheme is implicitly included when transmitting any other part of the URL. The scheme is said to be “downgraded” when the protocol security level is lowered (i.e., from HTTPS to HTTP) when visiting the destination.

Let’s examine the options available for restricting referrer information.

no-referrer

This instructs browsers to completely omit the Referer header. No referrer information is included.

no-referrer-when-downgrade

This is the default behavior in Firefox when no policy is specified or an invalid value is specified. It transmits the full URL if the scheme isn’t downgraded. When the scheme is downgraded, no referrer information is transmitted.

origin

This only transmits the origin.

origin-when-cross-origin

For requests to the same origin, this transmits the full URL. Otherwise, it transmits only the origin.

same-origin

For requests to the same origin, this transmits the full URL. Otherwise, no referrer information is transmitted.

strict-origin

This transmits only the origin when the scheme stays the same. When the scheme is downgraded, no referrer information is transmitted.

strict-origin-when-cross-origin

For requests to the same origin, the full URL is transmitted. For requests to a different origin and if the scheme isn’t downgraded, only the origin is transmitted. When the scheme is downgraded, no referrer information is transmitted.

unsafe-url

This transmits the full URL always regardless of the scheme. This is the worst setting security wise as it potentially leaks sensitive information in URLs. Pick any other option but this.

Permissions-Policy

Permissions-Policy, formerly known as Feature-Policy, is a security header that instructs browsers whether the page can make use of various browser or device features. For instance, if the page doesn’t need access to the microphone, you can explicitly instruct the browser to prevent any part of the page from accessing the user’s microphone.

Nginx e.g.
add_header Permissions-Policy "geolocation=(),microphone=()" always;
Apache e.g.
Header always set Permissions-Policy "geolocation=(),microphone=()"

The full list of features supported by Permissions-Policy is rather extensive and likely to grow over time. Even if you don’t specify all of them, at least include the ones you think lead to the most egregious violations of a user’s privacy or security.

X-Permitted-Cross-Domain-Policies

This header specifies how cross-domain requests to your site’s resources are handled when initiated from web clients like Adobe Flash, Adobe Acrobat etc. Implement this header to restrict loading of your site’s resources from other domains via these clients.

Nginx e.g.
add_header X-Permitted-Cross-Domain-Policies none always;
Apache e.g.
Header always set X-Permitted-Cross-Domain-Policies none

Normally the policy is specified in a master policy XML file at the root directory of your domain. However, if you can’t write to the root directory or as a matter of convenience, you can declare this policy using the X-Permitted-Cross-Domain-Policies header. For more information, read Adobe’s Cross domain workflow.

In most cases you would want to prevent loading resources to prevent cross-domain resource abuse. This is done by using none as shown in the examples above. The other available options are any one of the following.

master-only

Only this master policy file is allowed.

by-content-type

Only policy files served over HTTP/S with a Content-Type: text/x-cross-domain-policy is allowed.

by-ftp-filename

Only policy files named crossdomain.xml are allowed (applicable only to FTP).

all

All policy files on this target domain are allowed.

Configuring security headers based on the file type

If you want to optimize the size of every response, you may be tempted to configure your server to emit precisely the required security headers based on the MIME-type in the response. But before you go down that path of potential micro-optimization, consider if it’s worth optimizing in the first place.

  1. These security headers don’t take up much space. Even if you specify all of the aforementioned headers, they only add around 500 to 700 bytes more per response.

  2. It requires scrupulously deciding the precise set of security headers for each file type. For instance, you might think SVG files as image files. They are, but SVG files also support embedding CSS and JavaScript. If you have an Nginx rule for handling static image files that includes SVGs, you’ll now have to handle them differently.

  3. Which optimization is worth doing first — saving a few bytes per response or reducing the number of responses (with better caching)?

Every website has different requirements and you should be the judge of what your web server ultimately implements. I’m not against optimizing away the last byte possible. All I’m saying is, it’s prudent to optimize after thoroughly analyzing the cost, risks and rewards.

Validating security headers

Once you’ve configured the relevant security headers in your web server, you should inspect as many pages of your site as possible to ensure that they still work properly with the newly enforced policies and constraints.

There are several sites that offer HTTP security validation and scoring services. While they won’t tell you if some aspect of your page is broken due to a security policy, they’re nonetheless useful for validating security headers. They usually also offer tips on making improvements. The two security header validation sites I use are the Mozilla Observatory and SecurityHeaders.com.

Since this is a static site, it lets me tighten the screws quite a bit when it comes to security. Here’s a screenshot of my site’s A+ rating after properly configuring my security headers. After kindergarten, this is the first time I’m getting a perfect score at anything, so I just had to share :)

Ramshankar.org’s security report from SecurityHeaders.com and the Mozilla Observatory
My site’s security score from SecurityHeaders.com and the Mozilla Observatory

Don’t fret if you’re not able obtain a perfect score for your site. It’s more important to understand the nuances of each security header and assessing practical risks. Also take into account, some security-scoring sites are too eager by expecting security headers that are hardly supported by any browser. Or worse, they expect security headers that are deprecated or superseded by newer and more widely supported headers. Use your discretion in determining which security headers matter the most in practice.

Security headers keep evolving as new exploits are found and fixed. While your site’s security headers may receive an A+ rating today, frequent security audits and staying abreast of the latest developments are crucial to making your site, and the internet as a whole, more secure.


1. Recommended by the OWASP Secure Headers Projects.
2. curl ships as part of Windows 10 since Insider Build 17063.
3. The US government’s thorough explanation for the need for HSTS preloading.
4. CanIUse - HSTS is widely supported though not part of the official HSTS specification.
5. Chromium - Determining how a response is CORB protected.
6. The style-src-attr directive isn’t supported by Firefox and Safari as of Nov 1, 2020.
7. The Referer header was mispelled in the original HTTP RFC.