summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2205.section.xml125
-rw-r--r--nixos/doc/manual/release-notes/rl-2205.section.md75
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix873
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml142
-rw-r--r--nixos/tests/keycloak.nix22
-rw-r--r--pkgs/servers/keycloak/default.nix98
6 files changed, 686 insertions, 649 deletions
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 830730caef8..c815d50089a 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -645,6 +645,131 @@
       </listitem>
       <listitem>
         <para>
+          The Keycloak package (<literal>pkgs.keycloak</literal>) has
+          been switched from the Wildfly version, which will soon be
+          deprecated, to the Quarkus based version. The Keycloak service
+          (<literal>services.keycloak</literal>) has been updated to
+          accommodate the change and now differs from the previous
+          version in a few ways:
+        </para>
+        <itemizedlist>
+          <listitem>
+            <para>
+              <literal>services.keycloak.extraConfig</literal> has been
+              removed in favor of the new
+              <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">settings-style</link>
+              <link linkend="opt-services.keycloak.settings"><literal>services.keycloak.settings</literal></link>
+              option. The available options correspond directly to
+              parameters in <literal>conf/keycloak.conf</literal>. Some
+              of the most important parameters are documented as
+              suboptions, the rest can be found in the
+              <link xlink:href="https://www.keycloak.org/server/all-config">All
+              configuration section of the Keycloak Server Installation
+              and Configuration Guide</link>. While the new
+              configuration is much simpler and cleaner than the old
+              JBoss CLI one, this unfortunately mean that there’s no
+              straightforward way to convert an old configuration to the
+              new format and some settings may not even be available
+              anymore.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>services.keycloak.frontendUrl</literal> was
+              removed and the frontend URL is now configured through the
+              <literal>hostname</literal> family of settings in
+              <link linkend="opt-services.keycloak.settings"><literal>services.keycloak.settings</literal></link>
+              instead. See the
+              <link xlink:href="https://www.keycloak.org/server/hostname">Hostname
+              section of the Keycloak Server Installation and
+              Configuration Guide</link> for more details. Additionally,
+              <literal>/auth</literal> was removed from the default
+              context path and needs to be added back in
+              <link linkend="opt-services.keycloak.settings.http-relative-path"><literal>services.keycloak.settings.http-relative-path</literal></link>
+              if you want to keep compatibility with your current
+              clients.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              <literal>services.keycloak.bindAddress</literal>,
+              <literal>services.keycloak.forceBackendUrlToFrontendUrl</literal>,
+              <literal>services.keycloak.httpPort</literal> and
+              <literal>services.keycloak.httpsPort</literal> have been
+              removed in favor of their equivalent options in
+              <link linkend="opt-services.keycloak.settings"><literal>services.keycloak.settings</literal></link>.
+              <literal>httpPort</literal> and
+              <literal>httpsPort</literal> have additionally had their
+              types changed from <literal>str</literal> to
+              <literal>port</literal>.
+            </para>
+            <para>
+              The new names are as follows:
+            </para>
+            <itemizedlist spacing="compact">
+              <listitem>
+                <para>
+                  <literal>bindAddress</literal>:
+                  <link linkend="opt-services.keycloak.settings.http-host"><literal>services.keycloak.settings.http-host</literal></link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>forceBackendUrlToFrontendUrl</literal>:
+                  <link linkend="opt-services.keycloak.settings.hostname-strict-backchannel"><literal>services.keycloak.settings.hostname-strict-backchannel</literal></link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>httpPort</literal>:
+                  <link linkend="opt-services.keycloak.settings.http-port"><literal>services.keycloak.settings.http-port</literal></link>
+                </para>
+              </listitem>
+              <listitem>
+                <para>
+                  <literal>httpsPort</literal>:
+                  <link linkend="opt-services.keycloak.settings.https-port"><literal>services.keycloak.settings.https-port</literal></link>
+                </para>
+              </listitem>
+            </itemizedlist>
+          </listitem>
+        </itemizedlist>
+        <para>
+          For example, when using a reverse proxy the migration could
+          look like this:
+        </para>
+        <para>
+          Before:
+        </para>
+        <programlisting language="bash">
+  services.keycloak = {
+    enable = true;
+    httpPort = &quot;8080&quot;;
+    frontendUrl = &quot;https://keycloak.example.com/auth&quot;;
+    database.passwordFile = &quot;/run/keys/db_password&quot;;
+    extraConfig = {
+      &quot;subsystem=undertow&quot;.&quot;server=default-server&quot;.&quot;http-listener=default&quot;.proxy-address-forwarding = true;
+    };
+  };
+</programlisting>
+        <para>
+          After:
+        </para>
+        <programlisting language="bash">
+  services.keycloak = {
+    enable = true;
+    settings = {
+      http-port = 8080;
+      hostname = &quot;keycloak.example.com&quot;;
+      http-relative-path = &quot;/auth&quot;;
+      proxy = &quot;edge&quot;;
+    };
+    database.passwordFile = &quot;/run/keys/db_password&quot;;
+  };
+</programlisting>
+      </listitem>
+      <listitem>
+        <para>
           The MoinMoin wiki engine
           (<literal>services.moinmoin</literal>) has been removed,
           because Python 2 is being retired from nixpkgs.
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 333c2355232..2e083d000d3 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -267,6 +267,81 @@ In addition to numerous new and upgraded packages, this release has the followin
   `media_store_path` was changed from `${dataDir}/media` to `${dataDir}/media_store` if `system.stateVersion` is at least `22.05`. Files will need to be manually moved to the new
   location if the `stateVersion` is updated.
 
