Piwik Hardening with Nginx and PHP-FPM

Piwik is a Free/Libre real time web analytics software. As Google Analytics, it provides you with detailed reports on your website visitors; the search engines and keywords they used, the language they speak, your popular pages, and much more.

Piwik features a modern user interface and APIs that runs on a PHP/MySQL webserver. Like every PHP based application, Piwik can present severe vulnerabilities that can be exploited by blackhat hackers. Those vulns are mostly caused by not following best practice programming rules. Less frequently, technical security flaws can affect the language itself or its core libraries.

Here comes this article in which I will try to give you my hints to secure a Piwik instance running on Nginx and PHP-FPM.

The key thing you have to know to secure Piwik is this:

  • piwik.php: statistics / API access
  • index.php: web interface access
  • All other PHP files including librairies can be access denied

Vhost configuration: sites-available/067-piwik.example.tld

server { # Statistic vhost : must be publicly available
        server_name piwik.example.tld;
        index piwik.php;
        root /var/www/piwik.example.tld/;
        access_log  /var/log/nginx/piwik.example.tld_access.log;
        error_log  /var/log/nginx/piwik.example.tld_error.log;
        location = /piwik.php { # We only allow piwik.php to be crawled.
                include conf.d/phpfpm;
        }
        include conf.d/piwik; # Security config inclusion
}
server { # Administration vhost : can be private
        listen 443;
        server_name piwik.example.tld;
        include conf.d/ssl; # SSL config inclusion - Use of SSL is strongly recommended on this vhost because piwik username/password need to be transmitted securely to the server
        include conf.d/ip-restrict; # You can restrict this vhost by IP
        index index.php;
        root /var/www/piwik.example.tld/;
        access_log  /var/log/nginx/piwik.example.tld_access.log;
        error_log  /var/log/nginx/piwik.example.tld_error.log;
        location = /index.php { # Allow index.php to access the web interface
                include conf.d/phpfpm;
        }
        # location = /piwik.php { # Allow piwik.php if you want to use the API through SSL.
        #         include conf.d/phpfpm;
        # }
        include conf.d/piwik; # Security config inclusion
}

Security configuration: conf.d/piwik

# Any other attempt to access PHP files is forbidden
location ~* ^.+\.php$ {
        return 403;
}
# Redirect to the root if attempting to access a txt file.
location ~* (?:DESIGN|(?:gpl|README|LICENSE)[^.]*|LEGALNOTICE)(?:\.txt)*$ {
        return 302 /;
}
# Disallow access to several helper files.
location ~* \.(?:bat|git|ini|sh|svn[^.]*|txt|tpl|xml)$ {
        return 404;
}
# Disallow access to directories
location ~ ^/(config|core|lang|misc|tmp)/ {
        deny all;
}

PHP-FPM configuration: conf.d/phpfpm

fastcgi_pass    127.0.0.1:9000; # Send PHP requests to your PHP-FPM server
include         fastcgi_params;

If you want to go further in the Piwik hardening, you can look at perusio's github. My Nginx configuration was mostly forked from his work.

Of course you should also update your Piwik instance as soon as an update is released.

Going much moar further!

You can even go further in hardening by removing the statistic Vhost: because piwik.js along with piwik.php can be called in a standalone way, without other files.

So, it means you just have to symlink those 2 files in the root directory of the website you want to get the stats from. That is what I did for fralef.me. (Edit: I removed piwik then because I care about privacy). Thanks to this trick, I have all the files served by fralef.me:443 and I avoid mixed-content issues - because I don't have a trusted certificate for my statistic Vhost.

$ cd /var/www/example.tld
$ ln -s ../piwik.example.tld/piwik.php .
$ ln -s ../piwik.example.tld/piwik.js .

Then reconfigure your tracking code accordingly and of course, make sure that you webserver allow symbolic links. Nginx does by default.

Enjoy. :)