summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorElis Hirwing <elis@hirwing.se>2021-05-21 16:55:26 +0200
committerGitHub <noreply@github.com>2021-05-21 16:55:26 +0200
commite9cca93bf9b75d6207aa1517b34d9681b76b06dc (patch)
tree2bdce4b93b76c9bf2f8186bceca412299a06d9e7 /nixos
parent7b76e495df68217b43df39748b99ad70250d88a0 (diff)
parent2d8a87081305f0bcfb401f32aff2822797264b3d (diff)
downloadnixpkgs-e9cca93bf9b75d6207aa1517b34d9681b76b06dc.tar
nixpkgs-e9cca93bf9b75d6207aa1517b34d9681b76b06dc.tar.gz
nixpkgs-e9cca93bf9b75d6207aa1517b34d9681b76b06dc.tar.bz2
nixpkgs-e9cca93bf9b75d6207aa1517b34d9681b76b06dc.tar.lz
nixpkgs-e9cca93bf9b75d6207aa1517b34d9681b76b06dc.tar.xz
nixpkgs-e9cca93bf9b75d6207aa1517b34d9681b76b06dc.tar.zst
nixpkgs-e9cca93bf9b75d6207aa1517b34d9681b76b06dc.zip
Merge pull request #121778 from talyz/keycloak-security
nixos/keycloak: Security fixes + misc
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix284
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml37
-rw-r--r--nixos/tests/keycloak.nix24
3 files changed, 199 insertions, 146 deletions
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index e2e6df41dfa..f0b9e60116d 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
@@ -84,111 +85,126 @@ 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.
       '';
     };
 
-    databaseType = lib.mkOption {
-      type = lib.types.enum [ "mysql" "postgresql" ];
-      default = "postgresql";
-      example = "mysql";
+    sslCertificateKey = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      example = "/run/keys/ssl_key";
       description = ''
-        The type of database Keycloak should connect to.
-      '';
-    };
+        The path to a PEM formatted private key to use for TLS/SSL
+        connections.
 
-    databaseHost = lib.mkOption {
-      type = lib.types.str;
-      default = "localhost";
-      description = ''
-        Hostname of the database to connect to.
+        This should be a string, not a Nix path, since Nix paths are
+        copied into the world-readable Nix store.
       '';
     };
 
-    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.
-          '';
-        };
+    database = {
+      type = lib.mkOption {
+        type = lib.types.enum [ "mysql" "postgresql" ];
+        default = "postgresql";
+        example = "mysql";
+        description = ''
+          The type of database Keycloak should connect to.
+        '';
+      };
 
-    databaseUseSSL = lib.mkOption {
-      type = lib.types.bool;
-      default = cfg.databaseHost != "localhost";
-      description = ''
-        Whether the database connection should be secured by SSL /
-        TLS.
-      '';
-    };
+      host = lib.mkOption {
+        type = lib.types.str;
+        default = "localhost";
+        description = ''
+          Hostname of the database to connect to.
+        '';
+      };
 
-    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.
+      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.
+            '';
+          };
 
-        Required when PostgreSQL is used and SSL is turned on.
+      useSSL = lib.mkOption {
+        type = lib.types.bool;
+        default = cfg.database.host != "localhost";
+        description = ''
+          Whether the database connection should be secured by SSL /
+          TLS.
+        '';
+      };
 
-        For MySQL, if left at <literal>null</literal>, the default
-        Java keystore is used, which should suffice if the server
-        certificate is issued by an official CA.
-      '';
-    };
+      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.
 
-    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.
-      '';
-    };
+          Required when PostgreSQL is used and SSL is turned on.
 
-    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.
-
-        To use this with a local database, set <xref
-        linkend="opt-services.keycloak.databaseCreateLocally" /> to
-        <literal>false</literal> and create the database and user
-        manually. The database should be called
-        <literal>keycloak</literal>.
-      '';
-    };
+          For MySQL, if left at <literal>null</literal>, the default
+          Java keystore is used, which should suffice if the server
+          certificate is issued by an official CA.
+        '';
+      };
 
-    databasePasswordFile = lib.mkOption {
-      type = lib.types.path;
-      example = "/run/keys/db_password";
-      description = ''
-        File containing the database password.
+      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.
+        '';
+      };
 
-        This should be a string, not a Nix path, since Nix paths are
-        copied into the world-readable Nix store.
-      '';
+      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 <xref
+          linkend="opt-services.keycloak.database.createLocally" /> to
+          <literal>false</literal> and create the database and user
+          manually. The database should be called
+          <literal>keycloak</literal>.
+        '';
+      };
+
+      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.
+        '';
+      };
     };
 
     package = lib.mkOption {
@@ -261,12 +277,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 {
@@ -282,11 +298,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";
@@ -294,16 +310,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";
@@ -311,22 +327,22 @@ 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";
             });
           };
         })
-        (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";
@@ -537,7 +553,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 +565,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 +579,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
       '';
@@ -570,8 +588,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";
           }
         ];
 
@@ -581,6 +599,7 @@ in
           after = [ "postgresql.service" ];
           before = [ "keycloak.service" ];
           bindsTo = [ "postgresql.service" ];
+          path = [ config.services.postgresql.package ];
           serviceConfig = {
             Type = "oneshot";
             RemainAfterExit = true;
@@ -588,13 +607,15 @@ 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
+            create_role="$(mktemp)"
+            trap 'rm -f "$create_role"' ERR EXIT
 
-            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"
-            $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
+            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"'
           '';
         };
 
