1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.mysql;
10
11 isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb;
12 isOracle = lib.getName cfg.package == lib.getName pkgs.mysql80;
13 # Oracle MySQL has supported "notify" service type since 8.0
14 hasNotify = isMariaDB || (isOracle && lib.versionAtLeast cfg.package.version "8.0");
15
16 mysqldOptions = "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}";
17
18 format = pkgs.formats.ini { listsAsDuplicateKeys = true; };
19 configFile = format.generate "my.cnf" cfg.settings;
20
21 generateClusterAddressExpr = ''
22 if (config.services.mysql.galeraCluster.nodeAddresses == [ ]) then
23 ""
24 else
25 "gcomm://''${builtins.concatStringsSep \",\" config.services.mysql.galeraCluster.nodeAddresses}"
26 + lib.optionalString (config.services.mysql.galeraCluster.clusterPassword != "")
27 "?gmcast.seg=1:''${config.services.mysql.galeraCluster.clusterPassword}"
28 '';
29 generateClusterAddress =
30 if (cfg.galeraCluster.nodeAddresses == [ ]) then
31 ""
32 else
33 "gcomm://${builtins.concatStringsSep "," cfg.galeraCluster.nodeAddresses}"
34 + lib.optionalString (
35 cfg.galeraCluster.clusterPassword != ""
36 ) "?gmcast.seg=1:${cfg.galeraCluster.clusterPassword}";
37in
38
39{
40 imports = [
41 (lib.mkRemovedOptionModule [
42 "services"
43 "mysql"
44 "pidDir"
45 ] "Don't wait for pidfiles, describe dependencies through systemd.")
46 (lib.mkRemovedOptionModule [
47 "services"
48 "mysql"
49 "rootPassword"
50 ] "Use socket authentication or set the password outside of the nix store.")
51 (lib.mkRemovedOptionModule [
52 "services"
53 "mysql"
54 "extraOptions"
55 ] "Use services.mysql.settings.mysqld instead.")
56 (lib.mkRemovedOptionModule [
57 "services"
58 "mysql"
59 "bind"
60 ] "Use services.mysql.settings.mysqld.bind-address instead.")
61 (lib.mkRemovedOptionModule [
62 "services"
63 "mysql"
64 "port"
65 ] "Use services.mysql.settings.mysqld.port instead.")
66 ];
67
68 ###### interface
69
70 options = {
71
72 services.mysql = {
73
74 enable = lib.mkEnableOption "MySQL server";
75
76 package = lib.mkOption {
77 type = lib.types.package;
78 example = lib.literalExpression "pkgs.mariadb";
79 description = ''
80 Which MySQL derivation to use. MariaDB packages are supported too.
81 '';
82 };
83
84 user = lib.mkOption {
85 type = lib.types.str;
86 default = "mysql";
87 description = ''
88 User account under which MySQL runs.
89
90 ::: {.note}
91 If left as the default value this user will automatically be created
92 on system activation, otherwise you are responsible for
93 ensuring the user exists before the MySQL service starts.
94 :::
95 '';
96 };
97
98 group = lib.mkOption {
99 type = lib.types.str;
100 default = "mysql";
101 description = ''
102 Group account under which MySQL runs.
103
104 ::: {.note}
105 If left as the default value this group will automatically be created
106 on system activation, otherwise you are responsible for
107 ensuring the user exists before the MySQL service starts.
108 :::
109 '';
110 };
111
112 dataDir = lib.mkOption {
113 type = lib.types.path;
114 example = "/var/lib/mysql";
115 description = ''
116 The data directory for MySQL.
117
118 ::: {.note}
119 If left as the default value of `/var/lib/mysql` this directory will automatically be created before the MySQL
120 server starts, otherwise you are responsible for ensuring the directory exists with appropriate ownership and permissions.
121 :::
122 '';
123 };
124
125 configFile = lib.mkOption {
126 type = lib.types.path;
127 default = configFile;
128 defaultText = ''
129 A configuration file automatically generated by NixOS.
130 '';
131 description = ''
132 Override the configuration file used by MySQL. By default,
133 NixOS generates one automatically from {option}`services.mysql.settings`.
134 '';
135 example = lib.literalExpression ''
136 pkgs.writeText "my.cnf" '''
137 [mysqld]
138 datadir = /var/lib/mysql
139 bind-address = 127.0.0.1
140 port = 3336
141
142 !includedir /etc/mysql/conf.d/
143 ''';
144 '';
145 };
146
147 settings = lib.mkOption {
148 type = format.type;
149 default = { };
150 description = ''
151 MySQL configuration. Refer to
152 <https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html>,
153 <https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html>,
154 and <https://mariadb.com/kb/en/server-system-variables/>
155 for details on supported values.
156
157 ::: {.note}
158 MySQL configuration options such as `--quick` should be treated as
159 boolean options and provided values such as `true`, `false`,
160 `1`, or `0`. See the provided example below.
161 :::
162 '';
163 example = lib.literalExpression ''
164 {
165 mysqld = {
166 key_buffer_size = "6G";
167 table_cache = 1600;
168 log-error = "/var/log/mysql_err.log";
169 plugin-load-add = [ "server_audit" "ed25519=auth_ed25519" ];
170 };
171 mysqldump = {
172 quick = true;
173 max_allowed_packet = "16M";
174 };
175 }
176 '';
177 };
178
179 initialDatabases = lib.mkOption {
180 type = lib.types.listOf (
181 lib.types.submodule {
182 options = {
183 name = lib.mkOption {
184 type = lib.types.str;
185 description = ''
186 The name of the database to create.
187 '';
188 };
189 schema = lib.mkOption {
190 type = lib.types.nullOr lib.types.path;
191 default = null;
192 description = ''
193 The initial schema of the database; if null (the default),
194 an empty database is created.
195 '';
196 };
197 };
198 }
199 );
200 default = [ ];
201 description = ''
202 List of database names and their initial schemas that should be used to create databases on the first startup
203 of MySQL. The schema attribute is optional: If not specified, an empty database is created.
204 '';
205 example = lib.literalExpression ''
206 [
207 { name = "foodatabase"; schema = ./foodatabase.sql; }
208 { name = "bardatabase"; }
209 ]
210 '';
211 };
212
213 initialScript = lib.mkOption {
214 type = lib.types.nullOr lib.types.path;
215 default = null;
216 description = "A file containing SQL statements to be executed on the first startup. Can be used for granting certain permissions on the database.";
217 };
218
219 ensureDatabases = lib.mkOption {
220 type = lib.types.listOf lib.types.str;
221 default = [ ];
222 description = ''
223 Ensures that the specified databases exist.
224 This option will never delete existing databases, especially not when the value of this
225 option is changed. This means that databases created once through this option or
226 otherwise have to be removed manually.
227 '';
228 example = [
229 "nextcloud"
230 "matomo"
231 ];
232 };
233
234 ensureUsers = lib.mkOption {
235 type = lib.types.listOf (
236 lib.types.submodule {
237 options = {
238 name = lib.mkOption {
239 type = lib.types.str;
240 description = ''
241 Name of the user to ensure.
242 '';
243 };
244 ensurePermissions = lib.mkOption {
245 type = lib.types.attrsOf lib.types.str;
246 default = { };
247 description = ''
248 Permissions to ensure for the user, specified as attribute set.
249 The attribute names specify the database and tables to grant the permissions for,
250 separated by a dot. You may use wildcards here.
251 The attribute values specfiy the permissions to grant.
252 You may specify one or multiple comma-separated SQL privileges here.
253
254 For more information on how to specify the target
255 and on which privileges exist, see the
256 [GRANT syntax](https://mariadb.com/kb/en/library/grant/).
257 The attributes are used as `GRANT ''${attrName} ON ''${attrValue}`.
258 '';
259 example = lib.literalExpression ''
260 {
261 "database.*" = "ALL PRIVILEGES";
262 "*.*" = "SELECT, LOCK TABLES";
263 }
264 '';
265 };
266 };
267 }
268 );
269 default = [ ];
270 description = ''
271 Ensures that the specified users exist and have at least the ensured permissions.
272 The MySQL users will be identified using Unix socket authentication. This authenticates the Unix user with the
273 same name only, and that without the need for a password.
274 This option will never delete existing users or remove permissions, especially not when the value of this
275 option is changed. This means that users created and permissions assigned once through this option or
276 otherwise have to be removed manually.
277 '';
278 example = lib.literalExpression ''
279 [
280 {
281 name = "nextcloud";
282 ensurePermissions = {
283 "nextcloud.*" = "ALL PRIVILEGES";
284 };
285 }
286 {
287 name = "backup";
288 ensurePermissions = {
289 "*.*" = "SELECT, LOCK TABLES";
290 };
291 }
292 ]
293 '';
294 };
295
296 replication = {
297 role = lib.mkOption {
298 type = lib.types.enum [
299 "master"
300 "slave"
301 "none"
302 ];
303 default = "none";
304 description = "Role of the MySQL server instance.";
305 };
306
307 serverId = lib.mkOption {
308 type = lib.types.int;
309 default = 1;
310 description = "Id of the MySQL server instance. This number must be unique for each instance.";
311 };
312
313 masterHost = lib.mkOption {
314 type = lib.types.str;
315 description = "Hostname of the MySQL master server.";
316 };
317
318 slaveHost = lib.mkOption {
319 type = lib.types.str;
320 description = "Hostname of the MySQL slave server.";
321 };
322
323 masterUser = lib.mkOption {
324 type = lib.types.str;
325 description = "Username of the MySQL replication user.";
326 };
327
328 masterPassword = lib.mkOption {
329 type = lib.types.str;
330 description = "Password of the MySQL replication user.";
331 };
332
333 masterPort = lib.mkOption {
334 type = lib.types.port;
335 default = 3306;
336 description = "Port number on which the MySQL master server runs.";
337 };
338 };
339
340 galeraCluster = {
341 enable = lib.mkEnableOption "MariaDB Galera Cluster";
342
343 package = lib.mkOption {
344 type = lib.types.package;
345 description = "The MariaDB Galera package that provides the shared library 'libgalera_smm.so' required for cluster functionality.";
346 default = lib.literalExpression "pkgs.mariadb-galera";
347 };
348
349 name = lib.mkOption {
350 type = lib.types.str;
351 description = "The logical name of the Galera cluster. All nodes in the same cluster must use the same name.";
352 default = "galera";
353 };
354
355 sstMethod = lib.mkOption {
356 type = lib.types.enum [
357 "rsync"
358 "mariabackup"
359 ];
360 description = "Method for the initial state transfer (wsrep_sst_method) when a node joins the cluster. Be aware that rsync needs SSH keys to be generated and authorized on all nodes!";
361 default = "rsync";
362 example = "mariabackup";
363 };
364
365 localName = lib.mkOption {
366 type = lib.types.str;
367 description = "The unique name that identifies this particular node within the cluster. Each node must have a different name.";
368 example = "node1";
369 };
370
371 localAddress = lib.mkOption {
372 type = lib.types.str;
373 description = "IP address or hostname of this node that will be used for cluster communication. Must be reachable by all other nodes.";
374 example = "1.2.3.4";
375 default = cfg.galeraCluster.localName;
376 defaultText = lib.literalExpression "config.services.mysql.galeraCluster.localName";
377 };
378
379 nodeAddresses = lib.mkOption {
380 type = lib.types.listOf lib.types.str;
381 description = "IP addresses or hostnames of all nodes in the cluster, including this node. This is used to construct the default clusterAddress connection string.";
382 example = lib.literalExpression ''["10.0.0.10" "10.0.0.20" "10.0.0.30"]'';
383 default = [ ];
384 };
385
386 clusterPassword = lib.mkOption {
387 type = lib.types.str;
388 description = "Optional password for securing cluster communications. If provided, it will be used in the clusterAddress for authentication between nodes.";
389 example = "SomePassword";
390 default = "";
391 };
392
393 clusterAddress = lib.mkOption {
394 type = lib.types.str;
395 description = "Full Galera cluster connection string. If nodeAddresses is set, this will be auto-generated, but you can override it with a custom value. Format is typically 'gcomm://node1,node2,node3' with optional parameters.";
396 example = "gcomm://10.0.0.10,10.0.0.20,10.0.0.30?gmcast.seg=1:SomePassword";
397 default = ""; # will be evaluate by generateClusterAddress
398 defaultText = lib.literalExpression generateClusterAddressExpr;
399 };
400
401 };
402 };
403
404 };
405
406 ###### implementation
407
408 config = lib.mkIf cfg.enable {
409 assertions = [
410 {
411 assertion = !cfg.galeraCluster.enable || isMariaDB;
412 message = "'services.mysql.galeraCluster.enable' expect services.mysql.package to be an mariadb variant";
413 }
414 ]
415 # galeraCluster options checks
416 ++ lib.optionals cfg.galeraCluster.enable [
417 {
418 assertion =
419 cfg.galeraCluster.localAddress != ""
420 && (cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "");
421 message = "mariadb galera cluster is enabled but the localAddress and (nodeAddresses or clusterAddress) are not set";
422 }
423 {
424 assertion = cfg.galeraCluster.clusterPassword == "" || cfg.galeraCluster.clusterAddress == "";
425 message = "mariadb galera clusterPassword is set but overwritten by clusterAddress";
426 }
427 {
428 assertion = cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "";
429 message = "When services.mysql.galeraCluster.clusterAddress is set, setting services.mysql.galeraCluster.nodeAddresses is redundant and will be overwritten by clusterAddress. Choose one approach.";
430 }
431 ];
432
433 services.mysql.dataDir = lib.mkDefault (
434 if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" else "/var/mysql"
435 );
436
437 services.mysql.settings.mysqld = lib.mkMerge [
438 {
439 datadir = cfg.dataDir;
440 port = lib.mkDefault 3306;
441 }
442 (lib.mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
443 log-bin = "mysql-bin-${toString cfg.replication.serverId}";
444 log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
445 relay-log = "mysql-relay-bin";
446 server-id = cfg.replication.serverId;
447 binlog-ignore-db = [
448 "information_schema"
449 "performance_schema"
450 "mysql"
451 ];
452 })
453 (lib.mkIf (!isMariaDB) {
454 plugin-load-add = [ "auth_socket.so" ];
455 })
456 (lib.mkIf cfg.galeraCluster.enable {
457 # Ensure Only InnoDB is used as galera clusters can only work with them
458 enforce_storage_engine = "InnoDB";
459 default_storage_engine = "InnoDB";
460
461 # galera only support this binlog format
462 binlog-format = "ROW";
463
464 bind_address = lib.mkDefault "0.0.0.0";
465 })
466 ];
467
468 services.mysql.settings.galera = lib.optionalAttrs cfg.galeraCluster.enable {
469 wsrep_on = "ON";
470 wsrep_debug = lib.mkDefault "NONE";
471 wsrep_retry_autocommit = lib.mkDefault "3";
472 wsrep_provider = "${cfg.galeraCluster.package}/lib/galera/libgalera_smm.so";
473
474 wsrep_cluster_name = cfg.galeraCluster.name;
475 wsrep_cluster_address =
476 if (cfg.galeraCluster.clusterAddress != "") then
477 cfg.galeraCluster.clusterAddress
478 else
479 generateClusterAddress;
480
481 wsrep_node_address = cfg.galeraCluster.localAddress;
482 wsrep_node_name = "${cfg.galeraCluster.localName}";
483
484 # SST method using rsync
485 wsrep_sst_method = lib.mkDefault cfg.galeraCluster.sstMethod;
486 wsrep_sst_auth = lib.mkDefault "check_repl:check_pass";
487
488 binlog_format = "ROW";
489 innodb_autoinc_lock_mode = 2;
490 };
491
492 users.users = lib.optionalAttrs (cfg.user == "mysql") {
493 mysql = {
494 description = "MySQL server user";
495 group = cfg.group;
496 uid = config.ids.uids.mysql;
497 };
498 };
499
500 users.groups = lib.optionalAttrs (cfg.group == "mysql") {
501 mysql.gid = config.ids.gids.mysql;
502 };
503
504 environment.systemPackages = [ cfg.package ];
505
506 environment.etc."my.cnf".source = cfg.configFile;
507
508 # The mysql_install_db binary will try to adjust the permissions, but fail to do so with a permission
509 # denied error in some circumstances. Setting the permissions manually with tmpfiles is a workaround.
510 systemd.tmpfiles.rules = [
511 "d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} - -"
512 ];
513
514 systemd.services.mysql = {
515 description = "MySQL Server";
516
517 after = [ "network.target" ];
518 wantedBy = [ "multi-user.target" ];
519 restartTriggers = [ cfg.configFile ];
520
521 unitConfig.RequiresMountsFor = cfg.dataDir;
522
523 path = [
524 # Needed for the mysql_install_db command in the preStart script
525 # which calls the hostname command.
526 pkgs.hostname-debian
527 ]
528 # tools 'wsrep_sst_rsync' needs
529 ++ lib.optionals cfg.galeraCluster.enable [
530 cfg.package
531 pkgs.bash
532 pkgs.gawk
533 pkgs.gnutar
534 pkgs.gzip
535 pkgs.inetutils
536 pkgs.iproute2
537 pkgs.netcat
538 pkgs.procps
539 pkgs.pv
540 pkgs.rsync
541 pkgs.socat
542 pkgs.stunnel
543 pkgs.which
544 ];
545
546 preStart =
547 if isMariaDB then
548 ''
549 if ! test -e ${cfg.dataDir}/mysql; then
550 ${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
551 touch ${cfg.dataDir}/mysql_init
552 fi
553 ''
554 else
555 ''
556 if ! test -e ${cfg.dataDir}/mysql; then
557 ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
558 touch ${cfg.dataDir}/mysql_init
559 fi
560 '';
561
562 script = ''
563 # https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
564 if test -n "''${_WSREP_START_POSITION}"; then
565 if test -e "${cfg.package}/bin/galera_recovery"; then
566 VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
567 fi
568 fi
569
570 # The last two environment variables are used for starting Galera clusters
571 exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
572 '';
573
574 postStart =
575 let
576 # The super user account to use on *first* run of MySQL server
577 superUser = if isMariaDB then cfg.user else "root";
578 in
579 ''
580 ${lib.optionalString (!hasNotify) ''
581 # Wait until the MySQL server is available for use
582 while [ ! -e /run/mysqld/mysqld.sock ]
583 do
584 echo "MySQL daemon not yet started. Waiting for 1 second..."
585 sleep 1
586 done
587 ''}
588
589 ${lib.optionalString isMariaDB ''
590 # If MariaDB is used in an Galera cluster, we have to check if the sync is done,
591 # or it will fail to init the database while joining, so we get in an broken non recoverable state
592 # so we wait until we have an synced state
593 if ${cfg.package}/bin/mysql -u ${superUser} -N -e "SHOW VARIABLES LIKE 'wsrep_on'" 2>/dev/null | ${lib.getExe' pkgs.gnugrep "grep"} -q 'ON'; then
594 echo "Galera cluster detected, waiting for node to be synced..."
595 while true; do
596 STATE=$(${cfg.package}/bin/mysql -u ${superUser} -N -e "SHOW STATUS LIKE 'wsrep_local_state_comment'" | ${lib.getExe' pkgs.gawk "awk"} '{print $2}')
597 if [ "$STATE" = "Synced" ]; then
598 echo "Node is synced"
599 break
600 else
601 echo "Current state: $STATE - Waiting for 1 second..."
602 sleep 1
603 fi
604 done
605 fi
606 ''}
607
608 if [ -f ${cfg.dataDir}/mysql_init ]
609 then
610 # While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not
611 # Since we don't want to run this service as 'root' we need to ensure the account exists on first run
612 ( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${
613 if isMariaDB then "unix_socket" else "auth_socket"
614 };"
615 echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;"
616 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
617
618 ${lib.concatMapStrings (database: ''
619 # Create initial databases
620 if ! test -e "${cfg.dataDir}/${database.name}"; then
621 echo "Creating initial database: ${database.name}"
622 ( echo 'CREATE DATABASE IF NOT EXISTS `${database.name}`;'
623
624 ${lib.optionalString (database.schema != null) ''
625 echo 'USE `${database.name}`;'
626
627 # TODO: this silently falls through if database.schema does not exist,
628 # we should catch this somehow and exit, but can't do it here because we're in a subshell.
629 if [ -f "${database.schema}" ]
630 then
631 cat ${database.schema}
632 elif [ -d "${database.schema}" ]
633 then
634 cat ${database.schema}/mysql-databases/*.sql
635 fi
636 ''}
637 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
638 fi
639 '') cfg.initialDatabases}
640
641 ${lib.optionalString (cfg.replication.role == "master") ''
642 # Set up the replication master
643
644 ( echo "USE mysql;"
645 echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
646 echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
647 echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
648 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
649 ''}
650
651 ${lib.optionalString (cfg.replication.role == "slave") ''
652 # Set up the replication slave
653
654 ( echo "STOP SLAVE;"
655 echo "CHANGE MASTER TO MASTER_HOST='${cfg.replication.masterHost}', MASTER_USER='${cfg.replication.masterUser}', MASTER_PASSWORD='${cfg.replication.masterPassword}';"
656 echo "START SLAVE;"
657 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
658 ''}
659
660 ${lib.optionalString (cfg.initialScript != null) ''
661 # Execute initial script
662 # using toString to avoid copying the file to nix store if given as path instead of string,
663 # as it might contain credentials
664 cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
665 ''}
666
667 rm ${cfg.dataDir}/mysql_init
668 fi
669
670 ${lib.optionalString (cfg.ensureDatabases != [ ]) ''
671 (
672 ${lib.concatMapStrings (database: ''
673 echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
674 '') cfg.ensureDatabases}
675 ) | ${cfg.package}/bin/mysql -N
676 ''}
677
678 ${lib.concatMapStrings (user: ''
679 ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${
680 if isMariaDB then "unix_socket" else "auth_socket"
681 };"
682 ${lib.concatStringsSep "\n" (
683 lib.mapAttrsToList (database: permission: ''
684 echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
685 '') user.ensurePermissions
686 )}
687 ) | ${cfg.package}/bin/mysql -N
688 '') cfg.ensureUsers}
689 '';
690
691 serviceConfig = lib.mkMerge [
692 {
693 Type = if hasNotify then "notify" else "simple";
694 Restart = "on-abnormal";
695 RestartSec = "5s";
696
697 # User and group
698 User = cfg.user;
699 Group = cfg.group;
700 # Runtime directory and mode
701 RuntimeDirectory = "mysqld";
702 RuntimeDirectoryMode = "0755";
703 # Access write directories
704 ReadWritePaths = [ cfg.dataDir ];
705 # Capabilities
706 CapabilityBoundingSet = "";
707 # Security
708 NoNewPrivileges = true;
709 # Sandboxing
710 ProtectSystem = "strict";
711 ProtectHome = true;
712 PrivateTmp = true;
713 PrivateDevices = true;
714 ProtectHostname = true;
715 ProtectKernelTunables = true;
716 ProtectKernelModules = true;
717 ProtectControlGroups = true;
718 RestrictAddressFamilies = [
719 "AF_UNIX"
720 "AF_INET"
721 "AF_INET6"
722 ];
723 LockPersonality = true;
724 MemoryDenyWriteExecute = true;
725 RestrictRealtime = true;
726 RestrictSUIDSGID = true;
727 PrivateMounts = true;
728 # System Call Filtering
729 SystemCallArchitectures = "native";
730 }
731 (lib.mkIf (cfg.dataDir == "/var/lib/mysql") {
732 StateDirectory = "mysql";
733 StateDirectoryMode = "0700";
734 })
735 ];
736 };
737
738 # Open firewall ports for MySQL (and Galera)
739 networking.firewall.allowedTCPPorts = lib.optionals cfg.galeraCluster.enable [
740 3306 # MySQL
741 4567 # Galera Cluster
742 4568 # Galera IST
743 4444 # SST
744 ];
745 networking.firewall.allowedUDPPorts = lib.optionals cfg.galeraCluster.enable [
746 4567 # Galera Cluster
747 ];
748 };
749
750 meta.maintainers = [ lib.maintainers._6543 ];
751}