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