Experimenting with new HTTP Security Headers

8 min readJun 10, 2021


Recently, there was call at the day job to install new HTTP security headers. These headers mainly serve to do things like prevent code served by your system from *including* code that does things the user wants to do.

The day job uses Fastly to serve the location in question, but it’s otherwise static: it has no external loadables at all, no javascript (not even mouseovers), is purely https, and has no interactions. We want it to work equally well in Lynx or Curl or the latest Chrome. We have a mission of our software being accessable.

Effectively, it is a standard, static Apache download page (with FancyIndexing) to grab open source software from. As with many things, I started out myself by testing things on my own Apache server over on Gushi.org, before applying anything to the day job.

Explaining the Headers

The page I used to figure out which headers we needed to add was SecurityHeaders.com, which scans your site and gives you yay-or-nay on several headers of note (in the order they list them):

  • Content-Security-Policy
  • X-Frame-Options
  • X-Content-Type-Options
  • Referrer-Policy
  • Permissions-Policy
  • Strict-Transport-Security

I’ll go through these in order, and at the end, I’ll show how I nailed these into my Apache httpd webserver, as well as how we enabled things on our Fastly CDN.

Content-Security-Policy or Content-Security-Policy-Report-Only

This header basically is a header that says, in effect, things under this domain can only be loaded from these places in these ways.

If you are using the report-only variant of this header, it means that rather than blocking content, the browser should simply report to a URI (such as what I’m using with report-uri.com, below) and say, in browser-ese “Here’s some stuff that I would have blocked according to your existing policy”.

While there are many, many places a server can load images from off-site (share buttons, remote image loads, remote google fonts, google analytics javascripts, etc), we knew in our case that everything served via fastly was going to be on the same domain, so we were able to set a sane one:

Content-Security-Policy: "default-src 'self'"

Not everyone’s policy will be this simple. For my own domain, I signed up for an account at https://report-uri.com. Not only do they have clickable tools that will help you generate a policy (they can get quite advanced), but you can set a header so that browers will report violations of that policy back to them, and you can adjust your policy as you go along. Currently, they’re suggesting the policy I should have in place is:

Content-Security-Policy: "child-src 'self'; default-src 'self' 'unsafe-eval' 'unsafe-inline' data:; frame-ancestors 'self'; frame-src 'self'; img-src 'self' www.gstatic.com www.w3.org; script-src-attr 'unsafe-inline'; script-src-elem 'unsafe-inline'; script-src 'unsafe-eval' 'unsafe-inline'; style-src-attr 'unsafe-inline'; style-src-elem 'unsafe-inline'; style-src 'unsafe-eval' 'unsafe-inline'"

That’s a mouthful! And you’ll see there’s a number of “unsafe” words in there. According to CSP, your pages shouldn’t simply have embedded <script> tags anymore (that would be the unsafe-inline), or just javascript embedded in your HTML. Instead, you should be generating a strict “nonce” on every page, and loading scripts separately. Adding the unsafe-inline and unsafe-eval is basically your way of saying “I know I’m doing a bad thing, but at least I’m declaring it”.

I found a lot of useful reading in this Google Article: https://csp.withgoogle.com/docs/strict-csp.html

This google web fundamentals article was also useful: https://developers.google.com/web/fundamentals/security/csp


So, X-Frame-Options is a tool to prevent “clickjacking”. Unlike some of these headers, there’s only two valid values you can set, that either mean “don’t let this page be framed at all” or “only let it be framed on the same base URI”. These are the two valid options; they are mutually exclusive:

X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

There’s more info at this Mozilla Developer page: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options

We set the SAMEORIGIN policy, because the code we were using defaulted to it, even though we do not use Frames.


Arguably the most simple header here, this header stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type sent by the webserver. The only valid value for this header is:

 X-Content-Type-Options: nosniff

So that’s what we set.


Since the beginning of the web, sites have been able to tell what site you clicked on to get to a given page. In a privacy-aware world, this could be bad. Users don’t know it’s happening. (Users often don’t know a lot of things are happening, separate rant.)

Sometimes, the mechanics of ones own website require that referrer to be sent. (i.e. you jumped from an article to the general comment section, that’s important). The referrer header is also critical in preventing off-site “hotlinking”.

And arguably, there is some value not only in knowing which sites link to you, but also how many users are following which link, but the argument seems to be that this should be up to the referring sites.

Originally, I had set:

Referrer-Policy: origin-when-cross-origin

