at 25.11-pre 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 { 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}