at 17.09-beta 8.3 kB view raw
1{ config, lib, pkgs, services, ... }: 2with lib; 3let 4 cfg = config.services.piwik; 5 6 user = "piwik"; 7 dataDir = "/var/lib/${user}"; 8 9 pool = user; 10 # it's not possible to use /run/phpfpm/${pool}.sock because /run/phpfpm/ is root:root 0770, 11 # and therefore is not accessible by the web server. 12 phpSocket = "/run/phpfpm-${pool}.sock"; 13 phpExecutionUnit = "phpfpm-${pool}"; 14 databaseService = "mysql.service"; 15 16in { 17 options = { 18 services.piwik = { 19 # NixOS PR for database setup: https://github.com/NixOS/nixpkgs/pull/6963 20 # piwik issue for automatic piwik setup: https://github.com/piwik/piwik/issues/10257 21 # TODO: find a nice way to do this when more NixOS MySQL and / or piwik automatic setup stuff is implemented. 22 enable = mkOption { 23 type = types.bool; 24 default = false; 25 description = '' 26 Enable piwik web analytics with php-fpm backend. 27 Either the nginx option or the webServerUser option is mandatory. 28 ''; 29 }; 30 31 webServerUser = mkOption { 32 type = types.nullOr types.str; 33 default = null; 34 example = "lighttpd"; 35 description = '' 36 Name of the web server user that forwards requests to the ${phpSocket} fastcgi socket for piwik if the nginx 37 option is not used. Either this option or the nginx option is mandatory. 38 If you want to use another webserver than nginx, you need to set this to that server's user 39 and pass fastcgi requests to `index.php` and `piwik.php` to this socket. 40 ''; 41 }; 42 43 phpfpmProcessManagerConfig = mkOption { 44 type = types.str; 45 default = '' 46 ; default phpfpm process manager settings 47 pm = dynamic 48 pm.max_children = 75 49 pm.start_servers = 10 50 pm.min_spare_servers = 5 51 pm.max_spare_servers = 20 52 pm.max_requests = 500 53 54 ; log worker's stdout, but this has a performance hit 55 catch_workers_output = yes 56 ''; 57 description = '' 58 Settings for phpfpm's process manager. You might need to change this depending on the load for piwik. 59 ''; 60 }; 61 62 nginx = mkOption { 63 type = types.nullOr (types.submodule (import ../web-servers/nginx/vhost-options.nix { 64 inherit config lib; 65 })); 66 default = null; 67 example = { 68 serverName = "stats.$\{config.networking.hostName\}"; 69 enableACME = false; 70 }; 71 description = '' 72 With this option, you can customize an nginx virtualHost which already has sensible defaults for piwik. 73 Either this option or the webServerUser option is mandatory. 74 Set this to {} to just enable the virtualHost if you don't need any customization. 75 If enabled, then by default, the serverName is piwik.$\{config.networking.hostName\}, SSL is active, 76 and certificates are acquired via ACME. 77 If this is set to null (the default), no nginx virtualHost will be configured. 78 ''; 79 }; 80 }; 81 }; 82 83 config = mkIf cfg.enable { 84 warnings = mkIf (cfg.nginx != null && cfg.webServerUser != null) [ 85 "If services.piwik.nginx is set, services.piwik.nginx.webServerUser is ignored and should be removed." 86 ]; 87 88 assertions = [ { 89 assertion = cfg.nginx != null || cfg.webServerUser != null; 90 message = "Either services.piwik.nginx or services.piwik.nginx.webServerUser is mandatory"; 91 }]; 92 93 users.extraUsers.${user} = { 94 isSystemUser = true; 95 createHome = true; 96 home = dataDir; 97 group = user; 98 }; 99 users.extraGroups.${user} = {}; 100 101 systemd.services.piwik_setup_update = { 102 # everything needs to set up and up to date before piwik php files are executed 103 requiredBy = [ "${phpExecutionUnit}.service" ]; 104 before = [ "${phpExecutionUnit}.service" ]; 105 # the update part of the script can only work if the database is already up and running 106 requires = [ databaseService ]; 107 after = [ databaseService ]; 108 path = [ pkgs.piwik ]; 109 serviceConfig = { 110 Type = "oneshot"; 111 User = user; 112 # hide especially config.ini.php from other 113 UMask = "0007"; 114 Environment = "PIWIK_USER_PATH=${dataDir}"; 115 # chown + chmod in preStart needs root 116 PermissionsStartOnly = true; 117 }; 118 # correct ownership and permissions in case they're not correct anymore, 119 # e.g. after restoring from backup or moving from another system. 120 # Note that ${dataDir}/config/config.ini.php might contain the MySQL password. 121 preStart = '' 122 chown -R ${user}:${user} ${dataDir} 123 chmod -R ug+rwX,o-rwx ${dataDir} 124 ''; 125 script = '' 126 # Use User-Private Group scheme to protect piwik data, but allow administration / backup via piwik group 127 # Copy config folder 128 chmod g+s "${dataDir}" 129 cp -r "${pkgs.piwik}/config" "${dataDir}/" 130 chmod -R u+rwX,g+rwX,o-rwx "${dataDir}" 131 132 # check whether user setup has already been done 133 if test -f "${dataDir}/config/config.ini.php"; then 134 # then execute possibly pending database upgrade 135 piwik-console core:update --yes 136 fi 137 ''; 138 }; 139 140 systemd.services.${phpExecutionUnit} = { 141 # stop phpfpm on package upgrade, do database upgrade via piwik_setup_update, and then restart 142 restartTriggers = [ pkgs.piwik ]; 143 # stop config.ini.php from getting written with read permission for others 144 serviceConfig.UMask = "0007"; 145 }; 146 147 services.phpfpm.poolConfigs = let 148 # workaround for when both are null and need to generate a string, 149 # which is illegal, but as assertions apparently are being triggered *after* config generation, 150 # we have to avoid already throwing errors at this previous stage. 151 socketOwner = if (cfg.nginx != null) then config.services.nginx.user 152 else if (cfg.webServerUser != null) then cfg.webServerUser else ""; 153 in { 154 ${pool} = '' 155 listen = "${phpSocket}" 156 listen.owner = ${socketOwner} 157 listen.group = root 158 listen.mode = 0600 159 user = ${user} 160 env[PIWIK_USER_PATH] = ${dataDir} 161 ${cfg.phpfpmProcessManagerConfig} 162 ''; 163 }; 164 165 166 services.nginx.virtualHosts = mkIf (cfg.nginx != null) { 167 # References: 168 # https://fralef.me/piwik-hardening-with-nginx-and-php-fpm.html 169 # https://github.com/perusio/piwik-nginx 170 "${user}.${config.networking.hostName}" = mkMerge [ cfg.nginx { 171 # don't allow to override root, as it will almost certainly break piwik 172 root = mkForce "${pkgs.piwik}/share"; 173 174 # allow to override SSL settings if necessary, i.e. when using another method than ACME 175 # but enable them by default, as sensitive login and piwik data should not be transmitted in clear text. 176 forceSSL = mkDefault true; 177 enableACME = mkDefault true; 178 179 locations."/" = { 180 index = "index.php"; 181 }; 182 # allow index.php for webinterface 183 locations."= /index.php".extraConfig = '' 184 fastcgi_pass unix:${phpSocket}; 185 ''; 186 # allow piwik.php for tracking 187 locations."= /piwik.php".extraConfig = '' 188 fastcgi_pass unix:${phpSocket}; 189 ''; 190 # Any other attempt to access any php files is forbidden 191 locations."~* ^.+\.php$".extraConfig = '' 192 return 403; 193 ''; 194 # Disallow access to unneeded directories 195 # config and tmp are already removed 196 locations."~ ^/(?:core|lang|misc)/".extraConfig = '' 197 return 403; 198 ''; 199 # Disallow access to several helper files 200 locations."~* \.(?:bat|git|ini|sh|txt|tpl|xml|md)$".extraConfig = '' 201 return 403; 202 ''; 203 # No crawling of this site for bots that obey robots.txt - no useful information here. 204 locations."= /robots.txt".extraConfig = '' 205 return 200 "User-agent: *\nDisallow: /\n"; 206 ''; 207 # let browsers cache piwik.js 208 locations."= /piwik.js".extraConfig = '' 209 expires 1M; 210 ''; 211 }]; 212 }; 213 }; 214 215 meta = { 216 doc = ./piwik-doc.xml; 217 maintainers = with stdenv.lib.maintainers; [ florianjacob ]; 218 }; 219}