Merge pull request #167378 from talyz/keycloak-quarkus

keycloak: Switch to the new Quarkus version of Keycloak

Changed files
+679 -642
nixos
doc
manual
from_md
release-notes
release-notes
modules
services
tests
pkgs
servers
keycloak
+125
nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
···
</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.
+75
nixos/doc/manual/release-notes/rl-2205.section.md
···
`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.
+353 -508
nixos/modules/services/web-apps/keycloak.nix
···
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 (lib)
+
types
+
mkMerge
+
mkOption
+
mkChangedOptionModule
+
mkRenamedOptionModule
+
mkRemovedOptionModule
+
concatStringsSep
+
mapAttrsToList
+
escapeShellArg
+
mkIf
+
optionalString
+
optionals
+
mkDefault
+
literalExpression
+
isAttrs
+
literalDocBook
+
maintainers
+
catAttrs
+
collect
+
splitString
+
;
-
inherit (builtins) match typeOf;
+
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 {
···
'';
};
-
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.
'';
};
···
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.
'';
···
let
dbPorts = {
postgresql = 5432;
+
mariadb = 3306;
mysql = 3306;
};
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";
···
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.
'';
};
};
···
'';
};
-
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>.
+
Configuration options corresponding to parameters set in
+
<filename>conf/keycloak.conf</filename>.
-
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>.
+
Most available options are documented at <link
+
xlink:href="https://www.keycloak.org/server/all-config" />.
-
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>.
+
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"
···
}
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
···
${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
-
];
+
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}";
+
};
+
};
-
-
/* 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
-
-
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
{
···
}
];
-
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" ];
···
"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 = ''
···
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
-
}
+
ln -s ${themesBundle} /run/keycloak/themes
+
ln -s ${keycloakBuild}/providers /run/keycloak/
-
install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
-
install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
+
install -D -m 0600 ${confFile} /run/keycloak/conf/keycloak.conf
-
replace-secret '@db-password@' "$CREDENTIALS_DIRECTORY/db_password" /run/keycloak/configuration/standalone.xml
+
${secretReplacements}
-
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
+
'' + 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;
+61 -81
nixos/modules/services/web-apps/keycloak.xml
···
<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>
···
<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>
···
</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
···
</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>
···
<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>.
+
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>
-
<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:
+
</section>
-
<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>.
-
</para>
-
</section>
<section xml:id="module-services-keycloak-example-config">
<title>Example configuration</title>
···
<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";
+13 -9
nixos/tests/keycloak.nix
···
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 (
···
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
···
in ''
keycloak.start()
keycloak.wait_for_unit("keycloak.service")
+
keycloak.wait_for_open_port(443)
keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}")
-
### Realm Setup ###
···
# 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'"
)
···
in
{
postgres = keycloakTest { databaseType = "postgresql"; };
+
mariadb = keycloakTest { databaseType = "mariadb"; };
mysql = keycloakTest { databaseType = "mysql"; };
}
+52 -44
pkgs/servers/keycloak/default.nix
···
-
{ 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
+
'';
-
${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
-
''}
+
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'
-
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 ];
};