forked from mirrors/nixpkgs
5010f4fff9
Use systemd's LoadCredential mechanism to make the secret files available to the service. This gets rid of the privileged part of the ExecPreStart script which only served to copy these files and assign the correct permissions. There's been issues with this approach when used in combination with DynamicUser, where sometimes the user isn't created before the ExecPreStart script runs, causing the error install: invalid user ‘keycloak’ This should fix that issue. Unfortunately, all of the ExecPreStart script had to be moved to ExecStart, since credentials aren't provided to ExecPreStart. See https://github.com/systemd/systemd/issues/19604.
795 lines
32 KiB
Nix
795 lines
32 KiB
Nix
{ config, options, pkgs, lib, ... }:
|
|
|
|
let
|
|
cfg = config.services.keycloak;
|
|
opt = options.services.keycloak;
|
|
in
|
|
{
|
|
options.services.keycloak = {
|
|
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = ''
|
|
Whether to enable the Keycloak identity and access management
|
|
server.
|
|
'';
|
|
};
|
|
|
|
bindAddress = lib.mkOption {
|
|
type = lib.types.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 = lib.mkOption {
|
|
type = lib.types.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 = lib.mkOption {
|
|
type = lib.types.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 = lib.mkOption {
|
|
type = lib.types.str;
|
|
apply = x:
|
|
if x == "" || lib.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 = lib.mkOption {
|
|
type = lib.types.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 = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = "/run/keys/ssl_cert";
|
|
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.
|
|
'';
|
|
};
|
|
|
|
sslCertificateKey = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
example = "/run/keys/ssl_key";
|
|
description = ''
|
|
The path to a PEM formatted private key to use for TLS/SSL
|
|
connections.
|
|
|
|
This should be a string, not a Nix path, since Nix paths are
|
|
copied into the world-readable Nix store.
|
|
'';
|
|
};
|
|
|
|
database = {
|
|
type = lib.mkOption {
|
|
type = lib.types.enum [ "mysql" "postgresql" ];
|
|
default = "postgresql";
|
|
example = "mysql";
|
|
description = ''
|
|
The type of database Keycloak should connect to.
|
|
'';
|
|
};
|
|
|
|
host = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "localhost";
|
|
description = ''
|
|
Hostname of the database to connect to.
|
|
'';
|
|
};
|
|
|
|
port =
|
|
let
|
|
dbPorts = {
|
|
postgresql = 5432;
|
|
mysql = 3306;
|
|
};
|
|
in
|
|
lib.mkOption {
|
|
type = lib.types.port;
|
|
default = dbPorts.${cfg.database.type};
|
|
defaultText = lib.literalDocBook "default port of selected database";
|
|
description = ''
|
|
Port of the database to connect to.
|
|
'';
|
|
};
|
|
|
|
useSSL = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = cfg.database.host != "localhost";
|
|
defaultText = lib.literalExpression ''config.${opt.database.host} != "localhost"'';
|
|
description = ''
|
|
Whether the database connection should be secured by SSL /
|
|
TLS.
|
|
'';
|
|
};
|
|
|
|
caCert = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
The SSL / TLS CA certificate that verifies the identity of the
|
|
database server.
|
|
|
|
Required when PostgreSQL is used and SSL is turned on.
|
|
|
|
For MySQL, if left at <literal>null</literal>, the default
|
|
Java keystore is used, which should suffice if the server
|
|
certificate is issued by an official CA.
|
|
'';
|
|
};
|
|
|
|
createLocally = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether a database should be automatically created on the
|
|
local host. Set this to false if you plan on provisioning a
|
|
local database yourself. This has no effect if
|
|
services.keycloak.database.host is customized.
|
|
'';
|
|
};
|
|
|
|
username = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "keycloak";
|
|
description = ''
|
|
Username to use when connecting to an external or manually
|
|
provisioned database; has no effect when a local database is
|
|
automatically provisioned.
|
|
|
|
To use this with a local database, set <xref
|
|
linkend="opt-services.keycloak.database.createLocally" /> to
|
|
<literal>false</literal> and create the database and user
|
|
manually. The database should be called
|
|
<literal>keycloak</literal>.
|
|
'';
|
|
};
|
|
|
|
passwordFile = lib.mkOption {
|
|
type = lib.types.path;
|
|
example = "/run/keys/db_password";
|
|
description = ''
|
|
File containing the database password.
|
|
|
|
This should be a string, not a Nix path, since Nix paths are
|
|
copied into the world-readable Nix store.
|
|
'';
|
|
};
|
|
};
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = pkgs.keycloak;
|
|
defaultText = lib.literalExpression "pkgs.keycloak";
|
|
description = ''
|
|
Keycloak package to use.
|
|
'';
|
|
};
|
|
|
|
initialAdminPassword = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "changeme";
|
|
description = ''
|
|
Initial password set for the <literal>admin</literal>
|
|
user. The password is not stored safely and should be changed
|
|
immediately in the admin panel.
|
|
'';
|
|
};
|
|
|
|
themes = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.package;
|
|
default = {};
|
|
description = ''
|
|
Additional theme packages for Keycloak. Each theme is linked into
|
|
subdirectory with a corresponding attribute name.
|
|
|
|
Theme packages consist of several subdirectories which provide
|
|
different theme types: for example, <literal>account</literal>,
|
|
<literal>login</literal> etc. After adding a theme to this option you
|
|
can select it by its name in Keycloak administration console.
|
|
'';
|
|
};
|
|
|
|
extraConfig = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.anything;
|
|
default = { };
|
|
example = lib.literalExpression ''
|
|
{
|
|
"subsystem=keycloak-server" = {
|
|
"spi=hostname" = {
|
|
"provider=default" = null;
|
|
"provider=fixed" = {
|
|
enabled = true;
|
|
properties.hostname = "keycloak.example.com";
|
|
};
|
|
default-provider = "fixed";
|
|
};
|
|
};
|
|
}
|
|
'';
|
|
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>.
|
|
'';
|
|
};
|
|
|
|
};
|
|
|
|
config =
|
|
let
|
|
# 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";
|
|
|
|
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.
|
|
themesBundle = pkgs.runCommand "keycloak-themes" {} ''
|
|
linkTheme() {
|
|
theme="$1"
|
|
name="$2"
|
|
|
|
mkdir "$out/$name"
|
|
for typeDir in "$theme"/*; do
|
|
if [ -d "$typeDir" ]; then
|
|
type="$(basename "$typeDir")"
|
|
mkdir "$out/$name/$type"
|
|
for file in "$typeDir"/*; do
|
|
ln -sn "$file" "$out/$name/$type/$(basename "$file")"
|
|
done
|
|
fi
|
|
done
|
|
}
|
|
|
|
mkdir -p "$out"
|
|
for theme in ${cfg.package}/themes/*; do
|
|
if [ -d "$theme" ]; then
|
|
linkTheme "$theme" "$(basename "$theme")"
|
|
fi
|
|
done
|
|
|
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: theme: "linkTheme ${theme} ${lib.escapeShellArg name}") cfg.themes)}
|
|
'';
|
|
|
|
keycloakConfig' = builtins.foldl' lib.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@";
|
|
};
|
|
} [
|
|
(lib.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}:${builtins.toString cfg.database.port}/keycloak";
|
|
driver-name = "postgresql";
|
|
"connection-properties=ssl".value = lib.boolToString cfg.database.useSSL;
|
|
} // (lib.optionalAttrs (cfg.database.caCert != null) {
|
|
"connection-properties=sslrootcert".value = cfg.database.caCert;
|
|
"connection-properties=sslmode".value = "verify-ca";
|
|
});
|
|
};
|
|
})
|
|
(lib.optionalAttrs (cfg.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}:${builtins.toString cfg.database.port}/keycloak";
|
|
driver-name = "mysql";
|
|
"connection-properties=useSSL".value = lib.boolToString cfg.database.useSSL;
|
|
"connection-properties=requireSSL".value = lib.boolToString cfg.database.useSSL;
|
|
"connection-properties=verifyServerCertificate".value = lib.boolToString cfg.database.useSSL;
|
|
"connection-properties=characterEncoding".value = "UTF-8";
|
|
valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker";
|
|
validate-on-match = true;
|
|
exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter";
|
|
} // (lib.optionalAttrs (cfg.database.caCert != null) {
|
|
"connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}";
|
|
"connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword";
|
|
});
|
|
};
|
|
})
|
|
(lib.optionalAttrs (cfg.sslCertificate != null && cfg.sslCertificateKey != null) {
|
|
"socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort;
|
|
"subsystem=elytron" = lib.mkOrder 900 {
|
|
"key-store=httpsKS" = lib.mkOrder 900 {
|
|
path = "/run/keycloak/ssl/certificate_private_key_bundle.p12";
|
|
credential-reference.clear-text = "notsosecretpassword";
|
|
type = "JKS";
|
|
};
|
|
"key-manager=httpsKM" = lib.mkOrder 901 {
|
|
key-store = "httpsKS";
|
|
credential-reference.clear-text = "notsosecretpassword";
|
|
};
|
|
"server-ssl-context=httpsSSC" = lib.mkOrder 902 {
|
|
key-manager = "httpsKM";
|
|
};
|
|
};
|
|
"subsystem=undertow" = lib.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 = builtins.match ''"\$\{.*}"'' string;
|
|
in
|
|
if matchResult != null then
|
|
"expression " + string
|
|
else
|
|
string;
|
|
|
|
writeAttribute = attribute: value:
|
|
let
|
|
type = builtins.typeOf value;
|
|
in
|
|
if type == "set" then
|
|
let
|
|
names = builtins.attrNames value;
|
|
in
|
|
builtins.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 builtins.elem type [ "string" "path" "bool" ] then
|
|
let
|
|
value' = if type == "bool" then lib.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
|
|
lib.concatStrings
|
|
(lib.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 = builtins.typeOf value;
|
|
in
|
|
if type == "set" then
|
|
"${attribute} = { " + (makeArgList value) + " }"
|
|
else if builtins.elem type [ "string" "path" "bool" ] then
|
|
"${attribute} = ${if type == "bool" then lib.boolToString value else ''"${value}"''}"
|
|
else if value == null then
|
|
""
|
|
else
|
|
throw "Unsupported type '${type}' for attribute '${attribute}'!";
|
|
|
|
in
|
|
lib.concatStringsSep ", " (lib.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 builtins.isAttrs nodeValue && nodeValue._type or "" == "order" then
|
|
nodeValue.content
|
|
else
|
|
nodeValue;
|
|
isPath = name:
|
|
let
|
|
value = nodeContent.${name};
|
|
in
|
|
if (builtins.match ".*([=]).*" name) == [ "=" ] then
|
|
if builtins.isAttrs value || value == null then
|
|
true
|
|
else
|
|
throw "Parsing path '${lib.concatStringsSep "." (nodePath ++ [ name ])}' failed: JBoss attributes cannot contain '='!"
|
|
else
|
|
false;
|
|
jbossPath = "/" + lib.concatStringsSep "/" nodePath;
|
|
children = if !builtins.isAttrs nodeContent then {} else nodeContent;
|
|
subPaths = builtins.filter isPath (builtins.attrNames children);
|
|
getPriority = name:
|
|
let value = children.${name};
|
|
in if value._type or "" == "order" then value.priority else 1000;
|
|
orderedSubPaths = lib.sort (a: b: getPriority a < getPriority b) subPaths;
|
|
jbossAttrs = lib.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 + lib.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
|
|
'';
|
|
in
|
|
lib.mkIf cfg.enable {
|
|
|
|
assertions = [
|
|
{
|
|
assertion = (cfg.database.useSSL && cfg.database.type == "postgresql") -> (cfg.database.caCert != null);
|
|
message = "A CA certificate must be specified (in 'services.keycloak.database.caCert') when PostgreSQL is used with SSL";
|
|
}
|
|
];
|
|
|
|
environment.systemPackages = [ cfg.package ];
|
|
|
|
systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL {
|
|
after = [ "postgresql.service" ];
|
|
before = [ "keycloak.service" ];
|
|
bindsTo = [ "postgresql.service" ];
|
|
path = [ config.services.postgresql.package ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
User = "postgres";
|
|
Group = "postgres";
|
|
};
|
|
script = ''
|
|
set -o errexit -o pipefail -o nounset -o errtrace
|
|
shopt -s inherit_errexit
|
|
|
|
create_role="$(mktemp)"
|
|
trap 'rm -f "$create_role"' ERR EXIT
|
|
|
|
echo "CREATE ROLE keycloak WITH LOGIN PASSWORD '$(<'${cfg.database.passwordFile}')' CREATEDB" > "$create_role"
|
|
psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || psql -tA --file="$create_role"
|
|
psql -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || psql -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"'
|
|
'';
|
|
};
|
|
|
|
systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL {
|
|
after = [ "mysql.service" ];
|
|
before = [ "keycloak.service" ];
|
|
bindsTo = [ "mysql.service" ];
|
|
path = [ config.services.mysql.package ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
User = config.services.mysql.user;
|
|
Group = config.services.mysql.group;
|
|
};
|
|
script = ''
|
|
set -o errexit -o pipefail -o nounset -o errtrace
|
|
shopt -s inherit_errexit
|
|
|
|
db_password="$(<'${cfg.database.passwordFile}')"
|
|
( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';"
|
|
echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;"
|
|
echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';"
|
|
) | mysql -N
|
|
'';
|
|
};
|
|
|
|
systemd.services.keycloak =
|
|
let
|
|
databaseServices =
|
|
if createLocalPostgreSQL then [
|
|
"keycloakPostgreSQLInit.service" "postgresql.service"
|
|
]
|
|
else if createLocalMySQL then [
|
|
"keycloakMySQLInit.service" "mysql.service"
|
|
]
|
|
else [ ];
|
|
in {
|
|
after = databaseServices;
|
|
bindsTo = databaseServices;
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = with pkgs; [
|
|
cfg.package
|
|
openssl
|
|
replace-secret
|
|
];
|
|
environment = {
|
|
JBOSS_LOG_DIR = "/var/log/keycloak";
|
|
JBOSS_BASE_DIR = "/run/keycloak";
|
|
JBOSS_MODULEPATH = "${cfg.package}/modules";
|
|
};
|
|
serviceConfig = {
|
|
LoadCredential = [
|
|
"db_password:${cfg.database.passwordFile}"
|
|
] ++ lib.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"
|
|
];
|
|
RuntimeDirectoryMode = 0700;
|
|
LogsDirectory = "keycloak";
|
|
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
|
};
|
|
script = ''
|
|
set -o errexit -o pipefail -o nounset -o errtrace
|
|
shopt -s inherit_errexit
|
|
|
|
umask u=rwx,g=,o=
|
|
|
|
install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration
|
|
install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml
|
|
|
|
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.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
|
|
'' + ''
|
|
${cfg.package}/bin/standalone.sh
|
|
'';
|
|
};
|
|
|
|
services.postgresql.enable = lib.mkDefault createLocalPostgreSQL;
|
|
services.mysql.enable = lib.mkDefault createLocalMySQL;
|
|
services.mysql.package = lib.mkIf createLocalMySQL pkgs.mariadb;
|
|
};
|
|
|
|
meta.doc = ./keycloak.xml;
|
|
meta.maintainers = [ lib.maintainers.talyz ];
|
|
}
|