diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 314044b1b2c6..f8631605cc3a 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -24,6 +24,8 @@ - [Unpackerr](https://unpackerr.zip), extracts downloads for Radarr, Sonarr, Lidarr, Readarr, and/or a Watch folder. Available as [services.unpackerr](#opt-services.unpackerr.enable). +- [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service) is an OAuth2.0 and OpenID Connect provider for Matrix homeservers (such as Synapse). It replaces standard password authentication with modern OpenID Connect flows, and can delegate authentication to upstream OIDC providers. Available as [services.matrix-authentication-service](#opt-services.matrix-authentication-service.enable). + ## Backward Incompatibilities {#sec-release-26.11-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 507eeb3a9ff0..d5016c6e7f0e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -809,6 +809,7 @@ ./services/matrix/hookshot.nix ./services/matrix/lk-jwt-service.nix ./services/matrix/matrix-alertmanager.nix + ./services/matrix/matrix-authentication-service.nix ./services/matrix/maubot.nix ./services/matrix/mautrix-discord.nix ./services/matrix/mautrix-meta.nix diff --git a/nixos/modules/services/matrix/matrix-authentication-service.nix b/nixos/modules/services/matrix/matrix-authentication-service.nix new file mode 100644 index 000000000000..8ff62f0dbf28 --- /dev/null +++ b/nixos/modules/services/matrix/matrix-authentication-service.nix @@ -0,0 +1,445 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + concatMapStringsSep + filter + filterAttrs + getExe + isAttrs + isList + mapAttrs + mkDefault + mkEnableOption + mkIf + mkOption + mkPackageOption + optional + optionalAttrs + types + ; + + cfg = config.services.matrix-authentication-service; + format = pkgs.formats.yaml { }; + filterRecursiveNull = + o: + if isAttrs o then + mapAttrs (_: v: filterRecursiveNull v) (filterAttrs (_: v: v != null) o) + else if isList o then + map filterRecursiveNull (filter (v: v != null) o) + else + o; + + # remove null values from the final configuration + finalSettings = + let + pruned = filterRecursiveNull cfg.settings; + in + if pruned ? upstream_oauth2 && pruned.upstream_oauth2 == { } then + removeAttrs pruned [ "upstream_oauth2" ] + else + pruned; + configFile = format.generate "config.yaml" finalSettings; +in +{ + meta.maintainers = with lib.maintainers; [ + eymeric + flashonfire + mkoppmann + skowalak + ]; + meta.teams = [ lib.teams.matrix ]; + + options.services.matrix-authentication-service = { + enable = mkEnableOption "Matrix Authentication Service"; + + package = mkPackageOption pkgs "matrix-authentication-service" { }; + + settings = mkOption { + default = { }; + description = '' + The primary mas configuration. See the + [configuration reference](https://element-hq.github.io/matrix-authentication-service/usage/configuration.html) + for possible values. + + Secrets should be passed in by using the `extraConfigFiles` option. + ''; + type = types.submodule { + freeformType = format.type; + + options = { + http.public_base = mkOption { + type = types.str; + default = "http://[::]:8080/"; + description = '' + Public URL base used when building absolute public URLs. + ''; + }; + http.trusted_proxies = mkOption { + type = types.listOf types.str; + default = [ + "127.0.0.1/8" + "::1/128" + ]; + description = '' + MAS can infer the client IP address from the X-Forwarded-For header. It will trust the value for this header only if the request comes from a trusted reverse proxy listed here. + ''; + }; + http.listeners = mkOption { + type = types.listOf ( + types.submodule { + freeformType = format.type; + options = { + name = mkOption { + type = types.str; + example = "web"; + description = '' + The name of the listener, used in logs and metrics. + ''; + }; + proxy_protocol = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable the PROXY protocol on the listener. + ''; + }; + resources = mkOption { + type = types.listOf ( + types.submodule { + freeformType = format.type; + options = { + name = mkOption { + type = types.str; + description = '' + Serve the given resource. + ''; + }; + }; + } + ); + description = '' + List of resources to serve. + ''; + }; + binds = mkOption { + type = types.listOf ( + types.submodule { + freeformType = format.type; + options = { + host = mkOption { + type = types.nullOr types.str; + description = '' + Listen on the given host. + ''; + }; + port = mkOption { + type = types.nullOr types.port; + description = '' + Listen on the given port. + ''; + }; + }; + } + ); + description = '' + List of addresses and ports to listen to. + ''; + }; + }; + } + ); + default = [ + { + name = "web"; + resources = [ + { name = "discovery"; } + { name = "human"; } + { name = "oauth"; } + { name = "compat"; } + { name = "graphql"; } + { name = "assets"; } + ]; + binds = [ + { + host = "0.0.0.0"; + port = 8080; + } + ]; + proxy_protocol = false; + } + { + name = "internal"; + resources = [ + { name = "health"; } + ]; + binds = [ + { + host = "0.0.0.0"; + port = 8081; + } + ]; + proxy_protocol = false; + } + ]; + description = '' + Each listener can serve multiple resources, and listen on multiple TCP ports or UNIX sockets. + ''; + }; + + database.uri = mkOption { + type = types.str; + default = "postgresql:///matrix-authentication-service?host=/run/postgresql"; + description = '' + The postgres connection string. + Refer to . + If you need to put secrets in the uri, please use the `extraConfigFiles` option. + ''; + }; + + database.max_connections = mkOption { + type = types.ints.unsigned; + default = 10; + description = '' + Maximum number of connections for the connection pool. + ''; + }; + + database.min_connections = mkOption { + type = types.ints.unsigned; + default = 0; + description = '' + Minimum number of connections for the connection pool. + ''; + }; + + database.connect_timeout = mkOption { + type = types.ints.unsigned; + default = 30; + description = '' + Connection timeout for the connection pool. + ''; + }; + + database.idle_timeout = mkOption { + type = types.ints.unsigned; + default = 600; + description = '' + Idle timeout for the connection pool. + ''; + }; + + database.max_lifetime = mkOption { + type = types.ints.unsigned; + default = 1800; + description = '' + Maximum lifetime for the connection pool. + ''; + }; + + passwords.enabled = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable the password database. If disabled, users will only be able to log in using upstream OIDC providers. + ''; + }; + + passwords.schemes = mkOption { + type = types.listOf ( + types.submodule { + freeformType = format.type; + options = { + version = mkOption { + type = types.ints.unsigned; + description = '' + Password scheme version. + ''; + }; + algorithm = mkOption { + type = types.str; + description = '' + Password scheme algorithm. + ''; + }; + }; + } + ); + default = [ + { + version = 1; + algorithm = "argon2id"; + } + ]; + description = '' + List of password hashing schemes being used. Only change this if you know what you're doing. + ''; + }; + + passwords.minimum_complexity = mkOption { + type = types.enum [ + 0 + 1 + 2 + 3 + 4 + ]; + default = 3; + description = '' + Minimum complexity required for passwords, estimated by the zxcvbn algorithm. + Must be between 0 and 4, default is 3. See for more information. + ''; + }; + + matrix.homeserver = mkOption { + type = types.str; + default = ""; + description = '' + Corresponds to the server_name in the Synapse configuration file. + ''; + }; + matrix.endpoint = mkOption { + type = types.str; + default = ""; + description = '' + The URL to which the homeserver is accessible from the service. + ''; + }; + upstream_oauth2.providers = mkOption { + default = null; + type = types.nullOr ( + types.listOf ( + types.submodule { + freeformType = format.type; + options = { + id = mkOption { + type = types.nullOr types.str; + example = "01H8PKNWKKRPCBW4YGH1RWV279"; + default = null; + description = '' + Unique id for the provider, must be a ULID, and can be generated using online tools like . + ''; + }; + }; + } + ) + ); + description = '' + Configuration of upstream providers + ''; + }; + }; + }; + }; + + createDatabase = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable and configure `services.postgresql` to ensure that the database user `matrix-authentication-service` + and the database `matrix-authentication-service` exist. + ''; + }; + + extraConfigFiles = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Extra config files to include. + + The configuration files will be included based on the command line + argument --config. This allows to configure secrets without + having to go through the Nix store, e.g. based on deployment keys if + NixOps is in use. + ''; + }; + + serviceDependencies = mkOption { + type = types.listOf types.str; + default = optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit; + defaultText = lib.literalExpression '' + lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit + ''; + description = '' + List of Systemd services to require and wait for when starting the application service, + such as the Matrix homeserver if it's running on the same host. + ''; + }; + }; + + config = mkIf cfg.enable { + services.postgresql = mkIf cfg.createDatabase { + enable = true; + ensureDatabases = [ "matrix-authentication-service" ]; + ensureUsers = [ + { + name = "matrix-authentication-service"; + ensureDBOwnership = true; + } + ]; + }; + + systemd.services.matrix-authentication-service = rec { + after = optional cfg.createDatabase "postgresql.service" ++ cfg.serviceDependencies; + wants = after; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + DynamicUser = true; + ExecStartPre = '' + ${getExe cfg.package} config check \ + ${concatMapStringsSep " " (x: "--config ${x}") ([ configFile ] ++ cfg.extraConfigFiles)} + ''; + ExecStart = '' + ${getExe cfg.package} server \ + ${concatMapStringsSep " " (x: "--config ${x}") ([ configFile ] ++ cfg.extraConfigFiles)} + ''; + Restart = "on-failure"; + RestartSec = "1s"; + + # Security Hardening + CapabilityBoundingSet = ""; + AmbientCapabilities = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateMounts = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ + "@system-service" + ]; + UMask = "0077"; + + # Working and state directories + StateDirectory = "matrix-authentication-service"; + StateDirectoryMode = "0700"; + WorkingDirectory = "/var/lib/matrix-authentication-service"; + }; + }; + }; +}