From 89e83833af35bd0ec3fdc65c435358a676a41d89 Mon Sep 17 00:00:00 2001 From: talyz Date: Mon, 26 Oct 2020 15:33:57 +0100 Subject: [PATCH] nixos/keycloak: Add support for MySQL and external DBs with SSL - Add support for using MySQL as an option to PostgreSQL. - Enable connecting to external DBs with SSL - Add a database port config option --- nixos/modules/services/web-apps/keycloak.nix | 265 ++++++++++++++----- nixos/modules/services/web-apps/keycloak.xml | 27 +- nixos/tests/all-tests.nix | 2 +- nixos/tests/keycloak.nix | 21 +- pkgs/servers/keycloak/default.nix | 23 +- 5 files changed, 243 insertions(+), 95 deletions(-) diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix index 9c6a5ca305cb..bbb0c8d04831 100644 --- a/nixos/modules/services/web-apps/keycloak.nix +++ b/nixos/modules/services/web-apps/keycloak.nix @@ -97,11 +97,59 @@ in ''; }; + databaseType = 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 PostgreSQL database to connect to. + 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. + ''; + }; + + databaseUseSSL = lib.mkOption { + type = lib.types.bool; + default = cfg.databaseHost != "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. + + 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. ''; }; @@ -208,6 +256,12 @@ in 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"; + + mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} '' + ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.databaseCaCert} -keystore $out -storepass notsosecretpassword -noprompt + ''; keycloakConfig' = builtins.foldl' lib.recursiveUpdate { "interface=public".inet-address = cfg.bindAddress; @@ -220,19 +274,52 @@ in }; }; }; - "subsystem=datasources"."jdbc-driver=postgresql" = { - driver-module-name = "org.postgresql"; - driver-name = "postgresql"; - driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; - }; "subsystem=datasources"."data-source=KeycloakDS" = { - connection-url = "jdbc:postgresql://${cfg.databaseHost}/keycloak"; - driver-name = "postgresql"; max-pool-size = "20"; user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.databaseUsername; password = "@db-password@"; }; } [ + (lib.optionalAttrs (cfg.databaseType == "postgresql") { + "subsystem=datasources" = { + "jdbc-driver=postgresql" = { + driver-module-name = "org.postgresql"; + driver-name = "postgresql"; + driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; + }; + "data-source=KeycloakDS" = { + connection-url = "jdbc:postgresql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/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=sslmode".value = "verify-ca"; + }); + }; + }) + (lib.optionalAttrs (cfg.databaseType == "mysql") { + "subsystem=datasources" = { + "jdbc-driver=mysql" = { + driver-module-name = "com.mysql"; + driver-name = "mysql"; + driver-class-name = "com.mysql.jdbc.Driver"; + }; + "data-source=KeycloakDS" = { + connection-url = "jdbc:mysql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/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=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) { + "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}"; + "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword"; + }); + }; + }) (lib.optionalAttrs (cfg.certificatePrivateKeyBundle != null) { "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort; "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = { @@ -444,7 +531,7 @@ in jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); - keycloakConfig = pkgs.runCommand "keycloak-config" {} '' + keycloakConfig = pkgs.runCommandNoCC "keycloak-config" {} '' export JBOSS_BASE_DIR="$(pwd -P)"; export JBOSS_MODULEPATH="${cfg.package}/modules"; export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log"; @@ -475,9 +562,16 @@ in in lib.mkIf cfg.enable { + 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''; + } + ]; + environment.systemPackages = [ cfg.package ]; - systemd.services.keycloakDatabaseInit = lib.mkIf databaseActuallyCreateLocally { + systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL { after = [ "postgresql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "postgresql.service" ]; @@ -498,71 +592,100 @@ in ''; }; - systemd.services.keycloak = { - after = lib.optionals databaseActuallyCreateLocally [ - "keycloakDatabaseInit.service" "postgresql.service" - ]; - bindsTo = lib.optionals databaseActuallyCreateLocally [ - "keycloakDatabaseInit.service" "postgresql.service" - ]; - wantedBy = [ "multi-user.target" ]; - environment = { - JBOSS_LOG_DIR = "/var/log/keycloak"; - JBOSS_BASE_DIR = "/run/keycloak"; - JBOSS_MODULEPATH = "${cfg.package}/modules"; - }; + systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL { + after = [ "mysql.service" ]; + before = [ "keycloak.service" ]; + bindsTo = [ "mysql.service" ]; serviceConfig = { - ExecStartPre = let - startPreFullPrivileges = '' - set -eu - - 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 - - install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration - install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml - - db_password="$( 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 - popd - ''; - in [ - "+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}" - "${pkgs.writeShellScript "keycloak-start-pre" startPre}" - ]; - ExecStart = "${cfg.package}/bin/standalone.sh"; - User = "keycloak"; - Group = "keycloak"; - DynamicUser = true; - RuntimeDirectory = map (p: "keycloak/" + p) [ - "secrets" - "configuration" - "deployments" - "data" - "ssl" - "log" - "tmp" - ]; - RuntimeDirectoryMode = 0700; - LogsDirectory = "keycloak"; - AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + Type = "oneshot"; + RemainAfterExit = true; + User = config.services.mysql.user; + Group = config.services.mysql.group; }; + script = '' + set -eu + + db_password="$(<'${cfg.databasePasswordFile}')" + ( 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 + ''; }; - services.postgresql.enable = lib.mkDefault databaseActuallyCreateLocally; + systemd.services.keycloak = + let + databaseServices = + if createLocalPostgreSQL then [ + "keycloakPostgreSQLInit.service" "postgresql.service" + ] + else if createLocalMySQL then [ + "keycloakMySQLInit.service" "mysql.service" + ] + else [ ]; + in { + after = databaseServices; + bindsTo = databaseServices; + wantedBy = [ "multi-user.target" ]; + environment = { + JBOSS_LOG_DIR = "/var/log/keycloak"; + JBOSS_BASE_DIR = "/run/keycloak"; + JBOSS_MODULEPATH = "${cfg.package}/modules"; + }; + serviceConfig = { + ExecStartPre = let + startPreFullPrivileges = '' + set -eu + + 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 + + install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration + install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml + + db_password="$( 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 + popd + ''; + in [ + "+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}" + "${pkgs.writeShellScript "keycloak-start-pre" startPre}" + ]; + ExecStart = "${cfg.package}/bin/standalone.sh"; + User = "keycloak"; + Group = "keycloak"; + DynamicUser = true; + RuntimeDirectory = map (p: "keycloak/" + p) [ + "secrets" + "configuration" + "deployments" + "data" + "ssl" + "log" + "tmp" + ]; + RuntimeDirectoryMode = 0700; + LogsDirectory = "keycloak"; + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + }; + }; + + services.postgresql.enable = lib.mkDefault createLocalPostgreSQL; + services.mysql.enable = lib.mkDefault createLocalMySQL; + services.mysql.package = lib.mkIf createLocalMySQL pkgs.mysql; }; meta.doc = ./keycloak.xml; diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml index 6b97d48e0bd8..ca5e223eee46 100644 --- a/nixos/modules/services/web-apps/keycloak.xml +++ b/nixos/modules/services/web-apps/keycloak.xml @@ -37,15 +37,30 @@
Database access - Keycloak depends on - PostgreSQL and will automatically - enable it and create a database and role unless configured not - to, either by changing - from its default of localhost or setting - + Keycloak can be used with either + PostgreSQL or + MySQL. Which one is used can be + configured in . The selected + database will automatically be enabled and a database and role + created unless is changed from + its default of localhost or is set to false. + + External database access can also be configured by setting + , , 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 diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 5a10f60fc9a3..d49357cb463c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -175,7 +175,7 @@ in kernel-latest = handleTest ./kernel-latest.nix {}; kernel-lts = handleTest ./kernel-lts.nix {}; kernel-testing = handleTest ./kernel-testing.nix {}; - keycloak = handleTest ./keycloak.nix {}; + keycloak = discoverTests (import ./keycloak.nix); keymap = handleTest ./keymap.nix {}; knot = handleTest ./knot.nix {}; krb5 = discoverTests (import ./krb5 {}); diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix index e5e31b038e9c..f448a0f7095f 100644 --- a/nixos/tests/keycloak.nix +++ b/nixos/tests/keycloak.nix @@ -2,12 +2,12 @@ # OIDC client and a user, and simulates the user logging in to the # client using their Keycloak login. -import ./make-test-python.nix ( - { pkgs, ... }: - let - frontendUrl = "http://keycloak/auth"; - initialAdminPassword = "h4IhoJFnt2iQIR9"; - in +let + frontendUrl = "http://keycloak/auth"; + initialAdminPassword = "h4IhoJFnt2iQIR9"; + + keycloakTest = import ./make-test-python.nix ( + { pkgs, databaseType, ... }: { name = "keycloak"; meta = with pkgs.stdenv.lib.maintainers; { @@ -19,7 +19,7 @@ import ./make-test-python.nix ( virtualisation.memorySize = 1024; services.keycloak = { enable = true; - inherit frontendUrl initialAdminPassword; + inherit frontendUrl databaseType initialAdminPassword; databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; }; environment.systemPackages = with pkgs; [ @@ -136,4 +136,9 @@ import ./make-test-python.nix ( ) ''; } -) + ); +in +{ + postgres = keycloakTest { databaseType = "postgresql"; }; + mysql = keycloakTest { databaseType = "mysql"; }; +} diff --git a/pkgs/servers/keycloak/default.nix b/pkgs/servers/keycloak/default.nix index 95935ce8f8ae..67d3d9bd45a2 100644 --- a/pkgs/servers/keycloak/default.nix +++ b/pkgs/servers/keycloak/default.nix @@ -1,11 +1,11 @@ -{ stdenv, fetchzip, makeWrapper, jre, writeText, nixosTests -, postgresql_jdbc ? null +{ stdenv, lib, fetchzip, makeWrapper, jre, writeText, nixosTests +, postgresql_jdbc ? null, mysql_jdbc ? null }: let mkModuleXml = name: jarFile: writeText "module.xml" '' - + @@ -33,17 +33,22 @@ stdenv.mkDerivation rec { rm -rf $out/bin/*.{ps1,bat} - module_path=$out/modules/system/layers/keycloak/org + module_path=$out/modules/system/layers/keycloak if ! [[ -d $module_path ]]; then echo "The module path $module_path not found!" exit 1 fi - ${if postgresql_jdbc != null then '' - mkdir -p $module_path/postgresql/main - ln -s ${postgresql_jdbc}/share/java/postgresql-jdbc.jar $module_path/postgresql/main - ln -s ${mkModuleXml "postgresql" "postgresql-jdbc.jar"} $module_path/postgresql/main/module.xml - '' else ""} + ${lib.optionalString (postgresql_jdbc != null) '' + mkdir -p $module_path/org/postgresql/main + ln -s ${postgresql_jdbc}/share/java/postgresql-jdbc.jar $module_path/org/postgresql/main/ + ln -s ${mkModuleXml "org.postgresql" "postgresql-jdbc.jar"} $module_path/org/postgresql/main/module.xml + ''} + ${lib.optionalString (mysql_jdbc != null) '' + mkdir -p $module_path/com/mysql/main + ln -s ${mysql_jdbc}/share/java/mysql-connector-java.jar $module_path/com/mysql/main/ + ln -s ${mkModuleXml "com.mysql" "mysql-connector-java.jar"} $module_path/com/mysql/main/module.xml + ''} wrapProgram $out/bin/standalone.sh --set JAVA_HOME ${jre} wrapProgram $out/bin/add-user-keycloak.sh --set JAVA_HOME ${jre}