Self-hosted report-uri

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 !

Screenshot-2018-11-27-at-21.52.58 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.

  // Send `204 No Content` status code.
  // 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(
    # set options for syslog daemon
    openlog('report-uri', LOG_NDELAY, LOG_USER);

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


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, 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 instead of

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.


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":"","referrer":"","violated-directive":"default-src self","original-policy":"default-src self; report-uri","blocked-uri":""}}

You can now POST it to your report-uri :

curl -XPOST -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": "",
      "referrer": "",
      "violated-directive": "default-src self",
      "original-policy": "default-src self; report-uri",
      "blocked-uri": ""


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
  create 640 root adm


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 !