at 21.11-pre 31 kB view raw
1{ config, pkgs, lib, ... }: 2 3let 4 cfg = config.services.keycloak; 5in 6{ 7 options.services.keycloak = { 8 9 enable = lib.mkOption { 10 type = lib.types.bool; 11 default = false; 12 example = true; 13 description = '' 14 Whether to enable the Keycloak identity and access management 15 server. 16 ''; 17 }; 18 19 bindAddress = lib.mkOption { 20 type = lib.types.str; 21 default = "\${jboss.bind.address:0.0.0.0}"; 22 example = "127.0.0.1"; 23 description = '' 24 On which address Keycloak should accept new connections. 25 26 A special syntax can be used to allow command line Java system 27 properties to override the value: ''${property.name:value} 28 ''; 29 }; 30 31 httpPort = lib.mkOption { 32 type = lib.types.str; 33 default = "\${jboss.http.port:80}"; 34 example = "8080"; 35 description = '' 36 On which port Keycloak should listen for new HTTP connections. 37 38 A special syntax can be used to allow command line Java system 39 properties to override the value: ''${property.name:value} 40 ''; 41 }; 42 43 httpsPort = lib.mkOption { 44 type = lib.types.str; 45 default = "\${jboss.https.port:443}"; 46 example = "8443"; 47 description = '' 48 On which port Keycloak should listen for new HTTPS connections. 49 50 A special syntax can be used to allow command line Java system 51 properties to override the value: ''${property.name:value} 52 ''; 53 }; 54 55 frontendUrl = lib.mkOption { 56 type = lib.types.str; 57 apply = x: if lib.hasSuffix "/" x then x else x + "/"; 58 example = "keycloak.example.com/auth"; 59 description = '' 60 The public URL used as base for all frontend requests. Should 61 normally include a trailing <literal>/auth</literal>. 62 63 See <link xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the 64 Hostname section of the Keycloak server installation 65 manual</link> for more information. 66 ''; 67 }; 68 69 forceBackendUrlToFrontendUrl = lib.mkOption { 70 type = lib.types.bool; 71 default = false; 72 example = true; 73 description = '' 74 Whether Keycloak should force all requests to go through the 75 frontend URL configured in <xref 76 linkend="opt-services.keycloak.frontendUrl" />. By default, 77 Keycloak allows backend requests to instead use its local 78 hostname or IP address and may also advertise it to clients 79 through its OpenID Connect Discovery endpoint. 80 81 See <link 82 xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the 83 Hostname section of the Keycloak server installation 84 manual</link> for more information. 85 ''; 86 }; 87 88 sslCertificate = lib.mkOption { 89 type = lib.types.nullOr lib.types.path; 90 default = null; 91 example = "/run/keys/ssl_cert"; 92 description = '' 93 The path to a PEM formatted certificate to use for TLS/SSL 94 connections. 95 96 This should be a string, not a Nix path, since Nix paths are 97 copied into the world-readable Nix store. 98 ''; 99 }; 100 101 sslCertificateKey = lib.mkOption { 102 type = lib.types.nullOr lib.types.path; 103 default = null; 104 example = "/run/keys/ssl_key"; 105 description = '' 106 The path to a PEM formatted private key to use for TLS/SSL 107 connections. 108 109 This should be a string, not a Nix path, since Nix paths are 110 copied into the world-readable Nix store. 111 ''; 112 }; 113 114 database = { 115 type = lib.mkOption { 116 type = lib.types.enum [ "mysql" "postgresql" ]; 117 default = "postgresql"; 118 example = "mysql"; 119 description = '' 120 The type of database Keycloak should connect to. 121 ''; 122 }; 123 124 host = lib.mkOption { 125 type = lib.types.str; 126 default = "localhost"; 127 description = '' 128 Hostname of the database to connect to. 129 ''; 130 }; 131 132 port = 133 let 134 dbPorts = { 135 postgresql = 5432; 136 mysql = 3306; 137 }; 138 in 139 lib.mkOption { 140 type = lib.types.port; 141 default = dbPorts.${cfg.database.type}; 142 description = '' 143 Port of the database to connect to. 144 ''; 145 }; 146 147 useSSL = lib.mkOption { 148 type = lib.types.bool; 149 default = cfg.database.host != "localhost"; 150 description = '' 151 Whether the database connection should be secured by SSL / 152 TLS. 153 ''; 154 }; 155 156 caCert = lib.mkOption { 157 type = lib.types.nullOr lib.types.path; 158 default = null; 159 description = '' 160 The SSL / TLS CA certificate that verifies the identity of the 161 database server. 162 163 Required when PostgreSQL is used and SSL is turned on. 164 165 For MySQL, if left at <literal>null</literal>, the default 166 Java keystore is used, which should suffice if the server 167 certificate is issued by an official CA. 168 ''; 169 }; 170 171 createLocally = lib.mkOption { 172 type = lib.types.bool; 173 default = true; 174 description = '' 175 Whether a database should be automatically created on the 176 local host. Set this to false if you plan on provisioning a 177 local database yourself. This has no effect if 178 services.keycloak.database.host is customized. 179 ''; 180 }; 181 182 username = lib.mkOption { 183 type = lib.types.str; 184 default = "keycloak"; 185 description = '' 186 Username to use when connecting to an external or manually 187 provisioned database; has no effect when a local database is 188 automatically provisioned. 189 190 To use this with a local database, set <xref 191 linkend="opt-services.keycloak.database.createLocally" /> to 192 <literal>false</literal> and create the database and user 193 manually. The database should be called 194 <literal>keycloak</literal>. 195 ''; 196 }; 197 198 passwordFile = lib.mkOption { 199 type = lib.types.path; 200 example = "/run/keys/db_password"; 201 description = '' 202 File containing the database password. 203 204 This should be a string, not a Nix path, since Nix paths are 205 copied into the world-readable Nix store. 206 ''; 207 }; 208 }; 209 210 package = lib.mkOption { 211 type = lib.types.package; 212 default = pkgs.keycloak; 213 description = '' 214 Keycloak package to use. 215 ''; 216 }; 217 218 initialAdminPassword = lib.mkOption { 219 type = lib.types.str; 220 default = "changeme"; 221 description = '' 222 Initial password set for the <literal>admin</literal> 223 user. The password is not stored safely and should be changed 224 immediately in the admin panel. 225 ''; 226 }; 227 228 extraConfig = lib.mkOption { 229 type = lib.types.attrs; 230 default = { }; 231 example = lib.literalExample '' 232 { 233 "subsystem=keycloak-server" = { 234 "spi=hostname" = { 235 "provider=default" = null; 236 "provider=fixed" = { 237 enabled = true; 238 properties.hostname = "keycloak.example.com"; 239 }; 240 default-provider = "fixed"; 241 }; 242 }; 243 } 244 ''; 245 description = '' 246 Additional Keycloak configuration options to set in 247 <literal>standalone.xml</literal>. 248 249 Options are expressed as a Nix attribute set which matches the 250 structure of the jboss-cli configuration. The configuration is 251 effectively overlayed on top of the default configuration 252 shipped with Keycloak. To remove existing nodes and undefine 253 attributes from the default configuration, set them to 254 <literal>null</literal>. 255 256 The example configuration does the equivalent of the following 257 script, which removes the hostname provider 258 <literal>default</literal>, adds the deprecated hostname 259 provider <literal>fixed</literal> and defines it the default: 260 261 <programlisting> 262 /subsystem=keycloak-server/spi=hostname/provider=default:remove() 263 /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" }) 264 /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed") 265 </programlisting> 266 267 You can discover available options by using the <link 268 xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link> 269 program and by referring to the <link 270 xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak 271 Server Installation and Configuration Guide</link>. 272 ''; 273 }; 274 275 }; 276 277 config = 278 let 279 # We only want to create a database if we're actually going to connect to it. 280 databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost"; 281 createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql"; 282 createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql"; 283 284 mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} '' 285 ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt 286 ''; 287 288 keycloakConfig' = builtins.foldl' lib.recursiveUpdate { 289 "interface=public".inet-address = cfg.bindAddress; 290 "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort; 291 "subsystem=keycloak-server"."spi=hostname" = { 292 "provider=default" = { 293 enabled = true; 294 properties = { 295 inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl; 296 }; 297 }; 298 }; 299 "subsystem=datasources"."data-source=KeycloakDS" = { 300 max-pool-size = "20"; 301 user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username; 302 password = "@db-password@"; 303 }; 304 } [ 305 (lib.optionalAttrs (cfg.database.type == "postgresql") { 306 "subsystem=datasources" = { 307 "jdbc-driver=postgresql" = { 308 driver-module-name = "org.postgresql"; 309 driver-name = "postgresql"; 310 driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; 311 }; 312 "data-source=KeycloakDS" = { 313 connection-url = "jdbc:postgresql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak"; 314 driver-name = "postgresql"; 315 "connection-properties=ssl".value = lib.boolToString cfg.database.useSSL; 316 } // (lib.optionalAttrs (cfg.database.caCert != null) { 317 "connection-properties=sslrootcert".value = cfg.database.caCert; 318 "connection-properties=sslmode".value = "verify-ca"; 319 }); 320 }; 321 }) 322 (lib.optionalAttrs (cfg.database.type == "mysql") { 323 "subsystem=datasources" = { 324 "jdbc-driver=mysql" = { 325 driver-module-name = "com.mysql"; 326 driver-name = "mysql"; 327 driver-class-name = "com.mysql.jdbc.Driver"; 328 }; 329 "data-source=KeycloakDS" = { 330 connection-url = "jdbc:mysql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak"; 331 driver-name = "mysql"; 332 "connection-properties=useSSL".value = lib.boolToString cfg.database.useSSL; 333 "connection-properties=requireSSL".value = lib.boolToString cfg.database.useSSL; 334 "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.database.useSSL; 335 "connection-properties=characterEncoding".value = "UTF-8"; 336 valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"; 337 validate-on-match = true; 338 exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter"; 339 } // (lib.optionalAttrs (cfg.database.caCert != null) { 340 "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}"; 341 "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword"; 342 }); 343 }; 344 }) 345 (lib.optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) { 346 "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort; 347 "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = { 348 keystore-path = "/run/keycloak/ssl/certificate_private_key_bundle.p12"; 349 keystore-password = "notsosecretpassword"; 350 }; 351 "subsystem=undertow"."server=default-server"."https-listener=https".security-realm = "UndertowRealm"; 352 }) 353 cfg.extraConfig 354 ]; 355 356 357 /* Produces a JBoss CLI script that creates paths and sets 358 attributes matching those described by `attrs`. When the 359 script is run, the existing settings are effectively overlayed 360 by those from `attrs`. Existing attributes can be unset by 361 defining them `null`. 362 363 JBoss paths and attributes / maps are distinguished by their 364 name, where paths follow a `key=value` scheme. 365 366 Example: 367 mkJbossScript { 368 "subsystem=keycloak-server"."spi=hostname" = { 369 "provider=fixed" = null; 370 "provider=default" = { 371 enabled = true; 372 properties = { 373 inherit frontendUrl; 374 forceBackendUrlToFrontendUrl = false; 375 }; 376 }; 377 }; 378 } 379 => '' 380 if (outcome != success) of /:read-resource() 381 /:add() 382 end-if 383 if (outcome != success) of /subsystem=keycloak-server:read-resource() 384 /subsystem=keycloak-server:add() 385 end-if 386 if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource() 387 /subsystem=keycloak-server/spi=hostname:add() 388 end-if 389 if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource() 390 /subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }) 391 end-if 392 if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled") 393 /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true) 394 end-if 395 if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl") 396 /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false) 397 end-if 398 if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl") 399 /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth") 400 end-if 401 if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource() 402 /subsystem=keycloak-server/spi=hostname/provider=fixed:remove() 403 end-if 404 '' 405 */ 406 mkJbossScript = attrs: 407 let 408 /* From a JBoss path and an attrset, produces a JBoss CLI 409 snippet that writes the corresponding attributes starting 410 at `path`. Recurses down into subattrsets as necessary, 411 producing the variable name from its full path in the 412 attrset. 413 414 Example: 415 writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" { 416 enabled = true; 417 properties = { 418 forceBackendUrlToFrontendUrl = false; 419 frontendUrl = "https://keycloak.example.com/auth"; 420 }; 421 } 422 => '' 423 if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled") 424 /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true) 425 end-if 426 if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl") 427 /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false) 428 end-if 429 if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl") 430 /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth") 431 end-if 432 '' 433 */ 434 writeAttributes = path: set: 435 let 436 # JBoss expressions like `${var}` need to be prefixed 437 # with `expression` to evaluate. 438 prefixExpression = string: 439 let 440 match = (builtins.match ''"\$\{.*}"'' string); 441 in 442 if match != null then 443 "expression " + string 444 else 445 string; 446 447 writeAttribute = attribute: value: 448 let 449 type = builtins.typeOf value; 450 in 451 if type == "set" then 452 let 453 names = builtins.attrNames value; 454 in 455 builtins.foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names 456 else if value == null then '' 457 if (outcome == success) of ${path}:read-attribute(name="${attribute}") 458 ${path}:undefine-attribute(name="${attribute}") 459 end-if 460 '' 461 else if builtins.elem type [ "string" "path" "bool" ] then 462 let 463 value' = if type == "bool" then lib.boolToString value else ''"${value}"''; 464 in '' 465 if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}") 466 ${path}:write-attribute(name=${attribute}, value=${value'}) 467 end-if 468 '' 469 else throw "Unsupported type '${type}' for path '${path}'!"; 470 in 471 lib.concatStrings 472 (lib.mapAttrsToList 473 (attribute: value: (writeAttribute attribute value)) 474 set); 475 476 477 /* Produces an argument list for the JBoss `add()` function, 478 which adds a JBoss path and takes as its arguments the 479 required subpaths and attributes. 480 481 Example: 482 makeArgList { 483 enabled = true; 484 properties = { 485 forceBackendUrlToFrontendUrl = false; 486 frontendUrl = "https://keycloak.example.com/auth"; 487 }; 488 } 489 => '' 490 enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" } 491 '' 492 */ 493 makeArgList = set: 494 let 495 makeArg = attribute: value: 496 let 497 type = builtins.typeOf value; 498 in 499 if type == "set" then 500 "${attribute} = { " + (makeArgList value) + " }" 501 else if builtins.elem type [ "string" "path" "bool" ] then 502 "${attribute} = ${if type == "bool" then lib.boolToString value else ''"${value}"''}" 503 else if value == null then 504 "" 505 else 506 throw "Unsupported type '${type}' for attribute '${attribute}'!"; 507 in 508 lib.concatStringsSep ", " (lib.mapAttrsToList makeArg set); 509 510 511 /* Recurses into the `attrs` attrset, beginning at the path 512 resolved from `state.path ++ node`; if `node` is `null`, 513 starts from `state.path`. Only subattrsets that are JBoss 514 paths, i.e. follows the `key=value` format, are recursed 515 into - the rest are considered JBoss attributes / maps. 516 */ 517 recurse = state: node: 518 let 519 path = state.path ++ (lib.optional (node != null) node); 520 isPath = name: 521 let 522 value = lib.getAttrFromPath (path ++ [ name ]) attrs; 523 in 524 if (builtins.match ".*([=]).*" name) == [ "=" ] then 525 if builtins.isAttrs value || value == null then 526 true 527 else 528 throw "Parsing path '${lib.concatStringsSep "." (path ++ [ name ])}' failed: JBoss attributes cannot contain '='!" 529 else 530 false; 531 jbossPath = "/" + (lib.concatStringsSep "/" path); 532 nodeValue = lib.getAttrFromPath path attrs; 533 children = if !builtins.isAttrs nodeValue then {} else nodeValue; 534 subPaths = builtins.filter isPath (builtins.attrNames children); 535 jbossAttrs = lib.filterAttrs (name: _: !(isPath name)) children; 536 in 537 state // { 538 text = state.text + ( 539 if nodeValue != null then '' 540 if (outcome != success) of ${jbossPath}:read-resource() 541 ${jbossPath}:add(${makeArgList jbossAttrs}) 542 end-if 543 '' + (writeAttributes jbossPath jbossAttrs) 544 else '' 545 if (outcome == success) of ${jbossPath}:read-resource() 546 ${jbossPath}:remove() 547 end-if 548 '') + (builtins.foldl' recurse { text = ""; inherit path; } subPaths).text; 549 }; 550 in 551 (recurse { text = ""; path = []; } null).text; 552 553 554 jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); 555 556 keycloakConfig = pkgs.runCommandNoCC "keycloak-config" { 557 nativeBuildInputs = [ cfg.package ]; 558 } '' 559 export JBOSS_BASE_DIR="$(pwd -P)"; 560 export JBOSS_MODULEPATH="${cfg.package}/modules"; 561 export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log"; 562 563 cp -r ${cfg.package}/standalone/configuration . 564 chmod -R u+rwX ./configuration 565 566 mkdir -p {deployments,ssl} 567 568 standalone.sh& 569 570 attempt=1 571 max_attempts=30 572 while ! jboss-cli.sh --connect ':read-attribute(name=server-state)'; do 573 if [[ "$attempt" == "$max_attempts" ]]; then 574 echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2 575 exit 1 576 fi 577 echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)" 578 sleep 1 579 (( attempt++ )) 580 done 581 582 jboss-cli.sh --connect --file=${jbossCliScript} --echo-command 583 584 cp configuration/standalone.xml $out 585 ''; 586 in 587 lib.mkIf cfg.enable { 588 589 assertions = [ 590 { 591 assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null); 592 message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL"; 593 } 594 ]; 595 596 environment.systemPackages = [ cfg.package ]; 597 598 systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL { 599 after = [ "postgresql.service" ]; 600 before = [ "keycloak.service" ]; 601 bindsTo = [ "postgresql.service" ]; 602 path = [ config.services.postgresql.package ]; 603 serviceConfig = { 604 Type = "oneshot"; 605 RemainAfterExit = true; 606 User = "postgres"; 607 Group = "postgres"; 608 }; 609 script = '' 610 set -o errexit -o pipefail -o nounset -o errtrace 611 shopt -s inherit_errexit 612 613 create_role="$(mktemp)" 614 trap 'rm -f "$create_role"' ERR EXIT 615 616 echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.database.passwordFile}')' CREATEDB" > "$create_role" 617 psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role" 618 psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' 619 ''; 620 }; 621 622 systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL { 623 after = [ "mysql.service" ]; 624 before = [ "keycloak.service" ]; 625 bindsTo = [ "mysql.service" ]; 626 path = [ config.services.mysql.package ]; 627 serviceConfig = { 628 Type = "oneshot"; 629 RemainAfterExit = true; 630 User = config.services.mysql.user; 631 Group = config.services.mysql.group; 632 }; 633 script = '' 634 set -o errexit -o pipefail -o nounset -o errtrace 635 shopt -s inherit_errexit 636 637 db_password="$(<'${cfg.database.passwordFile}')" 638 ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" 639 echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;" 640 echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';" 641 ) | mysql -N 642 ''; 643 }; 644 645 systemd.services.keycloak = 646 let 647 databaseServices = 648 if createLocalPostgreSQL then [ 649 "keycloakPostgreSQLInit.service" "postgresql.service" 650 ] 651 else if createLocalMySQL then [ 652 "keycloakMySQLInit.service" "mysql.service" 653 ] 654 else [ ]; 655 in { 656 after = databaseServices; 657 bindsTo = databaseServices; 658 wantedBy = [ "multi-user.target" ]; 659 path = with pkgs; [ 660 cfg.package 661 openssl 662 replace-secret 663 ]; 664 environment = { 665 JBOSS_LOG_DIR = "/var/log/keycloak"; 666 JBOSS_BASE_DIR = "/run/keycloak"; 667 JBOSS_MODULEPATH = "${cfg.package}/modules"; 668 }; 669 serviceConfig = { 670 ExecStartPre = let 671 startPreFullPrivileges = '' 672 set -o errexit -o pipefail -o nounset -o errtrace 673 shopt -s inherit_errexit 674 675 umask u=rwx,g=,o= 676 677 install -T -m 0400 -o keycloak -g keycloak '${cfg.database.passwordFile}' /run/keycloak/secrets/db_password 678 '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' 679 install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificate}' /run/keycloak/secrets/ssl_cert 680 install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificateKey}' /run/keycloak/secrets/ssl_key 681 ''; 682 startPre = '' 683 set -o errexit -o pipefail -o nounset -o errtrace 684 shopt -s inherit_errexit 685 686 umask u=rwx,g=,o= 687 688 install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration 689 install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml 690 691 replace-secret '@db-password@' '/run/keycloak/secrets/db_password' /run/keycloak/configuration/standalone.xml 692 693 export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration 694 add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}' 695 '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' 696 pushd /run/keycloak/ssl/ 697 cat /run/keycloak/secrets/ssl_cert <(echo) \ 698 /run/keycloak/secrets/ssl_key <(echo) \ 699 /etc/ssl/certs/ca-certificates.crt \ 700 > allcerts.pem 701 openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert -inkey /run/keycloak/secrets/ssl_key -chain \ 702 -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \ 703 -CAfile allcerts.pem -passout pass:notsosecretpassword 704 popd 705 ''; 706 in [ 707 "+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}" 708 "${pkgs.writeShellScript "keycloak-start-pre" startPre}" 709 ]; 710 ExecStart = "${cfg.package}/bin/standalone.sh"; 711 User = "keycloak"; 712 Group = "keycloak"; 713 DynamicUser = true; 714 RuntimeDirectory = map (p: "keycloak/" + p) [ 715 "secrets" 716 "configuration" 717 "deployments" 718 "data" 719 "ssl" 720 "log" 721 "tmp" 722 ]; 723 RuntimeDirectoryMode = 0700; 724 LogsDirectory = "keycloak"; 725 AmbientCapabilities = "CAP_NET_BIND_SERVICE"; 726 }; 727 }; 728 729 services.postgresql.enable = lib.mkDefault createLocalPostgreSQL; 730 services.mysql.enable = lib.mkDefault createLocalMySQL; 731 services.mysql.package = lib.mkIf createLocalMySQL pkgs.mysql; 732 }; 733 734 meta.doc = ./keycloak.xml; 735 meta.maintainers = [ lib.maintainers.talyz ]; 736}