Activating HTTP Public Key Pinning (HPKP) on Let's Encrypt

Disclaimer: This might break your website, don't preceded if you don't know what you're doing.

HPKP is basically a header sent by a webserver to browsers containing multiple hashes of the SPKIs from certificates, and it's function is to tell the browser that "one of these certificates has to be in the chain of the certificate used in every HTTPS session with this domain for as long as the stated max age". HPKP is one of the testing criteria on and I wanted to get an A+ so I finally decided to enable HPKP on this domain.

Since the letsencrypt-auto command seems to create a new private key every time the certificate is renewed and Let's Encrypt requires you to renew you certificate once every 30 days pinning using your certificate's SPKI is probably not the way to go. So, what should we pin then? Let's Encrypt is currently issuing from Authority X3, and using Authority X4 as a backup, so these two is a great place to start. We should also include the ISRG Root so this might support new Authorities with other SPKIs as well. Lastly we want to have a few backup private keys in case Let's Encrypt is no longer able to issue certificates so you should create that as well.

What I long feared was a complex task turned out to be quite easy with the help of Report URI and this post from Scott Helme.

Let's do it

Backup private keys

We will generate two backup private keys, one RSA and one Elliptic Curve. To do this run the following openssl commands:

openssl req -nodes -sha256 -newkey rsa:4096 -keyout "" -out ""
openssl req -nodes -newkey ec:<(openssl ecparam -name prime256v1) -keyout "" -out ""

Replace with your domain, you can leave the last two of the prompts empty on both (A challenge password and an optional company name). If you want to know what these commands do, and the rest of the commands in this post (and you should want to know that before you copy-paste them into your terminal), head over the post by Scott Helme mentioned earlier in the text.

To generate the hash of the SPKI of these certificates run the following commands

openssl req -pubkey < | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
openssl req -pubkey < | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

Again replace with your domain. Remember to keep the resulting .key and .csr files in a safe place, and to not loose them as your domain might end up being unusable for the max age time if Let's Encrypt stop signing new certificates.

Let's Encrypt certificates

Generate the ISRG Root X1, Let’s Encrypt Authority X3 and Let’s Encrypt Authority X4 SPKI-hash using the following command:

curl | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
curl | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
curl | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64


Now take all the hashes you have generated and combine them into a config, replace Hash1 - Hash5 with the hashes. This will set max age to 1 minute, so you can verify that your config is working before going all in.


add_header Public-Key-Pins 'pin-sha256="Hash1"; pin-sha256="Hash2"; pin-sha256="Hash3"; pin-sha256="Hash4"; pin-sha256="Hash5"; max-age=60;';


Header always set Public-Key-Pins "pin-sha256=\"Hash1\"; pin-sha256=\"Hash2\"; pin-sha256=\"Hash3\"; pin-sha256=\"Hash4\"; pin-sha256=\"Hash5\"; max-age=60;";

Test your config

To test your config head over to Report URIs HPKP Analyser

If your config works you can now increase max-age. I have chosen 5184000, which is roughly 2 months. You can also add includeSubDomains after max-age if you want the policy to apply to sub-domains as well.

Please note that this will limit the certificates a browser will accept for your website to certificates issued by Let's Encrypt and issued using the private keys you have generated, but will not have any effect against a breech of Let's Encrypt or you.

Edit: The article is updated after feedback from reddit-users graingert, HSP95, BezierPatch and veeti. on, A+

This article is my oldest. It is 704 words long