# Self-hosted report-uri

27 November 2018

I’ve been playing with the security headers for this website for the past few days, most notably with the `Content-Security-Policy` as well as the `Expect-CT` headers.

After having spent a few hours on this, I’m pretty happy with the results !

Source : Observatory by Mozilla

This website runs on a Ghost installation that I keep up-to-date. Since an update might mean that the site will try to load new external resources, the `Content-Security-Policy` header might need updating as well.

This header has a `report-uri` directive that makes web browsers send json-formatted messages of policy violations they encounter.

There’s a great website (Report-URI) that you can use to handle these reports. It allows up to 10.000 reports per month with a free account, which should be enough for a low to mid trafic website once you’ve setup your initial policy.

However, since I’m all about self-hosting all of the things, I figured I would configure my own report-uri using a php script.

## The script

This script is heavily inspired from the ones available here and here.

The script checks that the content that was sent by the web browser is correctly formatted json message. It then removes the backslashes from the message, opens a connection to the local syslog daemon and sends the message.

    <?php
      // Send `204 No Content` status code.
      http_response_code(204);
      // collect data from post request
      $data = file_get_contents('php://input');
      if ($data = json_decode($data)) {
        // Remove slashes from the JSON-formatted data.
        $data = json_encode(
          $data, JSON_UNESCAPED_SLASHES
        );
        # set options for syslog daemon
        openlog('report-uri', LOG_NDELAY, LOG_USER);

        # send warning about csp report
        syslog(LOG_WARNING, $data);
      }
    ?>

## Nginx

I won’t go into too much details regarding the nginx configuration here as I’ve written on this subject before.

Since I now have a wildcard Let’s Encrypt certificate on captainark.net, I’ve decided to use a dedicated vhost for my report-uri. However, a subfolder would work just as well. Just make sure the script is stored in a folder that nginx can access.

I’ve also decided to call the script `index.php`. You can call it whatever you want, but your `report-uri` directive will have to match the full URL of the script (if I had named the script `report.php`, my `report-uri` would have been `https://report-uri.captainark.net/report.php` instead of `https://report-uri.captainark.net`).

A nginx location configured as follows should do the trick :

      location / {
        index index.php;

        location ~ \.php$ {
          try_files $uri =404;
          fastcgi_split_path_info ^(.+?\.php)(/.*)$;
          fastcgi_pass unix:/run/php/php7.0-fpm.sock;
          fastcgi_index index.php;
          include fastcgi.conf;
          fastcgi_hide_header X-Powered-By;
        }
      }

I’ve omitted the security headers I usually configure in all locations here because they are outside of the scope of this article (HSTS, X-Frame-Options, etc.)

Once you’ve configured nginx, you can `nginx -t` to check that the syntax is correct, and `nginx -s reload` to reload the configuration.

## Syslog-ng

Now that our reports are being sent to syslog-ng, we need to log them as proprely formatted json messages, in a dedicated file.

I’ve created a `/etc/syslog-ng/conf.d/report-uri.conf` configuration file for that :

    filter f_report-uri { program ("report-uri"); };
    destination d_report-uri { file ("/var/log/report-uri/report-uri.json" template("{\"@timestamp\": \"${ISODATE}\", \"host\": \"${HOST}\", \"message\": ${MSG} }\n")); };
    log { source(s_src); filter (f_report-uri); destination (d_report-uri); flags(final); };

We’ll also need to create the folder for the logs :

    mkdir -m 0750 /var/log/report-uri
    chown root:adm /var/log/report-uri

You can then reload syslog-ng with a `systemctl reload syslog-ng.service`

Policy violation messages should now start to appear in the `/var/log/report-uri/report-uri.json`

If you want to test that it’s working, you can create a `csp.json` file with the following content :

    {"csp-report":{"document-uri":"https://www.captainark.net/foo/bar","referrer":"https://www.google.com/","violated-directive":"default-src self","original-policy":"default-src self; report-uri https://report-uri.captainark.net","blocked-uri":"http://jscryptocurrency.cx"}}

You can now `POST` it to your report-uri :

    curl -XPOST https://report-uri.captainark.net -d @csp.json

The message should be added to your `report-uri.json` log file, and you should be able to prettify it with `jq` :

    tail -n1 /var/log/report-uri/report-uri.json | jq
    {
      "@timestamp": "2018-11-27T22:57:06+01:00",
      "host": "webserver",
      "message": {
        "csp-report": {
          "document-uri": "https://www.captainark.net/foo/bar",
          "referrer": "https://www.google.com/",
          "violated-directive": "default-src self",
          "original-policy": "default-src self; report-uri https://report-uri.captainark.net",
          "blocked-uri": "http://jscryptocurrency.cx"
        }
      }
    }

## Logrotate

It’s always a good idea to configure a log rotation when you add a new log file. To do so, let’s create the `/etc/logrotate.d/report-uri` file with the following content :

    /var/log/report-uri/report-uri.json {
      rotate 8
      weekly
      notifempty
      missingok
      create 640 root adm
      compress
      copytruncate
    }

## Conclusion

This configuration works as a report-uri for the `Content-Security` header as well as the newer `Expect-CT` header, and any future header that uses a report-uri directive (as long as the generated messages are json formatted).

Having a log file instead of the clean web interface of Report URI is not for everybody, but it is more than enough for my use case (this site gets like 10 clicks a day when I’m not playing with it so… yeah.)

Since the log messages are formatted in json, they should be pretty easy to integrate in Elasticsearch or Graylog. If I ever decide to configure one of those solutions, I should then be able to configure cool looking dashboards in Grafana as well.

As always, if you’ve found this article useful in any way, please let me know in the comments here, on Twitter or on the Fediverse if you’re a real cool kid !

## Debian repos over HTTPS

## DNS zone versioning