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 {
412 assertion = !cfg.galeraCluster.enable || isMariaDB;
413 message = "'services.mysql.galeraCluster.enable' expect services.mysql.package to be an mariadb variant";
414 }
415 ]
416 # galeraCluster options checks
417 ++ lib.optionals cfg.galeraCluster.enable [
418 {
419 assertion =
420 cfg.galeraCluster.localAddress != ""
421 && (cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "");
422 message = "mariadb galera cluster is enabled but the localAddress and (nodeAddresses or clusterAddress) are not set";
423 }
424 {
425 assertion = cfg.galeraCluster.clusterPassword == "" || cfg.galeraCluster.clusterAddress == "";
426 message = "mariadb galera clusterPassword is set but overwritten by clusterAddress";
427 }
428 {
429 assertion = cfg.galeraCluster.nodeAddresses != [ ] || cfg.galeraCluster.clusterAddress != "";
430 message = "When services.mysql.galeraCluster.clusterAddress is set, setting services.mysql.galeraCluster.nodeAddresses is redundant and will be overwritten by clusterAddress. Choose one approach.";
431 }
432 ];
433
434 services.mysql.dataDir = lib.mkDefault (
435 if lib.versionAtLeast config.system.stateVersion "17.09" then "/var/lib/mysql" else "/var/mysql"
436 );
437
438 services.mysql.settings.mysqld = lib.mkMerge [
439 {
440 datadir = cfg.dataDir;
441 port = lib.mkDefault 3306;
442 }
443 (lib.mkIf (cfg.replication.role == "master" || cfg.replication.role == "slave") {
444 log-bin = "mysql-bin-${toString cfg.replication.serverId}";
445 log-bin-index = "mysql-bin-${toString cfg.replication.serverId}.index";
446 relay-log = "mysql-relay-bin";
447 server-id = cfg.replication.serverId;
448 binlog-ignore-db = [
449 "information_schema"
450 "performance_schema"
451 "mysql"
452 ];
453 })
454 (lib.mkIf (!isMariaDB) {
455 plugin-load-add = [ "auth_socket.so" ];
456 })
457 (lib.mkIf cfg.galeraCluster.enable {
458 # Ensure Only InnoDB is used as galera clusters can only work with them
459 enforce_storage_engine = "InnoDB";
460 default_storage_engine = "InnoDB";
461
462 # galera only support this binlog format
463 binlog-format = "ROW";
464
465 bind_address = lib.mkDefault "0.0.0.0";
466 })
467 ];
468
469 services.mysql.settings.galera = lib.optionalAttrs cfg.galeraCluster.enable {
470 wsrep_on = "ON";
471 wsrep_debug = lib.mkDefault "NONE";
472 wsrep_retry_autocommit = lib.mkDefault "3";
473 wsrep_provider = "${cfg.galeraCluster.package}/lib/galera/libgalera_smm.so";
474
475 wsrep_cluster_name = cfg.galeraCluster.name;
476 wsrep_cluster_address =
477 if (cfg.galeraCluster.clusterAddress != "") then
478 cfg.galeraCluster.clusterAddress
479 else
480 generateClusterAddress;
481
482 wsrep_node_address = cfg.galeraCluster.localAddress;
483 wsrep_node_name = "${cfg.galeraCluster.localName}";
484
485 # SST method using rsync
486 wsrep_sst_method = lib.mkDefault cfg.galeraCluster.sstMethod;
487 wsrep_sst_auth = lib.mkDefault "check_repl:check_pass";
488
489 binlog_format = "ROW";
490 innodb_autoinc_lock_mode = 2;
491 };
492
493 users.users = lib.optionalAttrs (cfg.user == "mysql") {
494 mysql = {
495 description = "MySQL server user";
496 group = cfg.group;
497 uid = config.ids.uids.mysql;
498 };
499 };
500
501 users.groups = lib.optionalAttrs (cfg.group == "mysql") {
502 mysql.gid = config.ids.gids.mysql;
503 };
504
505 environment.systemPackages = [ cfg.package ];
506
507 environment.etc."my.cnf".source = cfg.configFile;
508
509 # The mysql_install_db binary will try to adjust the permissions, but fail to do so with a permission
510 # denied error in some circumstances. Setting the permissions manually with tmpfiles is a workaround.
511 systemd.tmpfiles.rules = [
512 "d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} - -"
513 ];
514
515 systemd.services.mysql = {
516 description = "MySQL Server";
517
518 after = [ "network.target" ];
519 wantedBy = [ "multi-user.target" ];
520 restartTriggers = [ cfg.configFile ];
521
522 unitConfig.RequiresMountsFor = cfg.dataDir;
523
524 path =
525 [
526 # Needed for the mysql_install_db command in the preStart script
527 # which calls the hostname command.
528 pkgs.nettools
529 ]
530 # tools 'wsrep_sst_rsync' needs
531 ++ lib.optionals cfg.galeraCluster.enable [
532 cfg.package
533 pkgs.bash
534 pkgs.gawk
535 pkgs.gnutar
536 pkgs.gzip
537 pkgs.inetutils
538 pkgs.iproute2
539 pkgs.netcat
540 pkgs.procps
541 pkgs.pv
542 pkgs.rsync
543 pkgs.socat
544 pkgs.stunnel
545 pkgs.which
546 ];
547
548 preStart =
549 if isMariaDB then
550 ''
551 if ! test -e ${cfg.dataDir}/mysql; then
552 ${cfg.package}/bin/mysql_install_db --defaults-file=/etc/my.cnf ${mysqldOptions}
553 touch ${cfg.dataDir}/mysql_init
554 fi
555 ''
556 else
557 ''
558 if ! test -e ${cfg.dataDir}/mysql; then
559 ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} --initialize-insecure
560 touch ${cfg.dataDir}/mysql_init
561 fi
562 '';
563
564 script = ''
565 # https://mariadb.com/kb/en/getting-started-with-mariadb-galera-cluster/#systemd-and-galera-recovery
566 if test -n "''${_WSREP_START_POSITION}"; then
567 if test -e "${cfg.package}/bin/galera_recovery"; then
568 VAR=$(cd ${cfg.package}/bin/..; ${cfg.package}/bin/galera_recovery); [[ $? -eq 0 ]] && export _WSREP_START_POSITION=$VAR || exit 1
569 fi
570 fi
571
572 # The last two environment variables are used for starting Galera clusters
573 exec ${cfg.package}/bin/mysqld --defaults-file=/etc/my.cnf ${mysqldOptions} $_WSREP_NEW_CLUSTER $_WSREP_START_POSITION
574 '';
575
576 postStart =
577 let
578 # The super user account to use on *first* run of MySQL server
579 superUser = if isMariaDB then cfg.user else "root";
580 in
581 ''
582 ${lib.optionalString (!hasNotify) ''
583 # Wait until the MySQL server is available for use
584 while [ ! -e /run/mysqld/mysqld.sock ]
585 do
586 echo "MySQL daemon not yet started. Waiting for 1 second..."
587 sleep 1
588 done
589 ''}
590
591 ${lib.optionalString isMariaDB ''
592 # If MariaDB is used in an Galera cluster, we have to check if the sync is done,
593 # or it will fail to init the database while joining, so we get in an broken non recoverable state
594 # so we wait until we have an synced state
595 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
596 echo "Galera cluster detected, waiting for node to be synced..."
597 while true; do
598 STATE=$(${cfg.package}/bin/mysql -u ${superUser} -N -e "SHOW STATUS LIKE 'wsrep_local_state_comment'" | ${lib.getExe' pkgs.gawk "awk"} '{print $2}')
599 if [ "$STATE" = "Synced" ]; then
600 echo "Node is synced"
601 break
602 else
603 echo "Current state: $STATE - Waiting for 1 second..."
604 sleep 1
605 fi
606 done
607 fi
608 ''}
609
610 if [ -f ${cfg.dataDir}/mysql_init ]
611 then
612 # While MariaDB comes with a 'mysql' super user account since 10.4.x, MySQL does not
613 # Since we don't want to run this service as 'root' we need to ensure the account exists on first run
614 ( echo "CREATE USER IF NOT EXISTS '${cfg.user}'@'localhost' IDENTIFIED WITH ${
615 if isMariaDB then "unix_socket" else "auth_socket"
616 };"
617 echo "GRANT ALL PRIVILEGES ON *.* TO '${cfg.user}'@'localhost' WITH GRANT OPTION;"
618 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
619
620 ${lib.concatMapStrings (database: ''
621 # Create initial databases
622 if ! test -e "${cfg.dataDir}/${database.name}"; then
623 echo "Creating initial database: ${database.name}"
624 ( echo 'CREATE DATABASE IF NOT EXISTS `${database.name}`;'
625
626 ${lib.optionalString (database.schema != null) ''
627 echo 'USE `${database.name}`;'
628
629 # TODO: this silently falls through if database.schema does not exist,
630 # we should catch this somehow and exit, but can't do it here because we're in a subshell.
631 if [ -f "${database.schema}" ]
632 then
633 cat ${database.schema}
634 elif [ -d "${database.schema}" ]
635 then
636 cat ${database.schema}/mysql-databases/*.sql
637 fi
638 ''}
639 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
640 fi
641 '') cfg.initialDatabases}
642
643 ${lib.optionalString (cfg.replication.role == "master") ''
644 # Set up the replication master
645
646 ( echo "USE mysql;"
647 echo "CREATE USER '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' IDENTIFIED WITH mysql_native_password;"
648 echo "SET PASSWORD FOR '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}' = PASSWORD('${cfg.replication.masterPassword}');"
649 echo "GRANT REPLICATION SLAVE ON *.* TO '${cfg.replication.masterUser}'@'${cfg.replication.slaveHost}';"
650 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
651 ''}
652
653 ${lib.optionalString (cfg.replication.role == "slave") ''
654 # Set up the replication slave
655
656 ( echo "STOP SLAVE;"
657 echo "CHANGE MASTER TO MASTER_HOST='${cfg.replication.masterHost}', MASTER_USER='${cfg.replication.masterUser}', MASTER_PASSWORD='${cfg.replication.masterPassword}';"
658 echo "START SLAVE;"
659 ) | ${cfg.package}/bin/mysql -u ${superUser} -N
660 ''}
661
662 ${lib.optionalString (cfg.initialScript != null) ''
663 # Execute initial script
664 # using toString to avoid copying the file to nix store if given as path instead of string,
665 # as it might contain credentials
666 cat ${toString cfg.initialScript} | ${cfg.package}/bin/mysql -u ${superUser} -N
667 ''}
668
669 rm ${cfg.dataDir}/mysql_init
670 fi
671
672 ${lib.optionalString (cfg.ensureDatabases != [ ]) ''
673 (
674 ${lib.concatMapStrings (database: ''
675 echo "CREATE DATABASE IF NOT EXISTS \`${database}\`;"
676 '') cfg.ensureDatabases}
677 ) | ${cfg.package}/bin/mysql -N
678 ''}
679
680 ${lib.concatMapStrings (user: ''
681 ( echo "CREATE USER IF NOT EXISTS '${user.name}'@'localhost' IDENTIFIED WITH ${
682 if isMariaDB then "unix_socket" else "auth_socket"
683 };"
684 ${lib.concatStringsSep "\n" (
685 lib.mapAttrsToList (database: permission: ''
686 echo "GRANT ${permission} ON ${database} TO '${user.name}'@'localhost';"
687 '') user.ensurePermissions
688 )}
689 ) | ${cfg.package}/bin/mysql -N
690 '') cfg.ensureUsers}
691 '';
692
693 serviceConfig = lib.mkMerge [
694 {
695 Type = if hasNotify then "notify" else "simple";
696 Restart = "on-abort";
697 RestartSec = "5s";
698
699 # User and group
700 User = cfg.user;
701 Group = cfg.group;
702 # Runtime directory and mode
703 RuntimeDirectory = "mysqld";
704 RuntimeDirectoryMode = "0755";
705 # Access write directories
706 ReadWritePaths = [ cfg.dataDir ];
707 # Capabilities
708 CapabilityBoundingSet = "";
709 # Security
710 NoNewPrivileges = true;
711 # Sandboxing
712 ProtectSystem = "strict";
713 ProtectHome = true;
714 PrivateTmp = true;
715 PrivateDevices = true;
716 ProtectHostname = true;
717 ProtectKernelTunables = true;
718 ProtectKernelModules = true;
719 ProtectControlGroups = true;
720 RestrictAddressFamilies = [
721 "AF_UNIX"
722 "AF_INET"
723 "AF_INET6"
724 ];
725 LockPersonality = true;
726 MemoryDenyWriteExecute = true;
727 RestrictRealtime = true;
728 RestrictSUIDSGID = true;
729 PrivateMounts = true;
730 # System Call Filtering
731 SystemCallArchitectures = "native";
732 }
733 (lib.mkIf (cfg.dataDir == "/var/lib/mysql") {
734 StateDirectory = "mysql";
735 StateDirectoryMode = "0700";
736 })
737 ];
738 };
739
740 # Open firewall ports for MySQL (and Galera)
741 networking.firewall.allowedTCPPorts = lib.optionals cfg.galeraCluster.enable [
742 3306 # MySQL
743 4567 # Galera Cluster
744 4568 # Galera IST
745 4444 # SST
746 ];
747 networking.firewall.allowedUDPPorts = lib.optionals cfg.galeraCluster.enable [
748 4567 # Galera Cluster
749 ];
750 };
751
752 meta.maintainers = [ lib.maintainers._6543 ];
753}