at 25.11-pre 26 kB view raw
1{ 2 config, 3 options, 4 pkgs, 5 lib, 6 ... 7}: 8 9let 10 cfg = config.services.keycloak; 11 opt = options.services.keycloak; 12 13 inherit (lib) 14 types 15 mkMerge 16 mkOption 17 mkChangedOptionModule 18 mkRenamedOptionModule 19 mkRemovedOptionModule 20 mkPackageOption 21 concatStringsSep 22 mapAttrsToList 23 escapeShellArg 24 mkIf 25 optionalString 26 optionals 27 mkDefault 28 literalExpression 29 isAttrs 30 literalMD 31 maintainers 32 catAttrs 33 collect 34 hasPrefix 35 ; 36 37 inherit (builtins) 38 elem 39 typeOf 40 isInt 41 isString 42 hashString 43 isPath 44 ; 45 46 prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}"; 47in 48{ 49 imports = [ 50 (mkRenamedOptionModule 51 [ "services" "keycloak" "bindAddress" ] 52 [ "services" "keycloak" "settings" "http-host" ] 53 ) 54 (mkRenamedOptionModule 55 [ "services" "keycloak" "forceBackendUrlToFrontendUrl" ] 56 [ "services" "keycloak" "settings" "hostname-strict-backchannel" ] 57 ) 58 (mkChangedOptionModule 59 [ "services" "keycloak" "httpPort" ] 60 [ "services" "keycloak" "settings" "http-port" ] 61 (config: builtins.fromJSON config.services.keycloak.httpPort) 62 ) 63 (mkChangedOptionModule 64 [ "services" "keycloak" "httpsPort" ] 65 [ "services" "keycloak" "settings" "https-port" ] 66 (config: builtins.fromJSON config.services.keycloak.httpsPort) 67 ) 68 (mkRemovedOptionModule [ "services" "keycloak" "frontendUrl" ] '' 69 Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead. 70 NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients. 71 See its description for more information. 72 '') 73 (mkRemovedOptionModule [ 74 "services" 75 "keycloak" 76 "extraConfig" 77 ] "Use `services.keycloak.settings' instead.") 78 ]; 79 80 options.services.keycloak = 81 let 82 inherit (types) 83 bool 84 str 85 int 86 nullOr 87 attrsOf 88 oneOf 89 path 90 enum 91 package 92 port 93 listOf 94 ; 95 96 assertStringPath = 97 optionName: value: 98 if isPath value then 99 throw '' 100 services.keycloak.${optionName}: 101 ${toString value} 102 is a Nix path, but should be a string, since Nix 103 paths are copied into the world-readable Nix store. 104 '' 105 else 106 value; 107 in 108 { 109 enable = mkOption { 110 type = bool; 111 default = false; 112 example = true; 113 description = '' 114 Whether to enable the Keycloak identity and access management 115 server. 116 ''; 117 }; 118 119 sslCertificate = mkOption { 120 type = nullOr path; 121 default = null; 122 example = "/run/keys/ssl_cert"; 123 apply = assertStringPath "sslCertificate"; 124 description = '' 125 The path to a PEM formatted certificate to use for TLS/SSL 126 connections. 127 ''; 128 }; 129 130 sslCertificateKey = mkOption { 131 type = nullOr path; 132 default = null; 133 example = "/run/keys/ssl_key"; 134 apply = assertStringPath "sslCertificateKey"; 135 description = '' 136 The path to a PEM formatted private key to use for TLS/SSL 137 connections. 138 ''; 139 }; 140 141 plugins = lib.mkOption { 142 type = lib.types.listOf lib.types.path; 143 default = [ ]; 144 description = '' 145 Keycloak plugin jar, ear files or derivations containing 146 them. Packaged plugins are available through 147 `pkgs.keycloak.plugins`. 148 ''; 149 }; 150 151 database = { 152 type = mkOption { 153 type = enum [ 154 "mysql" 155 "mariadb" 156 "postgresql" 157 ]; 158 default = "postgresql"; 159 example = "mariadb"; 160 description = '' 161 The type of database Keycloak should connect to. 162 ''; 163 }; 164 165 host = mkOption { 166 type = str; 167 default = "localhost"; 168 description = '' 169 Hostname of the database to connect to. 170 ''; 171 }; 172 173 port = 174 let 175 dbPorts = { 176 postgresql = 5432; 177 mariadb = 3306; 178 mysql = 3306; 179 }; 180 in 181 mkOption { 182 type = port; 183 default = dbPorts.${cfg.database.type}; 184 defaultText = literalMD "default port of selected database"; 185 description = '' 186 Port of the database to connect to. 187 ''; 188 }; 189 190 useSSL = mkOption { 191 type = bool; 192 default = cfg.database.host != "localhost"; 193 defaultText = literalExpression ''config.${opt.database.host} != "localhost"''; 194 description = '' 195 Whether the database connection should be secured by SSL / 196 TLS. 197 ''; 198 }; 199 200 caCert = mkOption { 201 type = nullOr path; 202 default = null; 203 description = '' 204 The SSL / TLS CA certificate that verifies the identity of the 205 database server. 206 207 Required when PostgreSQL is used and SSL is turned on. 208 209 For MySQL, if left at `null`, the default 210 Java keystore is used, which should suffice if the server 211 certificate is issued by an official CA. 212 ''; 213 }; 214 215 createLocally = mkOption { 216 type = bool; 217 default = true; 218 description = '' 219 Whether a database should be automatically created on the 220 local host. Set this to false if you plan on provisioning a 221 local database yourself. This has no effect if 222 services.keycloak.database.host is customized. 223 ''; 224 }; 225 226 name = mkOption { 227 type = str; 228 default = "keycloak"; 229 description = '' 230 Database name to use when connecting to an external or 231 manually provisioned database; has no effect when a local 232 database is automatically provisioned. 233 234 To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to 235 `false` and create the database and user 236 manually. 237 ''; 238 }; 239 240 username = mkOption { 241 type = str; 242 default = "keycloak"; 243 description = '' 244 Username to use when connecting to an external or manually 245 provisioned database; has no effect when a local database is 246 automatically provisioned. 247 248 To use this with a local database, set [](#opt-services.keycloak.database.createLocally) to 249 `false` and create the database and user 250 manually. 251 ''; 252 }; 253 254 passwordFile = mkOption { 255 type = path; 256 example = "/run/keys/db_password"; 257 apply = assertStringPath "passwordFile"; 258 description = '' 259 The path to a file containing the database password. 260 ''; 261 }; 262 }; 263 264 package = mkPackageOption pkgs "keycloak" { }; 265 266 initialAdminPassword = mkOption { 267 type = nullOr str; 268 default = null; 269 description = '' 270 Initial password set for the temporary `admin` user. 271 The password is not stored safely and should be changed 272 immediately in the admin panel. 273 274 See [Admin bootstrap and recovery](https://www.keycloak.org/server/bootstrap-admin-recovery) for details. 275 ''; 276 }; 277 278 themes = mkOption { 279 type = attrsOf package; 280 default = { }; 281 description = '' 282 Additional theme packages for Keycloak. Each theme is linked into 283 subdirectory with a corresponding attribute name. 284 285 Theme packages consist of several subdirectories which provide 286 different theme types: for example, `account`, 287 `login` etc. After adding a theme to this option you 288 can select it by its name in Keycloak administration console. 289 ''; 290 }; 291 292 realmFiles = mkOption { 293 type = listOf path; 294 example = lib.literalExpression '' 295 [ 296 ./some/realm.json 297 ./another/realm.json 298 ] 299 ''; 300 default = [ ]; 301 description = '' 302 Realm files that the server is going to import during startup. 303 If a realm already exists in the server, the import operation is 304 skipped. Importing the master realm is not supported. All files are 305 expected to be in `json` format. See the 306 [documentation](https://www.keycloak.org/server/importExport) for 307 further information. 308 ''; 309 }; 310 311 settings = mkOption { 312 type = lib.types.submodule { 313 freeformType = attrsOf ( 314 nullOr (oneOf [ 315 str 316 int 317 bool 318 (attrsOf path) 319 ]) 320 ); 321 322 options = { 323 http-host = mkOption { 324 type = str; 325 default = "::"; 326 example = "::1"; 327 description = '' 328 On which address Keycloak should accept new connections. 329 ''; 330 }; 331 332 http-port = mkOption { 333 type = port; 334 default = 80; 335 example = 8080; 336 description = '' 337 On which port Keycloak should listen for new HTTP connections. 338 ''; 339 }; 340 341 https-port = mkOption { 342 type = port; 343 default = 443; 344 example = 8443; 345 description = '' 346 On which port Keycloak should listen for new HTTPS connections. 347 ''; 348 }; 349 350 http-relative-path = mkOption { 351 type = str; 352 default = "/"; 353 example = "/auth"; 354 apply = x: if !(hasPrefix "/") x then "/" + x else x; 355 description = '' 356 The path relative to `/` for serving 357 resources. 358 359 ::: {.note} 360 In versions of Keycloak using Wildfly (&lt;17), 361 this defaulted to `/auth`. If 362 upgrading from the Wildfly version of Keycloak, 363 i.e. a NixOS version before 22.05, you'll likely 364 want to set this to `/auth` to 365 keep compatibility with your clients. 366 367 See <https://www.keycloak.org/migration/migrating-to-quarkus> 368 for more information on migrating from Wildfly to Quarkus. 369 ::: 370 ''; 371 }; 372 373 hostname = mkOption { 374 type = nullOr str; 375 example = "keycloak.example.com"; 376 description = '' 377 The hostname part of the public URL used as base for 378 all frontend requests. 379 380 See <https://www.keycloak.org/server/hostname> 381 for more information about hostname configuration. 382 ''; 383 }; 384 385 hostname-backchannel-dynamic = mkOption { 386 type = bool; 387 default = false; 388 example = true; 389 description = '' 390 Enables dynamic resolving of backchannel URLs, 391 including hostname, scheme, port and context path. 392 393 See <https://www.keycloak.org/server/hostname> 394 for more information about hostname configuration. 395 ''; 396 }; 397 }; 398 }; 399 400 example = literalExpression '' 401 { 402 hostname = "keycloak.example.com"; 403 https-key-store-file = "/path/to/file"; 404 https-key-store-password = { _secret = "/run/keys/store_password"; }; 405 } 406 ''; 407 408 description = '' 409 Configuration options corresponding to parameters set in 410 {file}`conf/keycloak.conf`. 411 412 Most available options are documented at <https://www.keycloak.org/server/all-config>. 413 414 Options containing secret data should be set to an attribute 415 set containing the attribute `_secret` - a 416 string pointing to a file containing the value the option 417 should be set to. See the example to get a better picture of 418 this: in the resulting 419 {file}`conf/keycloak.conf` file, the 420 `https-key-store-password` key will be set 421 to the contents of the 422 {file}`/run/keys/store_password` file. 423 ''; 424 }; 425 }; 426 427 config = 428 let 429 # We only want to create a database if we're actually going to 430 # connect to it. 431 databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost"; 432 createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql"; 433 createLocalMySQL = 434 databaseActuallyCreateLocally 435 && elem cfg.database.type [ 436 "mysql" 437 "mariadb" 438 ]; 439 440 mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } '' 441 ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt 442 ''; 443 444 # Both theme and theme type directories need to be actual 445 # directories in one hierarchy to pass Keycloak checks. 446 themesBundle = pkgs.runCommand "keycloak-themes" { } '' 447 linkTheme() { 448 theme="$1" 449 name="$2" 450 451 mkdir "$out/$name" 452 for typeDir in "$theme"/*; do 453 if [ -d "$typeDir" ]; then 454 type="$(basename "$typeDir")" 455 mkdir "$out/$name/$type" 456 for file in "$typeDir"/*; do 457 ln -sn "$file" "$out/$name/$type/$(basename "$file")" 458 done 459 fi 460 done 461 } 462 463 mkdir -p "$out" 464 for theme in ${keycloakBuild}/themes/*; do 465 if [ -d "$theme" ]; then 466 linkTheme "$theme" "$(basename "$theme")" 467 fi 468 done 469 470 ${concatStringsSep "\n" ( 471 mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes 472 )} 473 ''; 474 475 keycloakConfig = lib.generators.toKeyValue { 476 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { 477 mkValueString = 478 v: 479 if isInt v then 480 toString v 481 else if isString v then 482 v 483 else if true == v then 484 "true" 485 else if false == v then 486 "false" 487 else if isSecret v then 488 hashString "sha256" v._secret 489 else 490 throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty { }) v}"; 491 }; 492 }; 493 494 isSecret = v: isAttrs v && v ? _secret && isString v._secret; 495 filteredConfig = lib.converge (lib.filterAttrsRecursive ( 496 _: v: 497 !elem v [ 498 { } 499 null 500 ] 501 )) cfg.settings; 502 confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig); 503 keycloakBuild = cfg.package.override { 504 inherit confFile; 505 plugins = 506 cfg.package.enabledPlugins 507 ++ cfg.plugins 508 ++ (with cfg.package.plugins; [ 509 quarkus-systemd-notify 510 quarkus-systemd-notify-deployment 511 ]); 512 }; 513 in 514 mkIf cfg.enable { 515 assertions = [ 516 { 517 assertion = 518 (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null); 519 message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL"; 520 } 521 { 522 assertion = 523 createLocalPostgreSQL -> config.services.postgresql.settings.standard_conforming_strings or true; 524 message = "Setting up a local PostgreSQL db for Keycloak requires `standard_conforming_strings` turned on to work reliably"; 525 } 526 { 527 assertion = cfg.settings.hostname != null || !cfg.settings.hostname-strict or true; 528 message = "Setting the Keycloak hostname is required, see `services.keycloak.settings.hostname`"; 529 } 530 { 531 assertion = cfg.settings.hostname-url or null == null; 532 message = '' 533 The option `services.keycloak.settings.hostname-url' has been removed. 534 Set `services.keycloak.settings.hostname' instead. 535 See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details. 536 ''; 537 } 538 { 539 assertion = cfg.settings.hostname-strict-backchannel or null == null; 540 message = '' 541 The option `services.keycloak.settings.hostname-strict-backchannel' has been removed. 542 Set `services.keycloak.settings.hostname-backchannel-dynamic' instead. 543 See [New Hostname options](https://www.keycloak.org/docs/25.0.0/upgrading/#new-hostname-options) for details. 544 ''; 545 } 546 { 547 assertion = cfg.settings.proxy or null == null; 548 message = '' 549 The option `services.keycloak.settings.proxy' has been removed. 550 Set `services.keycloak.settings.proxy-headers` in combination 551 with other hostname options as needed instead. 552 See [Proxy option removed](https://www.keycloak.org/docs/latest/upgrading/index.html#proxy-option-removed) 553 for more information. 554 ''; 555 } 556 ]; 557 558 environment.systemPackages = [ keycloakBuild ]; 559 560 services.keycloak.settings = 561 let 562 postgresParams = concatStringsSep "&" ( 563 optionals cfg.database.useSSL [ 564 "ssl=true" 565 ] 566 ++ optionals (cfg.database.caCert != null) [ 567 "sslrootcert=${cfg.database.caCert}" 568 "sslmode=verify-ca" 569 ] 570 ); 571 mariadbParams = concatStringsSep "&" ( 572 [ 573 "characterEncoding=UTF-8" 574 ] 575 ++ optionals cfg.database.useSSL [ 576 "useSSL=true" 577 "requireSSL=true" 578 "verifyServerCertificate=true" 579 ] 580 ++ optionals (cfg.database.caCert != null) [ 581 "trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}" 582 "trustCertificateKeyStorePassword=notsosecretpassword" 583 ] 584 ); 585 dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams; 586 in 587 mkMerge [ 588 { 589 db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type; 590 db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username; 591 db-password._secret = cfg.database.passwordFile; 592 db-url-host = cfg.database.host; 593 db-url-port = toString cfg.database.port; 594 db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name; 595 db-url-properties = prefixUnlessEmpty "?" dbProps; 596 db-url = null; 597 } 598 (mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) { 599 https-certificate-file = "/run/keycloak/ssl/ssl_cert"; 600 https-certificate-key-file = "/run/keycloak/ssl/ssl_key"; 601 }) 602 ]; 603 604 systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL { 605 after = [ "postgresql.service" ]; 606 before = [ "keycloak.service" ]; 607 bindsTo = [ "postgresql.service" ]; 608 path = [ config.services.postgresql.package ]; 609 serviceConfig = { 610 Type = "oneshot"; 611 RemainAfterExit = true; 612 User = "postgres"; 613 Group = "postgres"; 614 LoadCredential = [ "db_password:${cfg.database.passwordFile}" ]; 615 }; 616 script = '' 617 set -o errexit -o pipefail -o nounset -o errtrace 618 shopt -s inherit_errexit 619 620 create_role="$(mktemp)" 621 trap 'rm -f "$create_role"' EXIT 622 623 # Read the password from the credentials directory and 624 # escape any single quotes by adding additional single 625 # quotes after them, following the rules laid out here: 626 # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS 627 db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")" 628 db_password="''${db_password//\'/\'\'}" 629 630 echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" > "$create_role" 631 psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role" 632 psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' 633 ''; 634 }; 635 636 systemd.services.keycloakMySQLInit = mkIf createLocalMySQL { 637 after = [ "mysql.service" ]; 638 before = [ "keycloak.service" ]; 639 bindsTo = [ "mysql.service" ]; 640 path = [ config.services.mysql.package ]; 641 serviceConfig = { 642 Type = "oneshot"; 643 RemainAfterExit = true; 644 User = config.services.mysql.user; 645 Group = config.services.mysql.group; 646 LoadCredential = [ "db_password:${cfg.database.passwordFile}" ]; 647 }; 648 script = '' 649 set -o errexit -o pipefail -o nounset -o errtrace 650 shopt -s inherit_errexit 651 652 # Read the password from the credentials directory and 653 # escape any single quotes by adding additional single 654 # quotes after them, following the rules laid out here: 655 # https://dev.mysql.com/doc/refman/8.0/en/string-literals.html 656 db_password="$(<"$CREDENTIALS_DIRECTORY/db_password")" 657 db_password="''${db_password//\'/\'\'}" 658 659 ( echo "SET sql_mode = 'NO_BACKSLASH_ESCAPES';" 660 echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" 661 echo "CREATE DATABASE IF NOT EXISTS keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;" 662 echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';" 663 ) | mysql -N 664 ''; 665 }; 666 667 systemd.tmpfiles.settings."10-keycloak" = 668 let 669 mkTarget = 670 file: 671 let 672 baseName = builtins.baseNameOf file; 673 name = if lib.hasSuffix ".json" baseName then baseName else "${baseName}.json"; 674 in 675 "/run/keycloak/data/import/${name}"; 676 settingsList = map (f: { 677 name = mkTarget f; 678 value = { 679 "L+".argument = "${f}"; 680 }; 681 }) cfg.realmFiles; 682 in 683 builtins.listToAttrs settingsList; 684 685 systemd.services.keycloak = 686 let 687 databaseServices = 688 if createLocalPostgreSQL then 689 [ 690 "keycloakPostgreSQLInit.service" 691 "postgresql.service" 692 ] 693 else if createLocalMySQL then 694 [ 695 "keycloakMySQLInit.service" 696 "mysql.service" 697 ] 698 else 699 [ ]; 700 secretPaths = catAttrs "_secret" (collect isSecret cfg.settings); 701 mkSecretReplacement = file: '' 702 replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf 703 ''; 704 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; 705 in 706 { 707 after = databaseServices; 708 bindsTo = databaseServices; 709 wantedBy = [ "multi-user.target" ]; 710 path = with pkgs; [ 711 keycloakBuild 712 openssl 713 replace-secret 714 ]; 715 environment = 716 { 717 KC_HOME_DIR = "/run/keycloak"; 718 KC_CONF_DIR = "/run/keycloak/conf"; 719 } 720 // lib.optionalAttrs (cfg.initialAdminPassword != null) { 721 KC_BOOTSTRAP_ADMIN_USERNAME = "admin"; 722 KC_BOOTSTRAP_ADMIN_PASSWORD = cfg.initialAdminPassword; 723 }; 724 serviceConfig = { 725 LoadCredential = 726 map (p: "${baseNameOf p}:${p}") secretPaths 727 ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [ 728 "ssl_cert:${cfg.sslCertificate}" 729 "ssl_key:${cfg.sslCertificateKey}" 730 ]; 731 User = "keycloak"; 732 Group = "keycloak"; 733 DynamicUser = true; 734 RuntimeDirectory = "keycloak"; 735 RuntimeDirectoryMode = "0700"; 736 AmbientCapabilities = "CAP_NET_BIND_SERVICE"; 737 Type = "notify"; # Requires quarkus-systemd-notify plugin 738 NotifyAccess = "all"; 739 }; 740 script = 741 '' 742 set -o errexit -o pipefail -o nounset -o errtrace 743 shopt -s inherit_errexit 744 745 umask u=rwx,g=,o= 746 747 ln -s ${themesBundle} /run/keycloak/themes 748 ln -s ${keycloakBuild}/providers /run/keycloak/ 749 ln -s ${keycloakBuild}/lib /run/keycloak/ 750 751 install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf 752 753 ${secretReplacements} 754 755 # Escape any backslashes in the db parameters, since 756 # they're otherwise unexpectedly read as escape 757 # sequences. 758 sed -i '/db-/ s|\\|\\\\|g' /run/keycloak/conf/keycloak.conf 759 760 '' 761 + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' 762 mkdir -p /run/keycloak/ssl 763 cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/ 764 '' 765 + '' 766 kc.sh --verbose start --optimized ${lib.optionalString (cfg.realmFiles != [ ]) "--import-realm"} 767 ''; 768 }; 769 770 services.postgresql.enable = mkDefault createLocalPostgreSQL; 771 services.mysql.enable = mkDefault createLocalMySQL; 772 services.mysql.package = 773 let 774 dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80; 775 in 776 mkIf createLocalMySQL (mkDefault dbPkg); 777 }; 778 779 meta.doc = ./keycloak.md; 780 meta.maintainers = [ maintainers.talyz ]; 781}