But the securityheaders.com website gave a warning for this (cryptically only saying it’s “not recommended”. I’ve changed it to:

Referrer-Policy: strict-origin

The spec is at https://www.w3.org/TR/referrer-policy/

However, I found the best document, with actual examples of what each policy did, where the origin was the same and differed only via HTTP/HTTPS was at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy

Permissions Policy

The Permissions Policy header is a collection of things that you can explicitly say “nothing on my page should try to do this weird webby thing, like provide access to the Camera, or the Microphone, or Geolocation”. You know, all the things people basically assume paranoid sites are doing (and which malicious Javascript loaded from a compromised place might.)

There is not, annoyingly, a permissions policy knob to stop sites from asking to show notifications in your notification center.

Unlike every cooking site on the internet, I’ll give you the recipe I used up front. It’s not a one-size-fits-all thing. Don’t blindly paste this stuff.

Here’s a fully “null” Permissions-Policy (generated by https://www.permissionspolicy.com). There are nice mouseovers on that site that explain what each thing means:

Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()


HTTP Strict Transport Security is a header that basically lets you say “Don’t ever load this page over plain vanilla HTTP again”. It prevents what’s called a “downgrade” attack. Additionally, it lets you say, in effect “and remember this session across browser restarts, for up to a long period of time (on the order of weeks).

We’ve had this header in place for a long time. Long before we were evaluating things using the securityheader.com site, it was required to get a good score using the SSLLabs.com site (which checks your HTTPS ciphers, certs, and the like).

Note well that this is one of the easiest headers to shoot your foot with. If you set includesubdomains and you aren’t very ready to serve everything over https, you can seriously break the user experience in a bad way due to the caching of this header that happens.

In our case, the header, for completeness, is:

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

That’s 365 days, in seconds. This was automatically set up by our CDN, Fastly. (As in, there is a single knob to turn this on, which we set when we enabled HTTPS.)

Unlike the rest of what I’ll describe later, there was no need to set any custom Varnish configs.

Putting it all in to place

So, as mentioned, my personal system where I workshopped this is running the Apache HTTPd. I do shared hosting, and Wordpress and the like come with a lot of knobs that expect .htaccess files to have an Apache syntax. So while NGINX or Lighttpd are great products, I use the tried and true classic.

Apache’s mod_headers module is responsible for setting HTTP headers for me, and in my case, that config snippet (inside my <VirtualHost> block), look like:

Header always set X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy no-referrer
Header always set Feature-Policy "camera 'none'"
Header always set Permissions-Policy "autoplay=(), microphone=()"
Header always set Content-Security-Policy "default-src 'self' 'unsafe-inline' *.youtube.com code.jquery.com; font-src 'self' fonts.googleapis.com fonts.gstatic.com;"Header always set Content-Security-Policy-Report-Only "default-src 'none'; form-action 'none'; frame-ancestors 'none'; report-uri https://gushi.report-uri.com/r/d/csp/wizard"

The break on the last two is just to display them. They’re a single line in the config, but Medium’s code formatting is not the best.

In the last link, you can see that I am using the report-only variant of the content-security policy, to try and fine-tune my policy before anything gets blocked out of hand. Even the little “W3C Valid HTML” box I have on some of my pages, needs an exception for this. So do your little Norton site security seals, or magic Share widgets, or weather embeds, or anything else you might have that loads anything from off-site.

Note that we send always set because we’d like these set even in the event of an error code.

Making this work with Fastly

It turns out there’s a great article on developer.fastly.com that tells you how to take a VCL (Varnish Control Language) snippet and apply it to your Fastly site, at a specific point in the delivery process.

Here’s the article: https://developer.fastly.com/solutions/examples/enable-modern-web-security-headers-to-all-responses, and while we of course had to tune the exact headers and their values to our needs, this made the process pretty effortless. Fastly validates any VCL you put in, so if you forget a closing quote or something, you can’t just break everything.


The web was the same for a long, long time, and in a time of increased security, browsers (rather than servers) executing more code, sites embedding more javascript to try and be more reactive (and more responsive), and with a greater service-oriented offering, there’s an increased focus on user security.

Static sites still are a thing, of course, and they can still look great with the right CSS, but the people who make the browsers and define the web protocols as they evolve are trying to make sure that a security-conscious server operator has some control over what happens after the page is served to the browser — that they can effectively give the browser the equivalent of “road signs” about what to expect, and how safely to proceed.

Securityheaders.com makes mention of several up-and-coming headers (at time of writing, they mention Cross-Origin-Embedder-Policy, Cross-Origin-Opener-Policy, and Cross-Origin-Resource-Policy), and with SSL itself having evolved several times over the past few years, this is going to continue to be a changing landscape.

Just as with enabling DMARC, there’s a requirement on the server operator to continue to monitor for feedback and reporting of failures, and that means an ecosystem of monitoring services and record generators will spring up over time. (My use of one I found for free is not any kind of official endorsement on the part of my employer.)




Gushi/Dan Mahoney is a sysadmin/network operator in Northern Washington, working for a global non-profit, as well as individually.