Change:
Project description Metadata homepage URL: source URL: license: Apache-2.0 platforms: linux, darwin (maybe?)
There exists a package: A module is currently missing.
Uh I was pretty sure I checked in my local nixpkgs checkout. Weird, I wonder if I just made a typo while checking. Sorry about that. We can leave this issue for the module part.
It looks like the maintainer is Have you started work on a module already, or would you prefer someone else step up to work on that part? I haven't dealt with creating modules in the past, but I'd be willing to learn.
I haven't dug into making a module previously. I suspect there's a bit of ugliness around the setup and configuration decoratively, but I'm happy to work with someone on it. Shall I start a basic PR?
yes please :)

Discussion:
<itemizedlist> <listitem> <del> <para /> </del> <ins> <para> <link xlink:href="https://www.keycloak.org/">Keycloak</link>, an open source identity and access management server with support for <link xlink:href="https://openid.net/connect/">OpenID Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH 2.0</link> and <link xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML 2.0</link>. </para> <para> See the <link linkend="module-services-keycloak">Keycloak section of the NixOS manual</link> for more information. </para> </ins> </listitem> </itemizedlist>

./services/web-apps/ihatemoney ./services/web-apps/jirafeau.nix ./services/web-apps/jitsi-meet.nix <ins> ./services/web-apps/keycloak.nix </ins> ./services/web-apps/limesurvey.nix ./services/web-apps/mattermost.nix ./services/web-apps/mediawiki.nix

