San Sebastián de los Reyes, Madrid, Spain. May 30, 2010.

CSP Nonces in Nginx

The easy way. Not at all lazy and kind of broken.

Update 2018-07-22: Well, I finally figured out why this is a bad idea! Nginx will generate the nonce even when it’s returning a 304 response. Firefox updates the cache with the new header and then refuses to execute the previously cached content. Chrome, incorrectly, does not do this.

I have been putting off fixing my Content-Security-Policy headers for a while now. Until today. It was a beautiful day out, so what better way to spend it fixing computer things that probably nobody even notices.

I figured this would have been a solved problem, so I punched it into a search engine. The only relevant result seemed to be this blog post by Scott Helme. In it, he uses set_secure_random_alphanum to generate a nonce, and then substitutes a magic variable in his HTML pages. A perfectly cromulent solution.

However, I’m lazy an efficient worker, so I saw two problems with this:

  1. Recompiling nginx. Sure, it’s not hard, but who wants to repackage the thing every time Debian releases an update?
  2. Pages on this site are generated by Jekyll. Now there’s probably a way to modify the theme to include a magic variable, but ugh… effort.

So instead I opted to use nginx’s LUA support. Install the necessary packages:

apt install nginx-full libnginx-mod-http-lua lua-basexx

I’m sure you can also do this through manual compilation or LuaRocks or whatever, but like I said, I’m lazy and apt is the best way to install stuff anyway.

With the necessary packages installed, add the following bit to your nginx configuration:

location / {
    [...]

    set_by_lua_block $cspNonce  {
        local basexx = require('basexx')
        local file = assert(io.open('/dev/urandom', 'rb'))
        local bytes = file:read(32)
        file:close()
        return basexx.to_base64(bytes)
    }

    [...]
}

This will set the variable $cspNonce by reading from /dev/urandom. It reads 32 bytes (256 bits). Double the minimum recommended value of 16 bytes (128 bits). But feel free to fiddle with it.

The obvious next step is to add the nonce to all your <script> tags:

    sub_filter_once off;
    sub_filter_types text/html;
    sub_filter_last_modified on;
    sub_filter '<script' '<script nonce="$cspNonce"';

This will replace all instances of <script to contain your nonce. Is this a good idea? Probably not, but since I’m not sure what will blow up exactly, let’s do it anyway.

Now all you need to do is add your Content-Security-Policy header like suchly:

add_header Content-Security-Policy "default-src 'self'; script-src 'nonce-$cspNonce' 'self'; [..etc..]";

Restart nginx and you should be good to go. One thing to note is that this will break any use of gzip_static. You’re going to want to turn that off.