at master 28 kB view raw
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}