<ins> { config, pkgs, lib, ... }: let cfg = config.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; 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. ''; }; certificatePrivateKeyBundle = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; example = "/run/keys/ssl_cert"; description = '' The path to a PEM formatted bundle of the private key and certificate to use for TLS connections. This should be a string, not a Nix path, since Nix paths are copied into the world-readable Nix store. ''; }; databaseType = lib.mkOption { type = lib.types.enum [ "mysql" "postgresql" ]; default = "postgresql"; example = "mysql"; description = '' The type of database Keycloak should connect to. ''; }; databaseHost = lib.mkOption { type = lib.types.str; default = "localhost"; description = '' Hostname of the database to connect to. ''; }; databasePort = let dbPorts = { postgresql = 5432; mysql = 3306; }; in lib.mkOption { type = lib.types.port; default = dbPorts.${cfg.databaseType}; description = '' Port of the database to connect to. ''; }; databaseUseSSL = lib.mkOption { type = lib.types.bool; default = cfg.databaseHost != "localhost"; description = '' Whether the database connection should be secured by SSL / TLS. ''; }; databaseCaCert = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' The SSL / TLS CA certificate that verifies the identity of the database server. Required when PostgreSQL is used and SSL is turned on. For MySQL, if left at <literal>null</literal>, the default Java keystore is used, which should suffice if the server certificate is issued by an official CA. ''; }; databaseCreateLocally = lib.mkOption { type = lib.types.bool; default = true; description = '' Whether a database should be automatically created on the local host. Set this to false if you plan on provisioning a local database yourself. This has no effect if services.keycloak.databaseHost is customized. ''; }; databaseUsername = lib.mkOption { type = lib.types.str; default = "keycloak"; description = '' Username to use when connecting to an external or manually provisioned database; has no effect when a local database is automatically provisioned. ''; }; databasePasswordFile = 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; 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. ''; }; extraConfig = lib.mkOption { type = lib.types.attrs; default = { }; example = lib.literalExample '' { "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.databaseCreateLocally && cfg.databaseHost == "localhost"; createLocalPostgreSQL = databaseActuallyCreateLocally && cfg.databaseType == "postgresql"; createLocalMySQL = databaseActuallyCreateLocally && cfg.databaseType == "mysql"; mySqlCaKeystore = pkgs.runCommandNoCC "mysql-ca-keystore" {} '' ${pkgs.jre}/bin/keytool -importcert -trustcacerts -alias MySQLCACert -file ${cfg.databaseCaCert} -keystore $out -storepass notsosecretpassword -noprompt ''; keycloakConfig' = builtins.foldl' lib.recursiveUpdate { "interface=public".inet-address = cfg.bindAddress; "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; }; }; }; "subsystem=datasources"."data-source=KeycloakDS" = { max-pool-size = "20"; user-name = if databaseActuallyCreateLocally then "keycloak" else cfg.databaseUsername; password = "@db-password@"; }; } [ (lib.optionalAttrs (cfg.databaseType == "postgresql") { "subsystem=datasources" = { "jdbc-driver=postgresql" = { driver-module-name = "org.postgresql"; driver-name = "postgresql"; driver-xa-datasource-class-name = "org.postgresql.xa.PGXADataSource"; }; "data-source=KeycloakDS" = { connection-url = "jdbc:postgresql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak"; driver-name = "postgresql"; "connection-properties=ssl".value = lib.boolToString cfg.databaseUseSSL; } // (lib.optionalAttrs (cfg.databaseCaCert != null) { "connection-properties=sslrootcert".value = cfg.databaseCaCert; "connection-properties=sslmode".value = "verify-ca"; }); }; }) (lib.optionalAttrs (cfg.databaseType == "mysql") { "subsystem=datasources" = { "jdbc-driver=mysql" = { driver-module-name = "com.mysql"; driver-name = "mysql"; driver-class-name = "com.mysql.jdbc.Driver"; }; "data-source=KeycloakDS" = { connection-url = "jdbc:mysql://${cfg.databaseHost}:${builtins.toString cfg.databasePort}/keycloak"; driver-name = "mysql"; "connection-properties=useSSL".value = lib.boolToString cfg.databaseUseSSL; "connection-properties=requireSSL".value = lib.boolToString cfg.databaseUseSSL; "connection-properties=verifyServerCertificate".value = lib.boolToString cfg.databaseUseSSL; "connection-properties=characterEncoding".value = "UTF-8"; valid-connection-checker-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"; validate-on-match = true; exception-sorter-class-name = "org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLExceptionSorter"; } // (lib.optionalAttrs (cfg.databaseCaCert != null) { "connection-properties=trustCertificateKeyStoreUrl".value = "file:${mySqlCaKeystore}"; "connection-properties=trustCertificateKeyStorePassword".value = "notsosecretpassword"; }); }; }) (lib.optionalAttrs (cfg.certificatePrivateKeyBundle != null) { "socket-binding-group=standard-sockets"."socket-binding=https".port = cfg.httpsPort; "core-service=management"."security-realm=UndertowRealm"."server-identity=ssl" = { keystore-path = "/run/keycloak/ssl/certificate_private_key_bundle.p12"; keystore-password = "notsosecretpassword"; }; "subsystem=undertow"."server=default-server"."https-listener=https".security-realm = "UndertowRealm"; }) 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 match = (builtins.match ''"${.*}"'' string); in if match != 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 `attrs` attrset, beginning at the path resolved from `state.path ++ node`; if `node` is `null`, starts from `state.path`. Only subattrsets that are JBoss paths, i.e. follows the `key=value` format, are recursed into - the rest are considered JBoss attributes / maps. */ recurse = state: node: let path = state.path ++ (lib.optional (node != null) node); isPath = name: let value = lib.getAttrFromPath (path ++ [ name ]) attrs; in if (builtins.match ".*([=]).*" name) == [ "=" ] then if builtins.isAttrs value || value == null then true else throw "Parsing path '${lib.concatStringsSep "." (path ++ [ name ])}' failed: JBoss attributes cannot contain '='!" else false; jbossPath = "/" + (lib.concatStringsSep "/" path); nodeValue = lib.getAttrFromPath path attrs; children = if !builtins.isAttrs nodeValue then {} else nodeValue; subPaths = builtins.filter isPath (builtins.attrNames children); jbossAttrs = lib.filterAttrs (name: _: !(isPath name)) children; in state // { text = state.text + ( if nodeValue != 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 '') + (builtins.foldl' recurse { text = ""; inherit path; } subPaths).text; }; in (recurse { text = ""; path = []; } null).text; jbossCliScript = pkgs.writeText "jboss-cli-script" (mkJbossScript keycloakConfig'); keycloakConfig = pkgs.runCommandNoCC "keycloak-config" {} '' export JBOSS_BASE_DIR="$(pwd -P)"; export JBOSS_MODULEPATH="${cfg.package}/modules"; export JBOSS_LOG_DIR="$JBOSS_BASE_DIR/log"; cp -r ${cfg.package}/standalone/configuration . chmod -R u+rwX ./configuration mkdir -p {deployments,ssl} "${cfg.package}/bin/standalone.sh"& attempt=1 max_attempts=30 while ! ${cfg.package}/bin/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 ${cfg.package}/bin/jboss-cli.sh --connect --file=${jbossCliScript} --echo-command cp configuration/standalone.xml $out ''; in lib.mkIf cfg.enable { assertions = [ { assertion = (cfg.databaseUseSSL && cfg.databaseType == "postgresql") -> (cfg.databaseCaCert != null); message = ''A CA certificate must be specified (in 'services.keycloak.databaseCaCert') when PostgreSQL is used with SSL''; } ]; environment.systemPackages = [ cfg.package ]; systemd.services.keycloakPostgreSQLInit = lib.mkIf createLocalPostgreSQL { after = [ "postgresql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "postgresql.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; User = "postgres"; Group = "postgres"; }; script = '' set -eu PSQL=${config.services.postgresql.package}/bin/psql db_password="$(<'${cfg.databasePasswordFile}')" $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='keycloak'" | grep -q 1 || $PSQL -tAc "CREATE ROLE keycloak WITH LOGIN PASSWORD '$db_password' CREATEDB" $PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = 'keycloak'" | grep -q 1 || $PSQL -tAc 'CREATE DATABASE "keycloak" OWNER "keycloak"' ''; }; systemd.services.keycloakMySQLInit = lib.mkIf createLocalMySQL { after = [ "mysql.service" ]; before = [ "keycloak.service" ]; bindsTo = [ "mysql.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; User = config.services.mysql.user; Group = config.services.mysql.group; }; script = '' set -eu db_password="$(<'${cfg.databasePasswordFile}')" ( echo "CREATE USER IF NOT EXISTS 'keycloak'@'localhost' IDENTIFIED BY '$db_password';" echo "CREATE DATABASE keycloak CHARACTER SET utf8 COLLATE utf8_unicode_ci;" echo "GRANT ALL PRIVILEGES ON keycloak.* TO 'keycloak'@'localhost';" ) | ${config.services.mysql.package}/bin/mysql -N ''; }; systemd.services.keycloak = let databaseServices = if createLocalPostgreSQL then [ "keycloakPostgreSQLInit.service" "postgresql.service" ] else if createLocalMySQL then [ "keycloakMySQLInit.service" "mysql.service" ] else [ ]; in { after = databaseServices; bindsTo = databaseServices; wantedBy = [ "multi-user.target" ]; environment = { JBOSS_LOG_DIR = "/var/log/keycloak"; JBOSS_BASE_DIR = "/run/keycloak"; JBOSS_MODULEPATH = "${cfg.package}/modules"; }; serviceConfig = { ExecStartPre = let startPreFullPrivileges = '' set -eu install -T -m 0400 -o keycloak -g keycloak '${cfg.databasePasswordFile}' /run/keycloak/secrets/db_password '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' install -T -m 0400 -o keycloak -g keycloak '${cfg.certificatePrivateKeyBundle}' /run/keycloak/secrets/ssl_cert_pk_bundle ''; startPre = '' set -eu install -m 0600 ${cfg.package}/standalone/configuration/*.properties /run/keycloak/configuration install -T -m 0600 ${keycloakConfig} /run/keycloak/configuration/standalone.xml db_password="$(</run/keycloak/secrets/db_password)" ${pkgs.replace}/bin/replace-literal -fe '@db-password@' "$db_password" /run/keycloak/configuration/standalone.xml export JAVA_OPTS=-Djboss.server.config.user.dir=/run/keycloak/configuration ${cfg.package}/bin/add-user-keycloak.sh -u admin -p '${cfg.initialAdminPassword}' '' + lib.optionalString (cfg.certificatePrivateKeyBundle != null) '' pushd /run/keycloak/ssl/ cat /run/keycloak/secrets/ssl_cert_pk_bundle <(echo) /etc/ssl/certs/ca-certificates.crt > allcerts.pem ${pkgs.openssl}/bin/openssl pkcs12 -export -in /run/keycloak/secrets/ssl_cert_pk_bundle -chain  -name "${cfg.frontendUrl}" -out certificate_private_key_bundle.p12  -CAfile allcerts.pem -passout pass:notsosecretpassword popd ''; in [ "+${pkgs.writeShellScript "keycloak-start-pre-full-privileges" startPreFullPrivileges}" "${pkgs.writeShellScript "keycloak-start-pre" startPre}" ]; ExecStart = "${cfg.package}/bin/standalone.sh"; User = "keycloak"; Group = "keycloak"; DynamicUser = true; RuntimeDirectory = map (p: "keycloak/" + p) [ "secrets" "configuration" "deployments" "data" "ssl" "log" "tmp" ]; RuntimeDirectoryMode = 0700; LogsDirectory = "keycloak"; AmbientCapabilities = "CAP_NET_BIND_SERVICE"; }; }; services.postgresql.enable = lib.mkDefault createLocalPostgreSQL; services.mysql.enable = lib.mkDefault createLocalMySQL; services.mysql.package = lib.mkIf createLocalMySQL pkgs.mysql; }; meta.doc = ./keycloak.xml; } </ins>

<ins> <chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" version="5.0" xml:id="module-services-keycloak"> <title>Keycloak</title> <para> <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an open source identity and access management server with support for <link xlink:href="https://openid.net/connect/">OpenID Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH 2.0</link> and <link xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML 2.0</link>. </para> <section xml:id="module-services-keycloak-admin"> <title>Administration</title> <para> An administrative user with the username <literal>admin</literal> is automatically created in the <literal>master</literal> realm. Its initial password can be configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" /> and defaults to <literal>changeme</literal>. The password is not stored safely and should be changed immediately in the admin panel. </para> <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. </para> </section> <section xml:id="module-services-keycloak-database"> <title>Database access</title> <para> <productname>Keycloak</productname> can be used with either <productname>PostgreSQL</productname> or <productname>MySQL</productname>. Which one is used can be configured in <xref linkend="opt-services.keycloak.databaseType" />. The selected database will automatically be enabled and a database and role created unless <xref linkend="opt-services.keycloak.databaseHost" /> is changed from its default of <literal>localhost</literal> or <xref linkend="opt-services.keycloak.databaseCreateLocally" /> is set to <literal>false</literal>. </para> <para> External database access can also be configured by setting <xref linkend="opt-services.keycloak.databaseHost" />, <xref linkend="opt-services.keycloak.databaseUsername" />, <xref linkend="opt-services.keycloak.databaseUseSSL" /> and <xref linkend="opt-services.keycloak.databaseCaCert" /> as appropriate. Note that you need to manually create a database called <literal>keycloak</literal> and allow the configured database user full access to it. </para> <para> <xref linkend="opt-services.keycloak.databasePasswordFile" /> must be set to the path to a file containing the password used to log in to the database. If <xref linkend="opt-services.keycloak.databaseHost" /> and <xref linkend="opt-services.keycloak.databaseCreateLocally" /> are kept at their defaults, the database role <literal>keycloak</literal> with that password is provisioned on the local database instance. </para> <warning> <para> The path should be provided as a string, not a Nix path, since Nix paths are copied into the world readable Nix store. </para> </warning> </section> <section xml:id="module-services-keycloak-frontendurl"> <title>Frontend URL</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). </para> <para> <xref linkend="opt-services.keycloak.forceBackendUrlToFrontendUrl" /> determines whether Keycloak should force all requests to go through the frontend URL. By default, <productname>Keycloak</productname> 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. </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. </para> </section> <section xml:id="module-services-keycloak-tls"> <title>Setting up TLS/SSL</title> <para> By default, <productname>Keycloak</productname> won't accept unsecured HTTP connections originating from outside its local network. </para> <para> For HTTPS support, a TLS certificate and private key is required. They should be <link xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM formatted</link> and concatenated into a single file. The path to this file should be configured in <xref linkend="opt-services.keycloak.certificatePrivateKeyBundle" />. </para> <warning> <para> The path should be provided as a string, not a Nix path, since Nix paths are copied into the world readable Nix store. </para> </warning> </section> <section xml:id="module-services-keycloak-extra-config"> <title>Additional configuration</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" />. </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>. </para> </section> <section xml:id="module-services-keycloak-example-config"> <title>Example configuration</title> <para> A basic configuration with some custom settings could look like this: <programlisting> services.keycloak = { <link linkend="opt-services.keycloak.enable">enable</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.certificatePrivateKeyBundle">certificatePrivateKeyBundle</link> = "/run/keys/ssl_cert"; <link linkend="opt-services.keycloak.databasePasswordFile">databasePasswordFile</link> = "/run/keys/db_password"; }; </programlisting> </para> </section> </chapter> </ins>

kernel-latest = handleTest ./kernel-latest.nix {}; kernel-lts = handleTest ./kernel-lts.nix {}; kernel-testing = handleTest ./kernel-testing.nix {}; <ins> keycloak = discoverTests (import ./keycloak.nix); </ins> keymap = handleTest ./keymap.nix {}; knot = handleTest ./knot.nix {}; krb5 = discoverTests (import ./krb5 {});

<ins> # This tests Keycloak: it starts the service, creates a realm with an # OIDC client and a user, and simulates the user logging in to the # client using their Keycloak login. let frontendUrl = "http://keycloak/auth"; initialAdminPassword = "h4IhoJFnt2iQIR9"; keycloakTest = import ./make-test-python.nix ( { pkgs, databaseType, ... }: { name = "keycloak"; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ talyz ]; }; nodes = { keycloak = { ... }: { virtualisation.memorySize = 1024; services.keycloak = { enable = true; inherit frontendUrl databaseType initialAdminPassword; databasePasswordFile = pkgs.writeText "dbPassword" "wzf6vOCbPp6cqTH"; }; environment.systemPackages = with pkgs; [ xmlstarlet libtidy jq ]; }; }; testScript = let client = { clientId = "test-client"; name = "test-client"; redirectUris = [ "urn:ietf:wg:oauth:2.0:oob" ]; }; user = { firstName = "Chuck"; lastName = "Testa"; username = "chuck.testa"; email = "chuck.testa@example.com"; }; password = "password1234"; realm = { enabled = true; realm = "test-realm"; clients = [ client ]; users = [( user // { enabled = true; credentials = [{ type = "password"; temporary = false; value = password; }]; } )]; }; realmDataJson = pkgs.writeText "realm-data.json" (builtins.toJSON realm); jqCheckUserinfo = pkgs.writeText "check-userinfo.jq" '' if { "firstName": .given_name, "lastName": .family_name, "username": .preferred_username, "email": .email } != ${builtins.toJSON user} then error("Wrong user info!") else empty end ''; in '' keycloak.start() keycloak.wait_for_unit("keycloak.service") keycloak.wait_until_succeeds("curl -sSf ${frontendUrl}") ### Realm Setup ### # Get an admin interface access token keycloak.succeed( "curl -sSf -d 'client_id=admin-cli' -d 'username=admin' -d 'password=${initialAdminPassword}' -d 'grant_type=password' '${frontendUrl}/realms/master/protocol/openid-connect/token' | jq -r '"Authorization: bearer " + .access_token' >admin_auth_header" ) # Publish the realm, including a test OIDC client and user keycloak.succeed( "curl -sSf -H @admin_auth_header -X POST -H 'Content-Type: application/json' -d @${realmDataJson} '${frontendUrl}/admin/realms/'" ) # Generate and save the client secret. To do this we need # Keycloak's internal id for the client. keycloak.succeed( "curl -sSf -H @admin_auth_header '${frontendUrl}/admin/realms/${realm.realm}/clients?clientId=${client.name}' | jq -r '.[].id' >client_id", "curl -sSf -H @admin_auth_header -X POST '${frontendUrl}/admin/realms/${realm.realm}/clients/'$(<client_id)'/client-secret' | jq -r .value >client_secret", ) ### Authentication Testing ### # Start the login process by sending an initial request to the # OIDC authentication endpoint, saving the returned page. Tidy # up the HTML (XmlStarlet is picky) and extract the login form # post url. keycloak.succeed( "curl -sSf -c cookie '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/auth?client_id=${client.name}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=openid+email&response_type=code&response_mode=query&nonce=qw4o89g3qqm' >login_form", "tidy -q -m login_form || true", "xml sel -T -t -m "_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:div/_:form[@id='kc-form-login']" -v @action login_form >form_post_url", ) # Post the login form and save the response. Once again tidy up # the HTML, then extract the authorization code. keycloak.succeed( "curl -sSf -L -b cookie -d 'username=${user.username}' -d 'password=${password}' -d 'credentialId=' "$(<form_post_url)" >auth_code_html", "tidy -q -m auth_code_html || true", "xml sel -T -t -m "_:html/_:body/_:div/_:div/_:div/_:div/_:div/_:input[@id='code']" -v @value auth_code_html >auth_code", ) # Exchange the authorization code for an access token. keycloak.succeed( "curl -sSf -d grant_type=authorization_code -d code=$(<auth_code) -d client_id=${client.name} -d client_secret=$(<client_secret) -d redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/token' | jq -r '"Authorization: bearer " + .access_token' >auth_header" ) # Use the access token on the OIDC userinfo endpoint and check # that the returned user info matches what we initialized the # realm with. keycloak.succeed( "curl -sSf -H @auth_header '${frontendUrl}/realms/${realm.realm}/protocol/openid-connect/userinfo' | jq -f ${jqCheckUserinfo}" ) ''; } ); in { postgres = keycloakTest { databaseType = "postgresql"; }; mysql = keycloakTest { databaseType = "mysql"; }; } </ins>

<del> { stdenv, fetchzip, makeWrapper, jre }: </del> <ins> { stdenv, lib, fetchzip, makeWrapper, jre, writeText, nixosTests , postgresql_jdbc ? null, mysql_jdbc ? null }: 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 </ins> stdenv.mkDerivation rec { pname   = "keycloak"; version = "11.0.2";

cp -r * $out rm -rf $out/bin/*.{ps1,bat} <del> rm -rf $out/bin/add-user-keycloak.sh rm -rf $out/bin/jconsole.sh chmod +x $out/bin/standalone.sh wrapProgram $out/bin/standalone.sh  --prefix PATH ":" ${jre}/bin ; </del> <ins> module_path=$out/modules/system/layers/keycloak if ! [[ -d $module_path ]]; then echo "The module path $module_path not found!" exit 1 fi ${lib.optionalString (postgresql_jdbc != null) '' mkdir -p $module_path/org/postgresql/main ln -s ${postgresql_jdbc}/share/java/postgresql-jdbc.jar $module_path/org/postgresql/main/ ln -s ${mkModuleXml "org.postgresql" "postgresql-jdbc.jar"} $module_path/org/postgresql/main/module.xml ''} ${lib.optionalString (mysql_jdbc != null) '' mkdir -p $module_path/com/mysql/main ln -s ${mysql_jdbc}/share/java/mysql-connector-java.jar $module_path/com/mysql/main/ ln -s ${mkModuleXml "com.mysql" "mysql-connector-java.jar"} $module_path/com/mysql/main/module.xml ''} wrapProgram $out/bin/standalone.sh --set JAVA_HOME ${jre} wrapProgram $out/bin/add-user-keycloak.sh --set JAVA_HOME ${jre} wrapProgram $out/bin/jboss-cli.sh --set JAVA_HOME ${jre} </ins> ''; <ins> passthru.tests = nixosTests.keycloak; </ins> meta = with stdenv.lib; { homepage    = "https://www.keycloak.org/"; description = "Identity and access management for modern applications and services"; license     = licenses.asl20; platforms   = jre.meta.platforms; <del> maintainers = [ maintainers.ngerstle ]; </del> <ins> maintainers = with maintainers; [ ngerstle talyz ]; </ins> }; }