diff options
Diffstat (limited to 'infra/libkookie/nixpkgs/nixos/modules/services/web-apps')
8 files changed, 966 insertions, 12 deletions
diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/codimd.nix b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/codimd.nix index c787c36b877c..0fbc9ee820e6 100644 --- a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/codimd.nix +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/codimd.nix @@ -6,8 +6,10 @@ let cfg = config.services.codimd; prettyJSON = conf: - pkgs.runCommand "codimd-config.json" { preferLocalBuild = true; } '' - echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq \ + pkgs.runCommandLocal "codimd-config.json" { + nativeBuildInputs = [ pkgs.jq ]; + } '' + echo '${builtins.toJSON conf}' | jq \ '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out ''; in @@ -878,7 +880,6 @@ in }; }; - environmentFile = mkOption { type = with types; nullOr path; default = null; @@ -908,6 +909,14 @@ in <literal>CodiMD</literal> is running. ''; }; + + package = mkOption { + type = types.package; + default = pkgs.codimd; + description = '' + Package that provides CodiMD. + ''; + }; }; config = mkIf cfg.enable { @@ -938,7 +947,7 @@ in ''; serviceConfig = { WorkingDirectory = cfg.workDir; - ExecStart = "${pkgs.codimd}/bin/codimd"; + ExecStart = "${cfg.package}/bin/codimd"; EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; Environment = [ "CMD_CONFIG_FILE=${cfg.workDir}/config.json" diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix index 899582a20304..2e755ae9d523 100644 --- a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/engelsystem.nix @@ -10,7 +10,7 @@ in { default = false; example = true; description = '' - Whether to enable engelsystem, an online tool for coordinating helpers + Whether to enable engelsystem, an online tool for coordinating volunteers and shifts on large events. ''; type = lib.types.bool; diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/gerrit.nix b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/gerrit.nix index 657b1a4fc5ba..864587aea565 100644 --- a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/gerrit.nix +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/gerrit.nix @@ -143,7 +143,7 @@ in Set a UUID that uniquely identifies the server. This can be generated with - <literal>nix-shell -p utillinux --run uuidgen</literal>. + <literal>nix-shell -p util-linux --run uuidgen</literal>. ''; }; }; diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/keycloak.nix b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/keycloak.nix new file mode 100644 index 000000000000..bbb0c8d04831 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/keycloak.nix @@ -0,0 +1,692 @@ +{ 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; +} diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/keycloak.xml b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/keycloak.xml new file mode 100644 index 000000000000..ca5e223eee46 --- /dev/null +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/keycloak.xml @@ -0,0 +1,205 @@ +<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> diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/moinmoin.nix b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/moinmoin.nix index dc7abce2a5cb..3a876f75f4a4 100644 --- a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/moinmoin.nix +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/moinmoin.nix @@ -224,6 +224,8 @@ in chmod -R u+w ${dataDir}/${wikiIdent}/underlay ''; + startLimitIntervalSec = 30; + serviceConfig = { User = user; Group = group; @@ -237,7 +239,6 @@ in Restart = "on-failure"; RestartSec = "2s"; - StartLimitIntervalSec = "30s"; StateDirectory = "moin/${wikiIdent}"; StateDirectoryMode = "0750"; diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix index bad1bd9c767d..53c2ab76fdfa 100644 --- a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/nextcloud.nix @@ -549,9 +549,7 @@ in { }; "/" = { priority = 900; - extraConfig = if major < 20 - then "rewrite ^ /index.php;" - else "try_files $uri $uri/ /index.php$request_uri;"; + extraConfig = "rewrite ^ /index.php;"; }; "~ ^/store-apps" = { priority = 201; @@ -575,7 +573,7 @@ in { "~ ^/(?:\\.|autotest|occ|issue|indie|db_|console)".extraConfig = '' return 404; ''; - ${if major < 20 then "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" else "~ \\.php(?:$|/)"} = { + "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" = { priority = 500; extraConfig = '' include ${config.services.nginx.package}/conf/fastcgi.conf; diff --git a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/shiori.nix b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/shiori.nix index 1817a2039352..9083ddfa2206 100644 --- a/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/shiori.nix +++ b/infra/libkookie/nixpkgs/nixos/modules/services/web-apps/shiori.nix @@ -37,11 +37,60 @@ in { description = "Shiori simple bookmarks manager"; wantedBy = [ "multi-user.target" ]; + environment.SHIORI_DIR = "/var/lib/shiori"; + serviceConfig = { ExecStart = "${package}/bin/shiori serve --address '${address}' --port '${toString port}'"; + DynamicUser = true; - Environment = "SHIORI_DIR=/var/lib/shiori"; StateDirectory = "shiori"; + # As the RootDirectory + RuntimeDirectory = "shiori"; + + # Security options + + BindReadOnlyPaths = [ + "/nix/store" + + # For SSL certificates, and the resolv.conf + "/etc" + ]; + + CapabilityBoundingSet = ""; + + DeviceAllow = ""; + + LockPersonality = true; + + MemoryDenyWriteExecute = true; + + PrivateDevices = true; + PrivateUsers = true; + + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + + RestrictNamespaces = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictRealtime = true; + RestrictSUIDSGID = true; + + RootDirectory = "/run/shiori"; + + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ + "@system-service" + + "~@chown" "~@cpu-emulation" "~@debug" "~@ipc" "~@keyring" "~@memlock" + "~@module" "~@obsolete" "~@privileged" "~@process" "~@raw-io" + "~@resources" "~@setuid" + ]; }; }; }; |