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 !