@@ -602,6 +623,7 @@ in
           after = [ "mysql.service" ];
           before = [ "keycloak.service" ];
           bindsTo = [ "mysql.service" ];
+          path = [ config.services.mysql.package ];
           serviceConfig = {
             Type = "oneshot";
             RemainAfterExit = true;
@@ -609,13 +631,14 @@ 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}')"
+            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';"
-            ) | ${config.services.mysql.package}/bin/mysql -N
+            ) | mysql -N
           '';
         };
 
@@ -634,6 +657,8 @@ in
             bindsTo = databaseServices;
             wantedBy = [ "multi-user.target" ];
             path = with pkgs; [
+              cfg.package
+              openssl
               replace-secret
             ];
             environment = {
@@ -644,14 +669,21 @@ 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
+                  umask u=rwx,g=,o=
+
+                  install -T -m 0400 -o keycloak -g keycloak '${cfg.database.passwordFile}' /run/keycloak/secrets/db_password
+                '' + 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 -eu
+                  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
@@ -659,13 +691,16 @@ 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}'
-                '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) ''
+                  add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
+                '' + 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
-                  ${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
+                  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
                 '';
               in [
@@ -697,4 +732,5 @@ in
       };
 
   meta.doc = ./keycloak.xml;
+  meta.maintainers = [ lib.maintainers.talyz ];
 }
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
index ca5e223eee4..7ba656c20f1 100644
--- a/nixos/modules/services/web-apps/keycloak.xml
+++ b/nixos/modules/services/web-apps/keycloak.xml
@@ -41,31 +41,31 @@
        <productname>PostgreSQL</productname> or
        <productname>MySQL</productname>. Which one is used can be
        configured in <xref
-       linkend="opt-services.keycloak.databaseType" />. The selected
+       linkend="opt-services.keycloak.database.type" />. The selected
        database will automatically be enabled and a database and role
        created unless <xref
-       linkend="opt-services.keycloak.databaseHost" /> is changed from
+       linkend="opt-services.keycloak.database.host" /> is changed from
        its default of <literal>localhost</literal> or <xref
-       linkend="opt-services.keycloak.databaseCreateLocally" /> is set
+       linkend="opt-services.keycloak.database.createLocally" /> is set
        to <literal>false</literal>.
      </para>
 
      <para>
        External database access can also be configured by setting
-       <xref linkend="opt-services.keycloak.databaseHost" />, <xref
-       linkend="opt-services.keycloak.databaseUsername" />, <xref
-       linkend="opt-services.keycloak.databaseUseSSL" /> and <xref
-       linkend="opt-services.keycloak.databaseCaCert" /> as
+       <xref linkend="opt-services.keycloak.database.host" />, <xref
+       linkend="opt-services.keycloak.database.username" />, <xref
+       linkend="opt-services.keycloak.database.useSSL" /> and <xref
+       linkend="opt-services.keycloak.database.caCert" /> as
        appropriate. Note that you need to manually create a database
        called <literal>keycloak</literal> and allow the configured
        database user full access to it.
      </para>
 
      <para>
-       <xref linkend="opt-services.keycloak.databasePasswordFile" />
+       <xref linkend="opt-services.keycloak.database.passwordFile" />
        must be set to the path to a file containing the password used
-       to log in to the database. If <xref linkend="opt-services.keycloak.databaseHost" />
-       and <xref linkend="opt-services.keycloak.databaseCreateLocally" />
+       to log in to the database. If <xref linkend="opt-services.keycloak.database.host" />
+       and <xref linkend="opt-services.keycloak.database.createLocally" />
        are kept at their defaults, the database role
        <literal>keycloak</literal> with that password is provisioned
        on the local database instance.
@@ -115,17 +115,17 @@
      </para>
 
      <para>
-       For HTTPS support, a TLS certificate and private key is
-       required. They should be <link
+       HTTPS support requires a TLS/SSL certificate and a private key,
+       both <link
        xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
-       formatted</link> and concatenated into a single file. The path
-       to this file should be configured in
-       <xref linkend="opt-services.keycloak.certificatePrivateKeyBundle" />.
+       formatted</link>. Their paths should be set through <xref
+       linkend="opt-services.keycloak.sslCertificate" /> and <xref
+       linkend="opt-services.keycloak.sslCertificateKey" />.
      </para>
 
      <warning>
        <para>
-         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.
        </para>
      </warning>
@@ -195,8 +195,9 @@ services.keycloak = {
   <link linkend="opt-services.keycloak.initialAdminPassword">initialAdminPassword</link> = "e6Wcm0RrtegMEHl";  # change on first login
   <link linkend="opt-services.keycloak.frontendUrl">frontendUrl</link> = "https://keycloak.example.com/auth";
   <link linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl">forceBackendUrlToFrontendUrl</link> = true;
-  <link linkend="opt-services.keycloak.certificatePrivateKeyBundle">certificatePrivateKeyBundle</link> = "/run/keys/ssl_cert";
-  <link linkend="opt-services.keycloak.databasePasswordFile">databasePasswordFile</link> = "/run/keys/db_password";
+  <link linkend="opt-services.keycloak.sslCertificate">sslCertificate</link> = "/run/keys/ssl_cert";
+  <link linkend="opt-services.keycloak.sslCertificateKey">sslCertificateKey</link> = "/run/keys/ssl_key";
+  <link linkend="opt-services.keycloak.database.passwordFile">database.passwordFile</link> = "/run/keys/db_password";
 };
 </programlisting>
      </para>
diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix
index 136e83b3e02..fc321b8902f 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,12 +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 databaseType initialAdminPassword;
-            databaseUsername = "bogus";
-            databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
+            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