The Sound of Silence

[Updated 3/29/2016 to reflect updated urls at report-uri.io]

Qualys SSL Labs’ SSL tester here, and Scott Helme’s securityheaders.io here, have very useful tools when attempting to harden one’s website to things like XSS attacks. One of my websites gets an A+ on the SSL tester and all of them currently get at least an A on securityheaders.io. The latter is more difficult to please, and for good reason.

Content Security Policy

While rather straightforward in concept, creating a coherent content security policy was more difficult than I expected it to be. Turns out I had a heck of a lot more external resources on this (WordPress enabled) website than I expected. So one of my nginx config files features the following line (breaks added for the sake of clarity):

add_header Content-Security-Policy "default-src 'self'; \
connect-src 'self'; \
script-src 'self' 'unsafe-inline' 'unsafe-eval'; \
img-src 'self' data: *.wp.com http://*.gravatar.com *.w.org; \
style-src 'self' 'unsafe-inline' fonts.googleapis.com; \
font-src 'self' data: fonts.gstatic.com; \
child-src 'self' https://www.youtube.com; \
object-src 'none'; \
media-src 'self; \
report-uri https://<my_subdomain>.report-uri.io/r/default/csp/enforce";

Lots of people reverse the double and single quotes, but that’s more of a preference thing, I believe. Doesn’t affect the outcome. Now I won’t go over all the elements, but I will point out a few things I learned. If you wish to restrict the protocol (http:// vs. https://), include that in the urls given. If you do not, a wildcard (e.g. “*://*.wp.com”) will not do what you might expect.

The keyword 'unsafe-inline'  I misunderstood at first. They allow the use of inline scripts and styles, which I first thought would be a given, but that can be unsafe, thus the restriction unless these are added. 'unsafe-eval' is similar in that it allows the use of the eval()  function in JavaScript. This I know to be rather unsafe, but rather than rewrite one particular script on my site, I decided to allow this, at least for the time being.

The report-uri directive was the hardest for me to get, because at first I thought I was going to have to create my own method for handling any and all requests this was going to generate. Then I found report-ui.io. Mr. Helme built that site as well, and it’s free to use while evaluating CSP and HPKP headers for one’s server.

ACME Protocol

Before I get to HPKP, there’s a “feature” of the ACME protocol that had me scratching my head for a long while. It requires an HTTP request (non-SSL/TLS) to verify ownership of the domain(s). The Let’s Encrypt client as well as others (lego, acme_tiny.py, etc.) set up their own servers to do the handshaking, which is fine as far as it goes, but in a production environment, one doesn’t want to drop a server even for the few seconds required to renew a certificate.

Anyway, I had configured my server to redirect all incoming URIs to use SSL/TLS, which meant even the directory that had to be accessible over HTTP was getting moved, and it wasn’t/isn’t in that server block. Eventually, I hacked together a new one:

server {
	listen 80;
	listen [::]:80;
	server_name example.com *.example.com;
	location ^~ /.well-known/acme-challenge/ {
		alias /path/to/challenges/;
		try_files $uri =404;
	}
	location / {
		return 301 https://$server_name$request_uri;
	}
}
server {
	listen 443;
	listen [::]:443;
	# the rest of ssl config goes here
}

Because of the way Chrome and Firefox at least cache the redirect, if one visits http://example.com, she is redirected to https://example.com. This is the typical desired behavior. If one subsequently attempts to visit http://example.com/.well-known/acme-challenge/index.html (assuming such a file exists), she would be redirected to https://example.com/.well-known/acme-challenge/index.html, which in the case of my server, does not exist. However, if one were to visit http://example.com/.well-known/acme-challenge/index.html initially, that page would display perfectly well—until any other directory on the server is viewed.

As far as I can tell, this does not cause an issue for most of the clients, as they don’t check other locations. I’m far from an expert on this, and am only relating my own experiences. The next section will eventually cover the ACME client I use and how I created my certificate(s).

HTTP Public Key Pinning

I still don’t totally understand this one. While I get the idea of ensuring visitors haven’t been duped into using a bogus or compromised certificate (that makes sense), exactly how this part works still is a bit confusing. The following is a work-in-progress.

My header:

add_header Public-Key-Pins-Report-Only "pin-sha256='<base64hash1>'; \
pin-sha256='<base64hash2>'; \
pin-sha256='<base64hash3>'; \
max-age=10; \
includeSubdomains; \
report-uri='https://<my_subdomain>.report-uri.io/r/default/hpkp/reportOnly'";

I’m really not sure what comes next. I’m not sure if I’m supposed to have all three certificates in my chain in that header, or if one of them is supposed to be a backup certificate (which also seems to be recommended), or what. I’ve followed this post about Let’s Encrypt and creating a more generic certificate using the ACME protocol (so I now have a certificate that covers all my domains). Thing is, the protocol (or maybe just the script) seems restricted to port 80, and if I set up my websites to force SSL, port 80 will get switched to port 443, and the script (cron-based renewal) will fail. And I’ll have to reconfigure my server blocks to open port 80 up, even for one directory, for 60 seconds or so every 90 days…

Let’s just say I’m sure there’s a better way, or something I’m totally missing. And if/when that time comes, I’ll update this.