From d6727d28e1c11887c8dad6860c0c204a21054d57 Mon Sep 17 00:00:00 2001 From: talyz Date: Tue, 4 May 2021 16:57:11 +0200 Subject: [PATCH 1/9] nixos/keycloak: Set the postgresql database password securely Feeding `psql` the password on the command line leaks it through the `psql` process' `/proc//cmdline` file. Using `echo` to put the command in a file and then feeding `psql` the file should work around this, since `echo` is a bash builtin and thus shouldn't spawn a new process. --- nixos/modules/services/web-apps/keycloak.nix | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index e2e6df41dfaa..073f793b4eda 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -592,8 +592,11 @@ in PSQL=${config.services.postgresql.package}/bin/psql - db_password="$(<'${cfg.databasePasswordFile}')" - $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || $PSQL -tAc "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" + create_role="$(mktemp)" + trap 'rm -f "$create_role"' ERR EXIT + + echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.databasePasswordFile}')' CREATEDB" > "$create_role" + $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || $PSQL -tA --file="$create_role" $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' ''; }; From c2bebf4ee2e4fd9ed96769c420b1ab2296feb717 Mon Sep 17 00:00:00 2001 From: talyz Date: Tue, 4 May 2021 17:03:49 +0200 Subject: [PATCH 2/9] nixos/keycloak: Improve bash error handling --- nixos/modules/services/web-apps/keycloak.nix | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 073f793b4eda..4d4c5f871a6f 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -588,7 +588,8 @@ in Group = "postgres"; }; script = '' - set -eu + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit PSQL=${config.services.postgresql.package}/bin/psql @@ -612,7 +613,8 @@ in Group = config.services.mysql.group; }; script = '' - set -eu + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit db_password="$(<'${cfg.databasePasswordFile}')" ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" @@ -647,14 +649,16 @@ in serviceConfig = { ExecStartPre = let startPreFullPrivileges = '' - set -eu + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle ''; startPre = '' - set -eu + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml From 8309368e4c29d0ffa954dde669db7f1a17fcd374 Mon Sep 17 00:00:00 2001 From: talyz Date: Tue, 4 May 2021 17:12:45 +0200 Subject: [PATCH 3/9] nixos/keycloak: Set umask before copying sensitive files `install` copies the files before setting their mode, so there could be a breif window where the secrets are readable by other users without a strict umask. --- nixos/modules/services/web-apps/keycloak.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 4d4c5f871a6f..73ab8e4de971 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -652,6 +652,8 @@ in set -o errexit -o pipefail -o nounset -o errtrace shopt -s inherit_errexit + umask u=rwx,g=,o= + install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle @@ -660,6 +662,8 @@ in set -o errexit -o pipefail -o nounset -o errtrace shopt -s inherit_errexit + umask u=rwx,g=,o= + install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml From d748c86389d2ae1707070906e0ccc9dd3e59ba54 Mon Sep 17 00:00:00 2001 From: talyz Date: Tue, 4 May 2021 17:16:41 +0200 Subject: [PATCH 4/9] nixos/keycloak: Improve readablility by putting executables in PATH --- nixos/modules/services/web-apps/keycloak.nix | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 73ab8e4de971..33155c114513 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -537,7 +537,9 @@ in jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); - keycloakConfig = pkgs.runCommandNoCC "keycloak-config" {} '' + keycloakConfig = pkgs.runCommandNoCC "keycloak-config" { + nativeBuildInputs = [ cfg.package ]; + } '' export JBOSS_BASE_DIR="$(pwd -P)"; export JBOSS_MODULEPATH="${cfg.package}/modules"; export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log"; @@ -547,11 +549,11 @@ in mkdir -p {deployments,ssl} - "${cfg.package}/bin/standalone.sh"& + standalone.sh& attempt=1 max_attempts=30 - while ! ${cfg.package}/bin/jboss-cli.sh --connect ':read-attribute(name=server-state)'; do + while ! jboss-cli.sh --connect ':read-attribute(name=server-state)'; do if [[ "$attempt" == "$max_attempts" ]]; then echo "ERROR: Could not connect to Keycloak after $attempt attempts! Failing.." >&2 exit 1 @@ -561,7 +563,7 @@ in (( attempt++ )) done - ${cfg.package}/bin/jboss-cli.sh --connect --file=${jbossCliScript} --echo-command + jboss-cli.sh --connect --file=${jbossCliScript} --echo-command cp configuration/standalone.xml $out ''; @@ -581,6 +583,7 @@ in after = [ "postgresql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "postgresql.service" ]; + path = [ config.services.postgresql.package ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; @@ -591,14 +594,12 @@ in set -o errexit -o pipefail -o nounset -o errtrace shopt -s inherit_errexit - PSQL=${config.services.postgresql.package}/bin/psql - create_role="$(mktemp)" trap 'rm -f "$create_role"' ERR EXIT echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.databasePasswordFile}')' CREATEDB" > "$create_role" - $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || $PSQL -tA --file="$create_role" - $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' + psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role" + psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' ''; }; @@ -606,6 +607,7 @@ in after = [ "mysql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "mysql.service" ]; + path = [ config.services.mysql.package ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; @@ -620,7 +622,7 @@ in ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;" echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';" - ) | ${config.services.mysql.package}/bin/mysql -N + ) | mysql -N ''; }; @@ -639,6 +641,8 @@ in bindsTo = databaseServices; wantedBy = [ "multi-user.target" ]; path = with pkgs; [ + cfg.package + openssl replace-secret ]; environment = { @@ -670,13 +674,13 @@ in replace-secret '@db-password@' '/run/keycloak/secrets/db_password' /run/keycloak/configuration/standalone.xml export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration - ${cfg.package}/bin/add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}' + add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}' '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' pushd /run/keycloak/ssl/ cat /run/keycloak/secrets/ssl_cert_pk_bundle <(echo) /etc/ssl/certs/ca-certificates.crt > allcerts.pem - ${pkgs.openssl}/bin/openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -chain \ - -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \ - -CAfile allcerts.pem -passout pass:notsosecretpassword + openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -chain \ + -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \ + -CAfile allcerts.pem -passout pass:notsosecretpassword popd ''; in [ From 58614f841625c3064b8229ef6438efebdc3cc851 Mon Sep 17 00:00:00 2001 From: talyz Date: Tue, 4 May 2021 17:17:27 +0200 Subject: [PATCH 5/9] nixos/keycloak: Add myself to maintainers --- nixos/modules/services/web-apps/keycloak.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 33155c114513..8986e9fb0abe 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -712,4 +712,5 @@ in }; meta.doc = ./keycloak.xml; + meta.maintainers = [ lib.maintainers.talyz ]; } From 83e406e97ab0feb90bfd2925fcf3ed5b01236591 Mon Sep 17 00:00:00 2001 From: talyz Date: Fri, 7 May 2021 15:33:45 +0200 Subject: [PATCH 6/9] nixos/keycloak: frontendUrl always needs to be suffixed with / In some places, Keycloak expects the frontendUrl to end with `/`, so let's make sure it always does. --- nixos/modules/services/web-apps/keycloak.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 8986e9fb0abe..38ade3560dee 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -54,6 +54,7 @@ in frontendUrl = lib.mkOption { type = lib.types.str; + apply = x: if lib.hasSuffix "/" x then x else x + "/"; example = "keycloak.example.com/auth"; description = '' The public URL used as base for all frontend requests. Should From dbf91bc2f12e80dd1933261e049b801d1a626a4e Mon Sep 17 00:00:00 2001 From: talyz Date: Fri, 14 May 2021 12:15:44 +0200 Subject: [PATCH 7/9] nixos/keycloak: keycloak.database* -> keycloak.database.* Move all database options to their own group / attribute. This makes the configuration clearer and brings it in line with most other modern modules. --- nixos/modules/services/web-apps/keycloak.nix | 206 ++++++++++--------- nixos/modules/services/web-apps/keycloak.xml | 22 +- nixos/tests/keycloak.nix | 9 +- 3 files changed, 121 insertions(+), 116 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 38ade3560dee..72ef715c847b 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -98,98 +98,100 @@ in ''; }; - databaseType = lib.mkOption { - type = lib.types.enum [ "mysql" "postgresql" ]; - default = "postgresql"; - example = "mysql"; - description = '' - The type of database Keycloak should connect to. - ''; - }; + database = { + type = lib.mkOption { + type = lib.types.enum [ "mysql" "postgresql" ]; + default = "postgresql"; + example = "mysql"; + description = '' + The type of database Keycloak should connect to. + ''; + }; - databaseHost = lib.mkOption { - type = lib.types.str; - default = "localhost"; - description = '' - Hostname of the database to connect to. - ''; - }; + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = '' + Hostname of the database to connect to. + ''; + }; - databasePort = - let - dbPorts = { - postgresql = 5432; - mysql = 3306; - }; - in - lib.mkOption { - type = lib.types.port; - default = dbPorts.${cfg.databaseType}; - description = '' - Port of the database to connect to. - ''; - }; + port = + let + dbPorts = { + postgresql = 5432; + mysql = 3306; + }; + in + lib.mkOption { + type = lib.types.port; + default = dbPorts.${cfg.database.type}; + description = '' + Port of the database to connect to. + ''; + }; - databaseUseSSL = lib.mkOption { - type = lib.types.bool; - default = cfg.databaseHost != "localhost"; - description = '' - Whether the database connection should be secured by SSL / - TLS. - ''; - }; + useSSL = lib.mkOption { + type = lib.types.bool; + default = cfg.database.host != "localhost"; + description = '' + Whether the database connection should be secured by SSL / + TLS. + ''; + }; - databaseCaCert = lib.mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - description = '' - The SSL / TLS CA certificate that verifies the identity of the - database server. + caCert = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + The SSL / TLS CA certificate that verifies the identity of the + database server. - Required when PostgreSQL is used and SSL is turned on. + Required when PostgreSQL is used and SSL is turned on. - For MySQL, if left at null, the default - Java keystore is used, which should suffice if the server - certificate is issued by an official CA. - ''; - }; + For MySQL, if left at null, the default + Java keystore is used, which should suffice if the server + certificate is issued by an official CA. + ''; + }; - databaseCreateLocally = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Whether a database should be automatically created on the - local host. Set this to false if you plan on provisioning a - local database yourself. This has no effect if - services.keycloak.databaseHost is customized. - ''; - }; + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether a database should be automatically created on the + local host. Set this to false if you plan on provisioning a + local database yourself. This has no effect if + services.keycloak.database.host is customized. + ''; + }; - databaseUsername = lib.mkOption { - type = lib.types.str; - default = "keycloak"; - description = '' - Username to use when connecting to an external or manually - provisioned database; has no effect when a local database is - automatically provisioned. + username = lib.mkOption { + type = lib.types.str; + default = "keycloak"; + description = '' + Username to use when connecting to an external or manually + provisioned database; has no effect when a local database is + automatically provisioned. - To use this with a local database, set to - false and create the database and user - manually. The database should be called - keycloak. - ''; - }; + To use this with a local database, set to + false and create the database and user + manually. The database should be called + keycloak. + ''; + }; - databasePasswordFile = lib.mkOption { - type = lib.types.path; - example = "/run/keys/db_password"; - description = '' - File containing the database password. + passwordFile = lib.mkOption { + type = lib.types.path; + example = "/run/keys/db_password"; + description = '' + File containing the database password. - This should be a string, not a Nix path, since Nix paths are - copied into the world-readable Nix store. - ''; + This should be a string, not a Nix path, since Nix paths are + copied into the world-readable Nix store. + ''; + }; }; package = lib.mkOption { @@ -262,12 +264,12 @@ in config = let # We only want to create a database if we're actually going to connect to it. - databaseActuallyCreateLocally = cfg.databaseCreateLocally && cfg.databaseHost == "localhost"; - createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.databaseType == "postgresql"; - createLocalMySQL = databaseActuallyCreateLocally && cfg.databaseType == "mysql"; + databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost"; + createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql"; + createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql"; mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} '' - ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.databaseCaCert} -keystore $out -storepass notsosecretpassword -noprompt + ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt ''; keycloakConfig' = builtins.foldl' lib.recursiveUpdate { @@ -283,11 +285,11 @@ in }; "subsystem=datasources"."data-source=KeycloakDS" = { max-pool-size = "20"; - user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.databaseUsername; + user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username; password = "@db-password@"; }; } [ - (lib.optionalAttrs (cfg.databaseType == "postgresql") { + (lib.optionalAttrs (cfg.database.type == "postgresql") { "subsystem=datasources" = { "jdbc-driver=postgresql" = { driver-module-name = "org.postgresql"; @@ -295,16 +297,16 @@ in driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; }; "data-source=KeycloakDS" = { - connection-url = "jdbc:postgresql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak"; + connection-url = "jdbc:postgresql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak"; driver-name = "postgresql"; - "connection-properties=ssl".value = lib.boolToString cfg.databaseUseSSL; - } // (lib.optionalAttrs (cfg.databaseCaCert != null) { - "connection-properties=sslrootcert".value = cfg.databaseCaCert; + "connection-properties=ssl".value = lib.boolToString cfg.database.useSSL; + } // (lib.optionalAttrs (cfg.database.caCert != null) { + "connection-properties=sslrootcert".value = cfg.database.caCert; "connection-properties=sslmode".value = "verify-ca"; }); }; }) - (lib.optionalAttrs (cfg.databaseType == "mysql") { + (lib.optionalAttrs (cfg.database.type == "mysql") { "subsystem=datasources" = { "jdbc-driver=mysql" = { driver-module-name = "com.mysql"; @@ -312,16 +314,16 @@ in driver-class-name = "com.mysql.jdbc.Driver"; }; "data-source=KeycloakDS" = { - connection-url = "jdbc:mysql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak"; + connection-url = "jdbc:mysql://${cfg.database.host}:${builtins.toString cfg.database.port}/keycloak"; driver-name = "mysql"; - "connection-properties=useSSL".value = lib.boolToString cfg.databaseUseSSL; - "connection-properties=requireSSL".value = lib.boolToString cfg.databaseUseSSL; - "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.databaseUseSSL; + "connection-properties=useSSL".value = lib.boolToString cfg.database.useSSL; + "connection-properties=requireSSL".value = lib.boolToString cfg.database.useSSL; + "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.database.useSSL; "connection-properties=characterEncoding".value = "UTF-8"; valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"; validate-on-match = true; exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter"; - } // (lib.optionalAttrs (cfg.databaseCaCert != null) { + } // (lib.optionalAttrs (cfg.database.caCert != null) { "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}"; "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword"; }); @@ -573,8 +575,8 @@ in assertions = [ { - assertion = (cfg.databaseUseSSL && cfg.databaseType == "postgresql") -> (cfg.databaseCaCert != null); - message = "A CA certificate must be specified (in 'services.keycloak.databaseCaCert') when PostgreSQL is used with SSL"; + assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null); + message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL"; } ]; @@ -598,7 +600,7 @@ in create_role="$(mktemp)" trap 'rm -f "$create_role"' ERR EXIT - echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.databasePasswordFile}')' CREATEDB" > "$create_role" + echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.database.passwordFile}')' CREATEDB" > "$create_role" psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role" psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' ''; @@ -619,7 +621,7 @@ in set -o errexit -o pipefail -o nounset -o errtrace shopt -s inherit_errexit - db_password="$(<'${cfg.databasePasswordFile}')" + db_password="$(<'${cfg.database.passwordFile}')" ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;" echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';" @@ -659,7 +661,7 @@ in umask u=rwx,g=,o= - install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password + install -T -m 0400 -o keycloak -g keycloak '${cfg.database.passwordFile}' /run/keycloak/secrets/db_password '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle ''; diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml index ca5e223eee46..b622735ca10a 100644 --- a/nixos/modules/services/web-apps/keycloak.xml +++ b/nixos/modules/services/web-apps/keycloak.xml @@ -41,31 +41,31 @@ PostgreSQL or MySQL. Which one is used can be configured in . The selected + linkend="opt-services.keycloak.database.type" />. The selected database will automatically be enabled and a database and role created unless is changed from + linkend="opt-services.keycloak.database.host" /> is changed from its default of localhost or is set + linkend="opt-services.keycloak.database.createLocally" /> is set to false. External database access can also be configured by setting - , , and as + , , and as appropriate. Note that you need to manually create a database called keycloak and allow the configured database user full access to it. - + must be set to the path to a file containing the password used - to log in to the database. If - and + to log in to the database. If + and are kept at their defaults, the database role keycloak with that password is provisioned on the local database instance. @@ -196,7 +196,7 @@ services.keycloak = { frontendUrl = "https://keycloak.example.com/auth"; forceBackendUrlToFrontendUrl = true; certificatePrivateKeyBundle = "/run/keys/ssl_cert"; - databasePasswordFile = "/run/keys/db_password"; + database.passwordFile = "/run/keys/db_password"; }; diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix index 136e83b3e021..ae8f4c5f7e68 100644 --- a/nixos/tests/keycloak.nix +++ b/nixos/tests/keycloak.nix @@ -19,9 +19,12 @@ let virtualisation.memorySize = 1024; services.keycloak = { enable = true; - inherit frontendUrl databaseType initialAdminPassword; - databaseUsername = "bogus"; - databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; + inherit frontendUrl initialAdminPassword; + database = { + type = databaseType; + username = "bogus"; + passwordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; + }; }; environment.systemPackages = with pkgs; [ xmlstarlet From ba00b0946ea624860032d836d1a3105e365cd84e Mon Sep 17 00:00:00 2001 From: talyz Date: Fri, 14 May 2021 14:34:22 +0200 Subject: [PATCH 8/9] nixos/keycloak: Split certificatePrivateKeyBundle into two options Instead of requiring the user to bundle the certificate and private key into a single file, provide separate options for them. This is more in line with most other modules. --- nixos/modules/services/web-apps/keycloak.nix | 35 +++++++++++++++----- nixos/modules/services/web-apps/keycloak.xml | 15 +++++---- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 72ef715c847b..f0b9e60116dd 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -85,13 +85,26 @@ in ''; }; - certificatePrivateKeyBundle = lib.mkOption { + sslCertificate = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; example = "/run/keys/ssl_cert"; description = '' - The path to a PEM formatted bundle of the private key and - certificate to use for TLS connections. + The path to a PEM formatted certificate to use for TLS/SSL + connections. + + This should be a string, not a Nix path, since Nix paths are + copied into the world-readable Nix store. + ''; + }; + + sslCertificateKey = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/ssl_key"; + description = '' + The path to a PEM formatted private key to use for TLS/SSL + connections. This should be a string, not a Nix path, since Nix paths are copied into the world-readable Nix store. @@ -329,7 +342,7 @@ in }); }; }) - (lib.optionalAttrs (cfg.certificatePrivateKeyBundle != null) { + (lib.optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) { "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort; "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = { keystore-path = "/run/keycloak/ssl/certificate_private_key_bundle.p12"; @@ -662,8 +675,9 @@ in umask u=rwx,g=,o= install -T -m 0400 -o keycloak -g keycloak '${cfg.database.passwordFile}' /run/keycloak/secrets/db_password - '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' - install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle + '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' + install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificate}' /run/keycloak/secrets/ssl_cert + install -T -m 0400 -o keycloak -g keycloak '${cfg.sslCertificateKey}' /run/keycloak/secrets/ssl_key ''; startPre = '' set -o errexit -o pipefail -o nounset -o errtrace @@ -678,10 +692,13 @@ in export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}' - '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' + '' + lib.optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) '' pushd /run/keycloak/ssl/ - cat /run/keycloak/secrets/ssl_cert_pk_bundle <(echo) /etc/ssl/certs/ca-certificates.crt > allcerts.pem - openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -chain \ + cat /run/keycloak/secrets/ssl_cert <(echo) \ + /run/keycloak/secrets/ssl_key <(echo) \ + /etc/ssl/certs/ca-certificates.crt \ + > allcerts.pem + openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert -inkey /run/keycloak/secrets/ssl_key -chain \ -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \ -CAfile allcerts.pem -passout pass:notsosecretpassword popd diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml index b622735ca10a..7ba656c20f16 100644 --- a/nixos/modules/services/web-apps/keycloak.xml +++ b/nixos/modules/services/web-apps/keycloak.xml @@ -115,17 +115,17 @@ - For HTTPS support, a TLS certificate and private key is - required. They should be PEM - formatted and concatenated into a single file. The path - to this file should be configured in - . + formatted. Their paths should be set through and . - The path should be provided as a string, not a Nix path, + The paths should be provided as a strings, not a Nix paths, since Nix paths are copied into the world readable Nix store. @@ -195,7 +195,8 @@ services.keycloak = { initialAdminPassword = "e6Wcm0RrtegMEHl"; # change on first login frontendUrl = "https://keycloak.example.com/auth"; forceBackendUrlToFrontendUrl = true; - certificatePrivateKeyBundle = "/run/keys/ssl_cert"; + sslCertificate = "/run/keys/ssl_cert"; + sslCertificateKey = "/run/keys/ssl_key"; database.passwordFile = "/run/keys/db_password"; }; From 2d8a87081305f0bcfb401f32aff2822797264b3d Mon Sep 17 00:00:00 2001 From: talyz Date: Fri, 14 May 2021 14:39:03 +0200 Subject: [PATCH 9/9] keycloak.tests: Test HTTPS support --- nixos/tests/keycloak.nix | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix index ae8f4c5f7e68..fc321b8902f1 100644 --- a/nixos/tests/keycloak.nix +++ b/nixos/tests/keycloak.nix @@ -3,7 +3,8 @@ # client using their Keycloak login. let - frontendUrl = "http://keycloak/auth"; + certs = import ./common/acme/server/snakeoil-certs.nix; + frontendUrl = "https://${certs.domain}/auth"; initialAdminPassword = "h4IhoJFnt2iQIR9"; keycloakTest = import ./make-test-python.nix ( @@ -17,15 +18,27 @@ let nodes = { keycloak = { ... }: { virtualisation.memorySize = 1024; + + security.pki.certificateFiles = [ + certs.ca.cert + ]; + + networking.extraHosts = '' + 127.0.0.1 ${certs.domain} + ''; + services.keycloak = { enable = true; inherit frontendUrl initialAdminPassword; + sslCertificate = certs.${certs.domain}.cert; + sslCertificateKey = certs.${certs.domain}.key; database = { type = databaseType; username = "bogus"; passwordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; }; }; + environment.systemPackages = with pkgs; [ xmlstarlet libtidy