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}