Content Security Policy headers can grow very long and as such are error prone if edited manually. One way to resolve that is generating them using a template language from a clean YAML structure, for example using Ansible. The Ansible setup for CSP and HPKP headers is relatively simple, especially if you compare it with the nightmarish IPSec configuration generator I wrote (but which works in production nonetheless). The whole CSP configuration sits in group variable file in YAML format (group_vars/all). Note how special CSP origins (‘none’) are placed in double quotes to ensure proper rendering by Ansible, and how the single keyword option (upgrade-insecure-requests) is assigned an empty list to render as the target keyword only:
csp:
"default-src":
- "'none'"
"img-src":
- https://webcookies-20c4.kxcdn.com
- https://*.facebook.com
- https://*.twitter.com
- https://pagead2.googlesyndication.com
"script-src":
- https://cdnjs.cloudflare.com
- https://maxcdn.bootstrapcdn.com
- https://pagead2.googlesyndication.com
- https://connect.facebook.net
- https://*.google.com
- https://*.twitter.com
- "'unsafe-inline'"
"style-src":
- https://maxcdn.bootstrapcdn.com
- https://webcookies-20c4.kxcdn.com
- "'unsafe-inline'"
"font-src":
- https://maxcdn.bootstrapcdn.com
"frame-src":
- https://*.google.com
- https://*.facebook.com
- https://*.twitter.com
- https://*.doubleclick.net
"upgrade-insecure-requests":
-
Then the template to output the Nginx header is quite simple:
add_header Content-Security-Policy "";
Same story for HTTP Public Key Pins (HPKP) header. First, the YAML configuration in the same group_vars/all file:
pkp_hashes:
- G5Yh5Mo/24pSh64SB3fhj0L5FZpnp4xjEg/INNDt9t8= # templates/backup.key.pem
- TRi1sP2dt38aFrLNgr+zmllBN3tlzm0B/Hb4JZxrGvk= # templates/backup2.key.pem
- YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg= # Let’s Encrypt Authority X3
- sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis= # Let’s Encrypt Authority X4
- C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M= # ISRG Root X1
- Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys= # DST Root CA X3 </code>
And the template (actually, the same file as above, just split for clarity):
add_header Public-Key-Pins '; max-age=3600';
The final output after Ansible task is run will be these machine-but-not-human-readable headers:
add_header Content-Security-Policy " script-src https://cdnjs.cloudflare.com https://maxcdn.bootstrapcdn.com https://pagead2.googlesyndication.com https://connect.facebook.net https://*.google.com https://*.twitter.com 'unsafe-inline'; img-src https://webcookies-20c4.kxcdn.com https://*.facebook.com https://*.twitter.com https://pagead2.googlesyndication.com; default-src 'none'; frame-src https://*.google.com https://*.facebook.com https://*.twitter.com https://*.doubleclick.net; style-src https://maxcdn.bootstrapcdn.com https://webcookies-20c4.kxcdn.com 'unsafe-inline'; upgrade-insecure-requests ; font-src https://maxcdn.bootstrapcdn.com;";
add_header Public-Key-Pins ‘pin-sha256=”G5Yh5Mo/24pSh64SB3fhj0L5FZpnp4xjEg/INNDt9t8=”; pin-sha256=”TRi1sP2dt38aFrLNgr+zmllBN3tlzm0B/Hb4JZxrGvk=”; pin-sha256=”YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=”; pin-sha256=”sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=”; pin-sha256=”C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=”; pin-sha256=”Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=”; ; max-age=3600’; </code>