+- The Keycloak package (`pkgs.keycloak`) has been switched from the
+  Wildfly version, which will soon be deprecated, to the Quarkus based
+  version. The Keycloak service (`services.keycloak`) has been updated
+  to accommodate the change and now differs from the previous version
+  in a few ways:
+
+  - `services.keycloak.extraConfig` has been removed in favor of the
+    new [settings-style](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md)
+    [`services.keycloak.settings`](#opt-services.keycloak.settings)
+    option. The available options correspond directly to parameters in
+    `conf/keycloak.conf`. Some of the most important parameters are
+    documented as suboptions, the rest can be found in the [All
+    configuration section of the Keycloak Server Installation and
+    Configuration
+    Guide](https://www.keycloak.org/server/all-config). While the new
+    configuration is much simpler and cleaner than the old JBoss CLI
+    one, this unfortunately mean that there's no straightforward way
+    to convert an old configuration to the new format and some
+    settings may not even be available anymore.
+
+  - `services.keycloak.frontendUrl` was removed and the frontend URL
+    is now configured through the `hostname` family of settings in
+    [`services.keycloak.settings`](#opt-services.keycloak.settings)
+    instead. See the [Hostname section of the Keycloak Server
+    Installation and Configuration
+    Guide](https://www.keycloak.org/server/hostname) for more
+    details. Additionally, `/auth` was removed from the default
+    context path and needs to be added back in
+    [`services.keycloak.settings.http-relative-path`](#opt-services.keycloak.settings.http-relative-path)
+    if you want to keep compatibility with your current clients.
+
+  - `services.keycloak.bindAddress`,
+    `services.keycloak.forceBackendUrlToFrontendUrl`,
+    `services.keycloak.httpPort` and `services.keycloak.httpsPort`
+    have been removed in favor of their equivalent options in
+    [`services.keycloak.settings`](#opt-services.keycloak.settings). `httpPort`
+    and `httpsPort` have additionally had their types changed from
+    `str` to `port`.
+
+    The new names are as follows:
+    - `bindAddress`: [`services.keycloak.settings.http-host`](#opt-services.keycloak.settings.http-host)
+    - `forceBackendUrlToFrontendUrl`: [`services.keycloak.settings.hostname-strict-backchannel`](#opt-services.keycloak.settings.hostname-strict-backchannel)
+    - `httpPort`: [`services.keycloak.settings.http-port`](#opt-services.keycloak.settings.http-port)
+    - `httpsPort`: [`services.keycloak.settings.https-port`](#opt-services.keycloak.settings.https-port)
+
+  For example, when using a reverse proxy the migration could look
+  like this:
+
+  Before:
+  ```nix
+    services.keycloak = {
+      enable = true;
+      httpPort = "8080";
+      frontendUrl = "https://keycloak.example.com/auth";
+      database.passwordFile = "/run/keys/db_password";
+      extraConfig = {
+        "subsystem=undertow"."server=default-server"."http-listener=default".proxy-address-forwarding = true;
+      };
+    };
+  ```
+
+  After:
+  ```nix
+    services.keycloak = {
+      enable = true;
+      settings = {
+        http-port = 8080;
+        hostname = "keycloak.example.com";
+        http-relative-path = "/auth";
+        proxy = "edge";
+      };
+      database.passwordFile = "/run/keys/db_password";
+    };
+  ```
+
 - The MoinMoin wiki engine (`services.moinmoin`) has been removed, because Python 2 is being retired from nixpkgs.
 
 - Services in the `hadoop` module previously set `openFirewall` to true by default.
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index c4a2127663a..2d817ca1923 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -4,20 +4,94 @@ let
   cfg = config.services.keycloak;
   opt = options.services.keycloak;
 
-  inherit (lib) types mkOption concatStringsSep mapAttrsToList
-    escapeShellArg recursiveUpdate optionalAttrs boolToString mkOrder
-    sort filterAttrs concatMapStringsSep concatStrings mkIf
-    optionalString optionals mkDefault literalExpression hasSuffix
-    foldl' isAttrs filter attrNames elem literalDocBook
-    maintainers;
-
-  inherit (builtins) match typeOf;
+  inherit (lib)
+    types
+    mkMerge
+    mkOption
+    mkChangedOptionModule
+    mkRenamedOptionModule
+    mkRemovedOptionModule
+    concatStringsSep
+    mapAttrsToList
+    escapeShellArg
+    mkIf
+    optionalString
+    optionals
+    mkDefault
+    literalExpression
+    isAttrs
+    literalDocBook
+    maintainers
+    catAttrs
+    collect
+    splitString
+    ;
+
+  inherit (builtins)
+    elem
+    typeOf
+    isInt
+    isString
+    hashString
+    isPath
+    ;
+
+  prefixUnlessEmpty = prefix: string: optionalString (string != "") "${prefix}${string}";
 in
 {
+  imports =
+    [
+      (mkRenamedOptionModule
+        [ "services" "keycloak" "bindAddress" ]
+        [ "services" "keycloak" "settings" "http-host" ])
+      (mkRenamedOptionModule
+        [ "services" "keycloak" "forceBackendUrlToFrontendUrl"]
+        [ "services" "keycloak" "settings" "hostname-strict-backchannel"])
+      (mkChangedOptionModule
+        [ "services" "keycloak" "httpPort" ]
+        [ "services" "keycloak" "settings" "http-port" ]
+        (config:
+          builtins.fromJSON config.services.keycloak.httpPort))
+      (mkChangedOptionModule
+        [ "services" "keycloak" "httpsPort" ]
+        [ "services" "keycloak" "settings" "https-port" ]
+        (config:
+          builtins.fromJSON config.services.keycloak.httpsPort))
+      (mkRemovedOptionModule
+        [ "services" "keycloak" "frontendUrl" ]
+        ''
+          Set `services.keycloak.settings.hostname' and `services.keycloak.settings.http-relative-path' instead.
+          NOTE: You likely want to set 'http-relative-path' to '/auth' to keep compatibility with your clients.
+                See its description for more information.
+        '')
+      (mkRemovedOptionModule
+        [ "services" "keycloak" "extraConfig" ]
+        "Use `services.keycloak.settings' instead.")
+    ];
+
   options.services.keycloak =
     let
-      inherit (types) bool str nullOr attrsOf path enum anything
-        package port;
+      inherit (types)
+        bool
+        str
+        int
+        nullOr
+        attrsOf
+        oneOf
+        path
+        enum
+        package
+        port;
+
+      assertStringPath = optionName: value:
+        if isPath value then
+          throw ''
+            services.keycloak.${optionName}:
+              ${toString value}
+              is a Nix path, but should be a string, since Nix
+              paths are copied into the world-readable Nix store.
+          ''
+        else value;
     in
     {
       enable = mkOption {
@@ -30,89 +104,14 @@ in
         '';
       };
 
-      bindAddress = mkOption {
-        type = str;
-        default = "\${jboss.bind.address:0.0.0.0}";
-        example = "127.0.0.1";
-        description = ''
-          On which address Keycloak should accept new connections.
-
-          A special syntax can be used to allow command line Java system
-          properties to override the value: ''${property.name:value}
-        '';
-      };
-
-      httpPort = mkOption {
-        type = str;
-        default = "\${jboss.http.port:80}";
-        example = "8080";
-        description = ''
-          On which port Keycloak should listen for new HTTP connections.
-
-          A special syntax can be used to allow command line Java system
-          properties to override the value: ''${property.name:value}
-        '';
-      };
-
-      httpsPort = mkOption {
-        type = str;
-        default = "\${jboss.https.port:443}";
-        example = "8443";
-        description = ''
-          On which port Keycloak should listen for new HTTPS connections.
-
-          A special syntax can be used to allow command line Java system
-          properties to override the value: ''${property.name:value}
-        '';
-      };
-
-      frontendUrl = mkOption {
-        type = str;
-        apply = x:
-          if x == "" || hasSuffix "/" x then
-            x
-          else
-            x + "/";
-        example = "keycloak.example.com/auth";
-        description = ''
-          The public URL used as base for all frontend requests. Should
-          normally include a trailing <literal>/auth</literal>.
-
-          See <link xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
-          Hostname section of the Keycloak server installation
-          manual</link> for more information.
-        '';
-      };
-
-      forceBackendUrlToFrontendUrl = mkOption {
-        type = bool;
-        default = false;
-        example = true;
-        description = ''
-          Whether Keycloak should force all requests to go through the
-          frontend URL configured in <xref
-          linkend="opt-services.keycloak.frontendUrl" />. By default,
-          Keycloak allows backend requests to instead use its local
-          hostname or IP address and may also advertise it to clients
-          through its OpenID Connect Discovery endpoint.
-
-          See <link
-          xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">the
-          Hostname section of the Keycloak server installation
-          manual</link> for more information.
-        '';
-      };
-
       sslCertificate = mkOption {
         type = nullOr path;
         default = null;
         example = "/run/keys/ssl_cert";
+        apply = assertStringPath "sslCertificate";
         description = ''
           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.
         '';
       };
 
@@ -120,28 +119,28 @@ in
         type = nullOr path;
         default = null;
         example = "/run/keys/ssl_key";
+        apply = assertStringPath "sslCertificateKey";
         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.
         '';
       };
 
       plugins = lib.mkOption {
         type = lib.types.listOf lib.types.path;
-        default = [];
+        default = [ ];
         description = ''
-          Keycloak plugin jar, ear files or derivations with them
+          Keycloak plugin jar, ear files or derivations containing
+          them. Packaged plugins are available through
+          <literal>pkgs.keycloak.plugins</literal>.
         '';
       };
 
       database = {
         type = mkOption {
-          type = enum [ "mysql" "postgresql" ];
+          type = enum [ "mysql" "mariadb" "postgresql" ];
           default = "postgresql";
-          example = "mysql";
+          example = "mariadb";
           description = ''
             The type of database Keycloak should connect to.
           '';
@@ -159,6 +158,7 @@ in
           let
             dbPorts = {
               postgresql = 5432;
+              mariadb = 3306;
               mysql = 3306;
             };
           in
@@ -207,6 +207,21 @@ in
           '';
         };
 
+        name = mkOption {
+          type = str;
+          default = "keycloak";
+          description = ''
+            Database name 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.
+          '';
+        };
+
         username = mkOption {
           type = str;
           default = "keycloak";
@@ -218,19 +233,16 @@ in
             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>.
+            manually.
           '';
         };
 
         passwordFile = mkOption {
           type = path;
           example = "/run/keys/db_password";
+          apply = assertStringPath "passwordFile";
           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.
+            The path to a file containing the database password.
           '';
         };
       };
@@ -268,67 +280,181 @@ in
         '';
       };
 
-      extraConfig = mkOption {
-        type = attrsOf anything;
-        default = { };
+      settings = mkOption {
+        type = lib.types.submodule {
+          freeformType = attrsOf (nullOr (oneOf [ str int bool (attrsOf path) ]));
+
+          options = {
+            http-host = mkOption {
+              type = str;
+              default = "0.0.0.0";
+              example = "127.0.0.1";
+              description = ''
+                On which address Keycloak should accept new connections.
+              '';
+            };
+
+            http-port = mkOption {
+              type = port;
+              default = 80;
+              example = 8080;
+              description = ''
+                On which port Keycloak should listen for new HTTP connections.
+              '';
+            };
+
+            https-port = mkOption {
+              type = port;
+              default = 443;
+              example = 8443;
+              description = ''
+                On which port Keycloak should listen for new HTTPS connections.
+              '';
+            };
+
+            http-relative-path = mkOption {
+              type = str;
+              default = "";
+              example = "/auth";
+              description = ''
+                The path relative to <literal>/</literal> for serving
+                resources.
+
+                <note>
+                  <para>
+                    In versions of Keycloak using Wildfly (&lt;17),
+                    this defaulted to <literal>/auth</literal>. If
+                    upgrading from the Wildfly version of Keycloak,
+                    i.e. a NixOS version before 22.05, you'll likely
+                    want to set this to <literal>/auth</literal> to
+                    keep compatibility with your clients.
+
+                    See <link
+                    xlink:href="https://www.keycloak.org/migration/migrating-to-quarkus"
+                    /> for more information on migrating from Wildfly
+                    to Quarkus.
+                  </para>
+                </note>
+              '';
+            };
+
+            hostname = mkOption {
+              type = str;
+              example = "keycloak.example.com";
+              description = ''
+                The hostname part of the public URL used as base for
+                all frontend requests.
+
+                See <link xlink:href="https://www.keycloak.org/server/hostname" />
+                for more information about hostname configuration.
+              '';
+            };
+
+            hostname-strict-backchannel = mkOption {
+              type = bool;
+              default = false;
+              example = true;
+              description = ''
+                Whether Keycloak should force all requests to go
+                through the frontend URL. By default, Keycloak allows
+                backend requests to instead use its local hostname or
+                IP address and may also advertise it to clients
+                through its OpenID Connect Discovery endpoint.
+
+                See <link xlink:href="https://www.keycloak.org/server/hostname" />
+                for more information about hostname configuration.
+              '';
+            };
+
+            proxy = mkOption {
+              type = enum [ "edge" "reencrypt" "passthrough" "none" ];
+              default = "none";
+              example = "edge";
+              description = ''
+                The proxy address forwarding mode if the server is
+                behind a reverse proxy.
+
+                <variablelist>
+                  <varlistentry>
+                    <term>edge</term>
+                    <listitem>
+                      <para>
+                        Enables communication through HTTP between the
+                        proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                  <varlistentry>
+                    <term>reencrypt</term>
+                    <listitem>
+                      <para>
+                        Requires communication through HTTPS between the
+                        proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                  <varlistentry>
+                    <term>passthrough</term>
+                    <listitem>
+                      <para>
+                        Enables communication through HTTP or HTTPS between
+                        the proxy and Keycloak.
+                      </para>
+                    </listitem>
+                  </varlistentry>
+                </variablelist>
+
+                See <link
+                xlink:href="https://www.keycloak.org/server/reverseproxy"
+                /> for more information.
+              '';
+            };
+          };
+        };
+
         example = literalExpression ''
           {
-            "subsystem=keycloak-server" = {
-              "spi=hostname" = {
-                "provider=default" = null;
-                "provider=fixed" = {
-                  enabled = true;
-                  properties.hostname = "keycloak.example.com";
-                };
-                default-provider = "fixed";
-              };
-            };
+            hostname = "keycloak.example.com";
+            proxy = "reencrypt";
+            https-key-store-file = "/path/to/file";
+            https-key-store-password = { _secret = "/run/keys/store_password"; };
           }
         '';
+
         description = ''
-          Additional Keycloak configuration options to set in
-          <literal>standalone.xml</literal>.
-
-          Options are expressed as a Nix attribute set which matches the
-          structure of the jboss-cli configuration. The configuration is
-          effectively overlayed on top of the default configuration
-          shipped with Keycloak. To remove existing nodes and undefine
-          attributes from the default configuration, set them to
-          <literal>null</literal>.
-
-          The example configuration does the equivalent of the following
-          script, which removes the hostname provider
-          <literal>default</literal>, adds the deprecated hostname
-          provider <literal>fixed</literal> and defines it the default:
-
-          <programlisting>
-          /subsystem=keycloak-server/spi=hostname/provider=default:remove()
-          /subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
-          /subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
-          </programlisting>
-
-          You can discover available options by using the <link
-          xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
-          program and by referring to the <link
-          xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
-          Server Installation and Configuration Guide</link>.
+          Configuration options corresponding to parameters set in
+          <filename>conf/keycloak.conf</filename>.
+
+          Most available options are documented at <link
+          xlink:href="https://www.keycloak.org/server/all-config" />.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute <literal>_secret</literal> - a
+          string pointing to a file containing the value the option
+          should be set to. See the example to get a better picture of
+          this: in the resulting
+          <filename>conf/keycloak.conf</filename> file, the
+          <literal>https-key-store-password</literal> key will be set
+          to the contents of the
+          <filename>/run/keys/store_password</filename> file.
         '';
       };
-
     };
 
   config =
     let
-      # We only want to create a database if we're actually going to connect to it.
+      # We only want to create a database if we're actually going to
+      # connect to it.
       databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "localhost";
       createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.database.type == "postgresql";
-      createLocalMySQL = databaseActuallyCreateLocally && cfg.database.type == "mysql";
+      createLocalMySQL = databaseActuallyCreateLocally && elem cfg.database.type [ "mysql" "mariadb" ];
 
       mySqlCaKeystore = pkgs.runCommand "mysql-ca-keystore" { } ''
         ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.database.caCert} -keystore $out -storepass notsosecretpassword -noprompt
       '';
 
-      # Both theme and theme type directories need to be actual directories in one hierarchy to pass Keycloak checks.
+      # Both theme and theme type directories need to be actual
+      # directories in one hierarchy to pass Keycloak checks.
       themesBundle = pkgs.runCommand "keycloak-themes" { } ''
         linkTheme() {
           theme="$1"
@@ -347,7 +473,7 @@ in
         }
 
         mkdir -p "$out"
-        for theme in ${cfg.package}/themes/*; do
+        for theme in ${keycloakBuild}/themes/*; do
           if [ -d "$theme" ]; then
             linkTheme "$theme" "$(basename "$theme")"
           fi
@@ -356,329 +482,25 @@ in
         ${concatStringsSep "\n" (mapAttrsToList (name: theme: "linkTheme ${theme} ${escapeShellArg name}") cfg.themes)}
       '';
 
-      keycloakConfig' = foldl' recursiveUpdate
-        {
-          "interface=public".inet-address = cfg.bindAddress;
-          "socket-binding-group=standard-sockets"."socket-binding=http".port = cfg.httpPort;
-          "subsystem=keycloak-server" = {
-            "spi=hostname"."provider=default" = {
-              enabled = true;
-              properties = {
-                inherit (cfg) frontendUrl forceBackendUrlToFrontendUrl;
-              };
-            };
-            "theme=defaults".dir = toString themesBundle;
-          };
-          "subsystem=datasources"."data-source=KeycloakDS" = {
-            max-pool-size = "20";
-            user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
-            password = "@db-password@";
-          };
-        } [
-        (optionalAttrs (cfg.database.type == "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.database.host}:${toString cfg.database.port}/keycloak";
-              driver-name = "postgresql";
-              "connection-properties=ssl".value = boolToString cfg.database.useSSL;
-            } // (optionalAttrs (cfg.database.caCert != null) {
-              "connection-properties=sslrootcert".value = cfg.database.caCert;
-              "connection-properties=sslmode".value = "verify-ca";
-            });
-          };
-        })
-        (optionalAttrs (cfg.database.type == "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.database.host}:${toString cfg.database.port}/keycloak";
-              driver-name = "mysql";
-              "connection-properties=useSSL".value = boolToString cfg.database.useSSL;
-              "connection-properties=requireSSL".value = boolToString cfg.database.useSSL;
-              "connection-properties=verifyServerCertificate".value = 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";
-            } // (optionalAttrs (cfg.database.caCert != null) {
-              "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
-              "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
-            });
-          };
-        })
-        (optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
-          "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
-          "subsystem=elytron" = mkOrder 900 {
-            "key-store=httpsKS" = mkOrder 900 {
-              path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
-              credential-reference.clear-text = "notsosecretpassword";
-              type = "JKS";
-            };
-            "key-manager=httpsKM" = mkOrder 901 {
-              key-store = "httpsKS";
-              credential-reference.clear-text = "notsosecretpassword";
-            };
-            "server-ssl-context=httpsSSC" = mkOrder 902 {
-              key-manager = "httpsKM";
-            };
-          };
-          "subsystem=undertow" = mkOrder 901 {
-            "server=default-server"."https-listener=https".ssl-context = "httpsSSC";
-          };
-        })
-        cfg.extraConfig
-      ];
-
-
-      /* Produces a JBoss CLI script that creates paths and sets
-         attributes matching those described by `attrs`. When the
-         script is run, the existing settings are effectively overlayed
-         by those from `attrs`. Existing attributes can be unset by
-         defining them `null`.
-
-         JBoss paths and attributes / maps are distinguished by their
-         name, where paths follow a `key=value` scheme.
-
-         Example:
-           mkJbossScript {
-             "subsystem=keycloak-server"."spi=hostname" = {
-               "provider=fixed" = null;
-               "provider=default" = {
-                 enabled = true;
-                 properties = {
-                   inherit frontendUrl;
-                   forceBackendUrlToFrontendUrl = false;
-                 };
-               };
-             };
-           }
-           => ''
-             if (outcome != success) of /:read-resource()
-                 /:add()
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server:read-resource()
-                 /subsystem=keycloak-server:add()
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server/spi=hostname:read-resource()
-                 /subsystem=keycloak-server/spi=hostname:add()
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=default:read-resource()
-                 /subsystem=keycloak-server/spi=hostname/provider=default:add(enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" })
-             end-if
-             if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
-               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
-             end-if
-             if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
-               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
-             end-if
-             if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
-               /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
-             end-if
-             if (outcome != success) of /subsystem=keycloak-server/spi=hostname/provider=fixed:read-resource()
-                 /subsystem=keycloak-server/spi=hostname/provider=fixed:remove()
-             end-if
-           ''
-      */
-      mkJbossScript = attrs:
-        let
-          /* From a JBoss path and an attrset, produces a JBoss CLI
-             snippet that writes the corresponding attributes starting
-             at `path`. Recurses down into subattrsets as necessary,
-             producing the variable name from its full path in the
-             attrset.
-
-             Example:
-               writeAttributes "/subsystem=keycloak-server/spi=hostname/provider=default" {
-                 enabled = true;
-                 properties = {
-                   forceBackendUrlToFrontendUrl = false;
-                   frontendUrl = "https://keycloak.example.com/auth";
-                 };
-               }
-               => ''
-                 if (result != true) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="enabled")
-                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=enabled, value=true)
-                 end-if
-                 if (result != false) of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.forceBackendUrlToFrontendUrl")
-                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=false)
-                 end-if
-                 if (result != "https://keycloak.example.com/auth") of /subsystem=keycloak-server/spi=hostname/provider=default:read-attribute(name="properties.frontendUrl")
-                   /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.frontendUrl, value="https://keycloak.example.com/auth")
-                 end-if
-               ''
-          */
-          writeAttributes = path: set:
-            let
-              # JBoss expressions like `${var}` need to be prefixed
-              # with `expression` to evaluate.
-              prefixExpression = string:
-                let
-                  matchResult = match ''"\$\{.*}"'' string;
-                in
-                if matchResult != null then
-                  "expression " + string
-                else
-                  string;
-
-              writeAttribute = attribute: value:
-                let
-                  type = typeOf value;
-                in
-                if type == "set" then
-                  let
-                    names = attrNames value;
-                  in
-                  foldl' (text: name: text + (writeAttribute "${attribute}.${name}" value.${name})) "" names
-                else if value == null then ''
-                  if (outcome == success) of ${path}:read-attribute(name="${attribute}")
-                      ${path}:undefine-attribute(name="${attribute}")
-                  end-if
-                ''
-                else if elem type [ "string" "path" "bool" ] then
-                  let
-                    value' = if type == "bool" then boolToString value else ''"${value}"'';
-                  in
-                  ''
-                    if (result != ${prefixExpression value'}) of ${path}:read-attribute(name="${attribute}")
-                      ${path}:write-attribute(name=${attribute}, value=${value'})
-                    end-if
-                  ''
-                else throw "Unsupported type '${type}' for path '${path}'!";
-            in
-            concatStrings
-              (mapAttrsToList
-                (attribute: value: (writeAttribute attribute value))
-                set);
-
-
-          /* Produces an argument list for the JBoss `add()` function,
-             which adds a JBoss path and takes as its arguments the
-             required subpaths and attributes.
-
-             Example:
-               makeArgList {
-                 enabled = true;
-                 properties = {
-                   forceBackendUrlToFrontendUrl = false;
-                   frontendUrl = "https://keycloak.example.com/auth";
-                 };
-               }
-               => ''
-                 enabled = true, properties = { forceBackendUrlToFrontendUrl = false, frontendUrl = "https://keycloak.example.com/auth" }
-               ''
-          */
-          makeArgList = set:
-            let
-              makeArg = attribute: value:
-                let
-                  type = typeOf value;
-                in
-                if type == "set" then
-                  "${attribute} = { " + (makeArgList value) + " }"
-                else if elem type [ "string" "path" "bool" ] then
-                  "${attribute} = ${if type == "bool" then boolToString value else ''"${value}"''}"
-                else if value == null then
-                  ""
-                else
-                  throw "Unsupported type '${type}' for attribute '${attribute}'!";
-
-            in
-            concatStringsSep ", " (mapAttrsToList makeArg set);
-
-
-          /* Recurses into the `nodeValue` attrset. Only subattrsets that
-             are JBoss paths, i.e. follows the `key=value` format, are recursed
-             into - the rest are considered JBoss attributes / maps.
-          */
-          recurse = nodePath: nodeValue:
-            let
-              nodeContent =
-                if isAttrs nodeValue && nodeValue._type or "" == "order" then
-                  nodeValue.content
-                else
-                  nodeValue;
-              isPath = name:
-                let
-                  value = nodeContent.${name};
-                in
-                if (match ".*([=]).*" name) == [ "=" ] then
-                  if isAttrs value || value == null then
-                    true
-                  else
-                    throw "Parsing path '${concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
-                else
-                  false;
-              jbossPath = "/" + concatStringsSep "/" nodePath;
-              children = if !isAttrs nodeContent then { } else nodeContent;
-              subPaths = filter isPath (attrNames children);
-              getPriority = name:
-                let
-                  value = children.${name};
-                in
-                if value._type or "" == "order" then value.priority else 1000;
-              orderedSubPaths = sort (a: b: getPriority a < getPriority b) subPaths;
-              jbossAttrs = filterAttrs (name: _: !(isPath name)) children;
-              text =
-                if nodeContent != null then
-                  ''
-                    if (outcome != success) of ${jbossPath}:read-resource()
-                        ${jbossPath}:add(${makeArgList jbossAttrs})
-                    end-if
-                  '' + writeAttributes jbossPath jbossAttrs
-                else
-                  ''
-                    if (outcome == success) of ${jbossPath}:read-resource()
-                        ${jbossPath}:remove()
-                    end-if
-                  '';
-            in
-            text + concatMapStringsSep "\n" (name: recurse (nodePath ++ [ name ]) children.${name}) orderedSubPaths;
-        in
-        recurse [ ] attrs;
-
-      jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig');
-
-      keycloakConfig = pkgs.runCommand "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";
-
-          cp -r ${cfg.package}/standalone/configuration .
-          chmod -R u+rwX ./configuration
-
-          mkdir -p {deployments,ssl}
-
-          standalone.sh&
-
-          attempt=1
-          max_attempts=30
-          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
-              fi
-              echo "Keycloak not fully started yet, retrying.. ($attempt/$max_attempts)"
-              sleep 1
-              (( attempt++ ))
-          done
-
-          jboss-cli.sh --connect --file=${jbossCliScript} --echo-command
+      keycloakConfig = lib.generators.toKeyValue {
+        mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
+          mkValueString = v: with builtins;
+            if isInt v then toString v
+            else if isString v then v
+            else if true == v then "true"
+            else if false == v then "false"
+            else if isSecret v then hashString "sha256" v._secret
+            else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
+        };
+      };
 
-          cp configuration/standalone.xml $out
-        '';
+      isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+      filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{ } null])) cfg.settings;
+      confFile = pkgs.writeText "keycloak.conf" (keycloakConfig filteredConfig);
+      keycloakBuild = cfg.package.override {
+        inherit confFile;
+        plugins = cfg.package.enabledPlugins ++ cfg.plugins;
+      };
     in
     mkIf cfg.enable
       {
@@ -689,7 +511,45 @@ in
           }
         ];
 
-        environment.systemPackages = [ cfg.package ];
+        environment.systemPackages = [ keycloakBuild ];
+
+        services.keycloak.settings =
+          let
+            postgresParams = concatStringsSep "&" (
+              optionals cfg.database.useSSL [
+                "ssl=true"
+              ] ++ optionals (cfg.database.caCert != null) [
+                "sslrootcert=${cfg.database.caCert}"
+                "sslmode=verify-ca"
+              ]
+            );
+            mariadbParams = concatStringsSep "&" ([
+              "characterEncoding=UTF-8"
+            ] ++ optionals cfg.database.useSSL [
+              "useSSL=true"
+              "requireSSL=true"
+              "verifyServerCertificate=true"
+            ] ++ optionals (cfg.database.caCert != null) [
+              "trustCertificateKeyStoreUrl=file:${mySqlCaKeystore}"
+              "trustCertificateKeyStorePassword=notsosecretpassword"
+            ]);
+            dbProps = if cfg.database.type == "postgresql" then postgresParams else mariadbParams;
+          in
+          mkMerge [
+            {
+              db = if cfg.database.type == "postgresql" then "postgres" else cfg.database.type;
+              db-username = if databaseActuallyCreateLocally then "keycloak" else cfg.database.username;
+              db-password._secret = cfg.database.passwordFile;
+              db-url-host = "${cfg.database.host}:${toString cfg.database.port}";
+              db-url-database = if databaseActuallyCreateLocally then "keycloak" else cfg.database.name;
+              db-url-properties = prefixUnlessEmpty "?" dbProps;
+              db-url = null;
+            }
+            (mkIf (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
+              https-certificate-file = "/run/keycloak/ssl/ssl_cert";
+              https-certificate-key-file = "/run/keycloak/ssl/ssl_key";
+            })
+          ];
 
         systemd.services.keycloakPostgreSQLInit = mkIf createLocalPostgreSQL {
           after = [ "postgresql.service" ];
@@ -752,41 +612,37 @@ in
                 "mysql.service"
               ]
               else [ ];
+            secretPaths = catAttrs "_secret" (collect isSecret cfg.settings);
+            mkSecretReplacement = file: ''
+              replace-secret ${hashString "sha256" file} $CREDENTIALS_DIRECTORY/${baseNameOf file} /run/keycloak/conf/keycloak.conf
+            '';
+            secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
           in
           {
             after = databaseServices;
             bindsTo = databaseServices;
             wantedBy = [ "multi-user.target" ];
             path = with pkgs; [
-              cfg.package
+              keycloakBuild
               openssl
               replace-secret
             ];
             environment = {
-              JBOSS_LOG_DIR = "/var/log/keycloak";
-              JBOSS_BASE_DIR = "/run/keycloak";
-              JBOSS_MODULEPATH = "${cfg.package}/modules";
+              KC_HOME_DIR = "/run/keycloak";
+              KC_CONF_DIR = "/run/keycloak/conf";
             };
             serviceConfig = {
-              LoadCredential = [
-                "db_password:${cfg.database.passwordFile}"
-              ] ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
-                "ssl_cert:${cfg.sslCertificate}"
-                "ssl_key:${cfg.sslCertificateKey}"
-              ];
+              LoadCredential =
+                map (p: "${baseNameOf p}:${p}") secretPaths
+                ++ optionals (cfg.sslCertificate != null && cfg.sslCertificateKey != null) [
+                  "ssl_cert:${cfg.sslCertificate}"
+                  "ssl_key:${cfg.sslCertificateKey}"
+                ];
               User = "keycloak";
               Group = "keycloak";
               DynamicUser = true;
-              RuntimeDirectory = map (p: "keycloak/" + p) [
-                "configuration"
-                "deployments"
-                "data"
-                "ssl"
-                "log"
-                "tmp"
-              ];
+              RuntimeDirectory = "keycloak";
               RuntimeDirectoryMode = 0700;
-              LogsDirectory = "keycloak";
               AmbientCapabilities = "CAP_NET_BIND_SERVICE";
             };
             script = ''
@@ -795,41 +651,30 @@ in
 
               umask u=rwx,g=,o=
 
-              install_plugin() {
-                if [ -d "$1" ]; then
-                  find "$1" -type f \( -iname \*.ear -o -iname \*.jar \) -exec install -m 0500 -o keycloak -g keycloak "{}" "/run/keycloak/deployments/" \;
-                else
-                  install -m 0500 -o keycloak -g keycloak "$1" "/run/keycloak/deployments/"
-                fi
-              }
-
-              install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
-              install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
-
-              replace-secret '@db-password@' "$CREDENTIALS_DIRECTORY/db_password" /run/keycloak/configuration/standalone.xml
-
-              export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration
-              add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}'
-            ''
-            + lib.optionalString (cfg.plugins != []) (lib.concatStringsSep "\n" (map (pl: "install_plugin ${lib.escapeShellArg pl}") cfg.plugins)) + "\n"
-            + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
-              pushd /run/keycloak/ssl/
-              cat "$CREDENTIALS_DIRECTORY/ssl_cert" <(echo) \
-                  "$CREDENTIALS_DIRECTORY/ssl_key" <(echo) \
-                  /etc/ssl/certs/ca-certificates.crt \
-                  > allcerts.pem
-              openssl pkcs12 -export -in "$CREDENTIALS_DIRECTORY/ssl_cert" -inkey "$CREDENTIALS_DIRECTORY/ssl_key" -chain \
-                             -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12 \
-                             -CAfile allcerts.pem -passout pass:notsosecretpassword
-              popd
+              ln -s ${themesBundle} /run/keycloak/themes
+              ln -s ${keycloakBuild}/providers /run/keycloak/
+
+              install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf
+
+              ${secretReplacements}
+
+            '' + optionalString (cfg.sslCertificate != null && cfg.sslCertificateKey != null) ''
+              mkdir -p /run/keycloak/ssl
+              cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
             '' + ''
-              ${cfg.package}/bin/standalone.sh
+              export KEYCLOAK_ADMIN=admin
+              export KEYCLOAK_ADMIN_PASSWORD=${cfg.initialAdminPassword}
+              kc.sh start
             '';
           };
 
         services.postgresql.enable = mkDefault createLocalPostgreSQL;
         services.mysql.enable = mkDefault createLocalMySQL;
-        services.mysql.package = mkIf createLocalMySQL pkgs.mariadb;
+        services.mysql.package =
+          let
+            dbPkg = if cfg.database.type == "mariadb" then pkgs.mariadb else pkgs.mysql80;
+          in
+          mkIf createLocalMySQL (mkDefault dbPkg);
       };
 
   meta.doc = ./keycloak.xml;
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
index cb706932f48..861756e33ac 100644
--- a/nixos/modules/services/web-apps/keycloak.xml
+++ b/nixos/modules/services/web-apps/keycloak.xml
@@ -27,10 +27,10 @@
 
      <para>
        Refer to the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html#admin-console">Admin
-       Console section of the Keycloak Server Administration Guide</link> for
-       information on how to administer your
-       <productname>Keycloak</productname> instance.
+       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">
+       Keycloak Server Administration Guide</link> for information on
+       how to administer your <productname>Keycloak</productname>
+       instance.
      </para>
    </section>
 
@@ -38,27 +38,28 @@
      <title>Database access</title>
      <para>
        <productname>Keycloak</productname> can be used with either
-       <productname>PostgreSQL</productname> or
+       <productname>PostgreSQL</productname>,
+       <productname>MariaDB</productname> or
        <productname>MySQL</productname>. Which one is used can be
        configured in <xref
        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.database.host" /> is changed from
-       its default of <literal>localhost</literal> or <xref
-       linkend="opt-services.keycloak.database.createLocally" /> is set
-       to <literal>false</literal>.
+       linkend="opt-services.keycloak.database.host" /> is changed
+       from its default of <literal>localhost</literal> or <xref
+       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.database.host" />, <xref
+       linkend="opt-services.keycloak.database.name" />, <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.
+       appropriate. Note that you need to manually create the database
+       and allow the configured database user full access to it.
      </para>
 
      <para>
@@ -79,22 +80,27 @@
      </warning>
    </section>
 
-   <section xml:id="module-services-keycloak-frontendurl">
-     <title>Frontend URL</title>
+   <section xml:id="module-services-keycloak-hostname">
+     <title>Hostname</title>
      <para>
-       The frontend URL is used as base for all frontend requests and
-       must be configured through <xref linkend="opt-services.keycloak.frontendUrl" />.
-       It should normally include a trailing <literal>/auth</literal>
-       (the default web context). If you use a reverse proxy, you need
-       to set this option to <literal>""</literal>, so that frontend URL
-       is derived from HTTP headers. <literal>X-Forwarded-*</literal> headers
-       support also should be enabled, using <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html#identifying-client-ip-addresses">
-       respective guidelines</link>.
+       The hostname is used to build the public URL used as base for
+       all frontend requests and must be configured through <xref
+       linkend="opt-services.keycloak.settings.hostname" />.
      </para>
 
+     <note>
+       <para>
+         If you're migrating an old Wildfly based Keycloak instance
+         and want to keep compatibility with your current clients,
+         you'll likely want to set <xref
+         linkend="opt-services.keycloak.settings.http-relative-path"
+         /> to <literal>/auth</literal>. See the option description
+         for more details.
+       </para>
+     </note>
+
      <para>
-       <xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" />
+       <xref linkend="opt-services.keycloak.settings.hostname-strict-backchannel" />
        determines whether Keycloak should force all requests to go
        through the frontend URL. By default,
        <productname>Keycloak</productname> allows backend requests to
@@ -104,10 +110,10 @@
      </para>
 
      <para>
-       See the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_installation/#_hostname">Hostname
-       section of the Keycloak Server Installation and Configuration
-       Guide</link> for more information.
+        For more information on hostname configuration, see the <link
+        xlink:href="https://www.keycloak.org/server/hostname">Hostname
+        section of the Keycloak Server Installation and Configuration
+        Guide</link>.
      </para>
    </section>
 
@@ -139,68 +145,40 @@
    <section xml:id="module-services-keycloak-themes">
      <title>Themes</title>
      <para>
-        You can package custom themes and make them visible to Keycloak via
-        <xref linkend="opt-services.keycloak.themes" />
-        option. See the <link xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
+        You can package custom themes and make them visible to
+        Keycloak through <xref linkend="opt-services.keycloak.themes"
+        />. See the <link
+        xlink:href="https://www.keycloak.org/docs/latest/server_development/#_themes">
         Themes section of the Keycloak Server Development Guide</link>
-        and respective NixOS option description for more information.
+        and the description of the aforementioned NixOS option for
+        more information.
      </para>
    </section>
 
-   <section xml:id="module-services-keycloak-extra-config">
-     <title>Additional configuration</title>
+   <section xml:id="module-services-keycloak-settings">
+     <title>Configuration file settings</title>
      <para>
-       Additional Keycloak configuration options, for which no
-       explicit <productname>NixOS</productname> options are provided,
-       can be set in <xref linkend="opt-services.keycloak.extraConfig" />.
+       Keycloak server configuration parameters can be set in <xref
+       linkend="opt-services.keycloak.settings" />. These correspond
+       directly to options in
+       <filename>conf/keycloak.conf</filename>. Some of the most
+       important parameters are documented as suboptions, the rest can
+       be found in the <link
+       xlink:href="https://www.keycloak.org/server/all-config">All
+       configuration section of the Keycloak Server Installation and
+       Configuration Guide</link>.
      </para>
 
      <para>
-       Options are expressed as a Nix attribute set which matches the
-       structure of the jboss-cli configuration. The configuration is
-       effectively overlayed on top of the default configuration
-       shipped with Keycloak. To remove existing nodes and undefine
-       attributes from the default configuration, set them to
-       <literal>null</literal>.
-     </para>
-     <para>
-       For example, the following script, which removes the hostname
-       provider <literal>default</literal>, adds the deprecated
-       hostname provider <literal>fixed</literal> and defines it the
-       default:
-
-<programlisting>
-/subsystem=keycloak-server/spi=hostname/provider=default:remove()
-/subsystem=keycloak-server/spi=hostname/provider=fixed:add(enabled = true, properties = { hostname = "keycloak.example.com" })
-/subsystem=keycloak-server/spi=hostname:write-attribute(name=default-provider, value="fixed")
-</programlisting>
-
-       would be expressed as
-
-<programlisting>
-services.keycloak.extraConfig = {
-  "subsystem=keycloak-server" = {
-    "spi=hostname" = {
-      "provider=default" = null;
-      "provider=fixed" = {
-        enabled = true;
-        properties.hostname = "keycloak.example.com";
-      };
-      default-provider = "fixed";
-    };
-  };
-};
-</programlisting>
-     </para>
-     <para>
-       You can discover available options by using the <link
-       xlink:href="http://docs.wildfly.org/21/Admin_Guide.html#Command_Line_Interface">jboss-cli.sh</link>
-       program and by referring to the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_installation/index.html">Keycloak
-       Server Installation and Configuration Guide</link>.
+       Options containing secret data should be set to an attribute
+       set containing the attribute <literal>_secret</literal> - a
+       string pointing to a file containing the value the option
+       should be set to. See the description of <xref
+       linkend="opt-services.keycloak.settings" /> for an example.
      </para>
    </section>
 
+
    <section xml:id="module-services-keycloak-example-config">
      <title>Example configuration</title>
      <para>
@@ -208,9 +186,11 @@ services.keycloak.extraConfig = {
 <programlisting>
 services.keycloak = {
   <link linkend="opt-services.keycloak.enable">enable</link> = true;
+  settings = {
+    <link linkend="opt-services.keycloak.settings.hostname">hostname</link> = "keycloak.example.com";
+    <link linkend="opt-services.keycloak.settings.hostname-strict-backchannel">hostname-strict-backchannel</link> = true;
+  };
   <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.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";
diff --git a/nixos/tests/keycloak.nix b/nixos/tests/keycloak.nix
index 267216a5e5a..6ce136330d4 100644
--- a/nixos/tests/keycloak.nix
+++ b/nixos/tests/keycloak.nix
@@ -4,7 +4,7 @@
 
 let
   certs = import ./common/acme/server/snakeoil-certs.nix;
-  frontendUrl = "https://${certs.domain}/auth";
+  frontendUrl = "https://${certs.domain}";
   initialAdminPassword = "h4IhoJFnt2iQIR9";
 
   keycloakTest = import ./make-test-python.nix (
@@ -27,20 +27,23 @@ let
 
           services.keycloak = {
             enable = true;
-            inherit frontendUrl initialAdminPassword;
-            sslCertificate = certs.${certs.domain}.cert;
-            sslCertificateKey = certs.${certs.domain}.key;
+            settings = {
+              hostname = certs.domain;
+            };
+            inherit initialAdminPassword;
+            sslCertificate = "${certs.${certs.domain}.cert}";
+            sslCertificateKey = "${certs.${certs.domain}.key}";
             database = {
               type = databaseType;
               username = "bogus";
-              passwordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH";
+              name = "also bogus";
+              passwordFile = "${pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"}";
             };
             plugins = with config.services.keycloak.package.plugins; [
               keycloak-discord
               keycloak-metrics-spi
             ];
           };
-
           environment.systemPackages = with pkgs; [
             xmlstarlet
             html-tidy
@@ -99,9 +102,9 @@ let
         in ''
           keycloak.start()
           keycloak.wait_for_unit("keycloak.service")
+          keycloak.wait_for_open_port(443)
           keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
 
-
           ### Realm Setup ###
 
           # Get an admin interface access token
@@ -117,8 +120,8 @@ let
           # Register the metrics SPI
           keycloak.succeed(
               "${pkgs.jre}/bin/keytool -import -alias snakeoil -file ${certs.ca.cert} -storepass aaaaaa -keystore cacert.jks -noprompt",
-              "KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' ${pkgs.keycloak}/bin/kcadm.sh config credentials --server '${frontendUrl}' --realm master --user admin --password '${initialAdminPassword}'",
-              "KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' ${pkgs.keycloak}/bin/kcadm.sh update events/config -s 'eventsEnabled=true' -s 'adminEventsEnabled=true' -s 'eventsListeners+=metrics-listener'",
+              "KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' kcadm.sh config credentials --server '${frontendUrl}' --realm master --user admin --password '${initialAdminPassword}'",
+              "KC_OPTS='-Djavax.net.ssl.trustStore=cacert.jks -Djavax.net.ssl.trustStorePassword=aaaaaa' kcadm.sh update events/config -s 'eventsEnabled=true' -s 'adminEventsEnabled=true' -s 'eventsListeners+=metrics-listener'",
               "curl -sSf '${frontendUrl}/realms/master/metrics' | grep '^keycloak_admin_event_UPDATE'"
           )
 
@@ -172,5 +175,6 @@ let
 in
 {
   postgres = keycloakTest { databaseType = "postgresql"; };
+  mariadb = keycloakTest { databaseType = "mariadb"; };
   mysql = keycloakTest { databaseType = "mysql"; };
 }
diff --git a/pkgs/servers/keycloak/default.nix b/pkgs/servers/keycloak/default.nix
index 6f7723eb344..f28679f2cf5 100644
--- a/pkgs/servers/keycloak/default.nix
+++ b/pkgs/servers/keycloak/default.nix
@@ -1,73 +1,81 @@
-{ stdenv, lib, fetchzip, makeWrapper, jre, writeText, nixosTests
-, postgresql_jdbc ? null, mysql_jdbc ? null
+{ stdenv
+, lib
+, fetchzip
+, makeWrapper
+, jre
+, writeText
+, nixosTests
 , callPackage
+
+, confFile ? null
+, plugins ? [ ]
 }:
 
-let
-  mkModuleXml = name: jarFile: writeText "module.xml" ''
-    <?xml version="1.0" ?>
-    <module xmlns="urn:jboss:module:1.3" name="${name}">
-        <resources>
-            <resource-root path="${jarFile}"/>
-        </resources>
-        <dependencies>
-            <module name="javax.api"/>
-            <module name="javax.transaction.api"/>
-        </dependencies>
-    </module>
-  '';
-in
 stdenv.mkDerivation rec {
-  pname   = "keycloak";
+  pname = "keycloak";
   version = "17.0.1";
 
   src = fetchzip {
-    url    = "https://github.com/keycloak/keycloak/releases/download/${version}/keycloak-legacy-${version}.zip";
-    sha256 = "sha256-oqANNk7T6+CAS818v3I1QNsuxetL/JFZMqxouRn+kdE=";
+    url = "https://github.com/keycloak/keycloak/releases/download/${version}/keycloak-${version}.zip";
+    sha256 = "sha256-z1LfTUoK+v4oQxdyIQruFhl5O333zirSrkPoTFgVfmI=";
   };
 
-  nativeBuildInputs = [ makeWrapper ];
+  nativeBuildInputs = [ makeWrapper jre ];
+
+  buildPhase = ''
+    runHook preBuild
+  '' + lib.optionalString (confFile != null) ''
+    install -m 0600 ${confFile} conf/keycloak.conf
+  '' + ''
+    install_plugin() {
+    if [ -d "$1" ]; then
+      find "$1" -type f \( -iname \*.ear -o -iname \*.jar \) -exec install -m 0500 "{}" "providers/" \;
+    else
+      install -m 0500 "$1" "providers/"
+    fi
+    }
+    ${lib.concatMapStringsSep "\n" (pl: "install_plugin ${lib.escapeShellArg pl}") plugins}
+  '' + ''
+    export KC_HOME_DIR=$out
+    export KC_CONF_DIR=$out/conf
+
+    patchShebangs bin/kc.sh
+    bin/kc.sh build
+
+    runHook postBuild
+  '';
 
   installPhase = ''
+    runHook preInstall
+
     mkdir $out
     cp -r * $out
 
-    rm -rf $out/bin/*.{ps1,bat}
+    rm $out/bin/*.{ps1,bat}
 
-    module_path=$out/modules/system/layers/keycloak
-    if ! [[ -d $module_path ]]; then
-        echo "The module path $module_path not found!"
-        exit 1
-    fi
+    runHook postInstall
+  '';
+
+  postFixup = ''
+    substituteInPlace $out/bin/kc.sh --replace '-Dkc.home.dir=$DIRNAME/../' '-Dkc.home.dir=$KC_HOME_DIR'
+    substituteInPlace $out/bin/kc.sh --replace '-Djboss.server.config.dir=$DIRNAME/../conf' '-Djboss.server.config.dir=$KC_CONF_DIR'
 
-    ${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
-    ''}
-
-    for script in add-user-keycloak.sh add-user.sh domain.sh elytron-tool.sh jboss-cli.sh jconsole.sh jdr.sh standalone.sh wsconsume.sh wsprovide.sh; do
-      wrapProgram $out/bin/$script --set JAVA_HOME ${jre}
+    for script in $(find $out/bin -type f -executable); do
+      wrapProgram "$script" --set JAVA_HOME ${jre} --prefix PATH : ${jre}/bin
     done
-    wrapProgram $out/bin/kcadm.sh --prefix PATH : ${jre}/bin
-    wrapProgram $out/bin/kcreg.sh --prefix PATH : ${jre}/bin
   '';
 
   passthru = {
     tests = nixosTests.keycloak;
-    plugins = callPackage ./all-plugins.nix {};
+    plugins = callPackage ./all-plugins.nix { };
+    enabledPlugins = plugins;
   };
 
   meta = with lib; {
-    homepage    = "https://www.keycloak.org/";
+    homepage = "https://www.keycloak.org/";
     description = "Identity and access management for modern applications and services";
-    license     = licenses.asl20;
-    platforms   = jre.meta.platforms;
+    license = licenses.asl20;
+    platforms = jre.meta.platforms;
     maintainers = with maintainers; [ ngerstle talyz ];
   };