diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index ba9b88f448e..c79647962b9 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -45,6 +45,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [opensearch](https://opensearch.org), a search server alternative to Elasticsearch. Available as [services.opensearch](options.html#opt-services.opensearch.enable). +- [authelia](https://www.authelia.com/), is an open-source authentication and authorization server. Available under [services.authelia](options.html#opt-services.authelia.enable). + - [goeland](https://github.com/slurdge/goeland), an alternative to rss2email written in golang with many filters. Available as [services.goeland](#opt-services.goeland.enable). - [alertmanager-irc-relay](https://github.com/google/alertmanager-irc-relay), a Prometheus Alertmanager IRC Relay. Available as [services.prometheus.alertmanagerIrcRelay](options.html#opt-services.prometheus.alertmanagerIrcRelay.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6ebbe3ff2a0..945b053f7cd 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1212,6 +1212,7 @@ ./services/web-apps/zabbix.nix ./services/web-servers/agate.nix ./services/web-servers/apache-httpd/default.nix + ./services/web-servers/authelia.nix ./services/web-servers/caddy/default.nix ./services/web-servers/darkhttpd.nix ./services/web-servers/fcgiwrap.nix diff --git a/nixos/modules/services/web-servers/authelia.nix b/nixos/modules/services/web-servers/authelia.nix new file mode 100644 index 00000000000..143c441c7e1 --- /dev/null +++ b/nixos/modules/services/web-servers/authelia.nix @@ -0,0 +1,401 @@ +{ lib +, pkgs +, config +, ... +}: + +let + cfg = config.services.authelia; + + format = pkgs.formats.yaml { }; + configFile = format.generate "config.yml" cfg.settings; + + autheliaOpts = with lib; { name, ... }: { + options = { + enable = mkEnableOption (mdDoc "Authelia instance"); + + name = mkOption { + type = types.str; + default = name; + description = mdDoc '' + Name is used as a suffix for the service name, user, and group. + By default it takes the value you use for `` in: + {option}`services.authelia.` + ''; + }; + + package = mkOption { + default = pkgs.authelia; + type = types.package; + defaultText = literalExpression "pkgs.authelia"; + description = mdDoc "Authelia derivation to use."; + }; + + user = mkOption { + default = "authelia-${name}"; + type = types.str; + description = mdDoc "The name of the user for this authelia instance."; + }; + + group = mkOption { + default = "authelia-${name}"; + type = types.str; + description = mdDoc "The name of the group for this authelia instance."; + }; + + secrets = mkOption { + description = mdDoc '' + It is recommended you keep your secrets separate from the configuration. + It's especially important to keep the raw secrets out of your nix configuration, + as the values will be preserved in your nix store. + This attribute allows you to configure the location of secret files to be loaded at runtime. + + https://www.authelia.com/configuration/methods/secrets/ + ''; + default = { }; + type = types.submodule { + options = { + manual = mkOption { + default = false; + example = true; + description = mdDoc '' + Configuring authelia's secret files via the secrets attribute set + is intended to be convenient and help catch cases where values are required + to run at all. + If a user wants to set these values themselves and bypass the validation they can set this value to true. + ''; + type = types.bool; + }; + + # required + jwtSecretFile = mkOption { + type = types.nullOr types.path; + default = null; + description = mdDoc '' + Path to your JWT secret used during identity verificaiton. + ''; + }; + + oidcIssuerPrivateKeyFile = mkOption { + type = types.nullOr types.path; + default = null; + description = mdDoc '' + Path to your private key file used to encrypt OIDC JWTs. + ''; + }; + + oidcHmacSecretFile = mkOption { + type = types.nullOr types.path; + default = null; + description = mdDoc '' + Path to your HMAC secret used to sign OIDC JWTs. + ''; + }; + + sessionSecretFile = mkOption { + type = types.nullOr types.path; + default = null; + description = mdDoc '' + Path to your session secret. Only used when redis is used as session storage. + ''; + }; + + # required + storageEncryptionKeyFile = mkOption { + type = types.nullOr types.path; + default = null; + description = mdDoc '' + Path to your storage encryption key. + ''; + }; + }; + }; + }; + + environmentVariables = mkOption { + type = types.attrsOf types.str; + description = mdDoc '' + Additional environment variables to provide to authelia. + If you are providing secrets please consider the options under {option}`services.authelia..secrets` + or make sure you use the `_FILE` suffix. + If you provide the raw secret rather than the location of a secret file that secret will be preserved in the nix store. + For more details: https://www.authelia.com/configuration/methods/secrets/ + ''; + default = { }; + }; + + settings = mkOption { + description = mdDoc '' + Your Authelia config.yml as a Nix attribute set. + There are several values that are defined and documented in nix such as `default_2fa_method`, + but additional items can also be included. + + https://github.com/authelia/authelia/blob/master/config.template.yml + ''; + default = { }; + example = '' + { + theme = "light"; + default_2fa_method = "totp"; + log.level = "debug"; + server.disable_healthcheck = true; + } + ''; + type = types.submodule { + freeformType = format.type; + options = { + theme = mkOption { + type = types.enum [ "light" "dark" "grey" "auto" ]; + default = "light"; + example = "dark"; + description = mdDoc "The theme to display."; + }; + + default_2fa_method = mkOption { + type = types.enum [ "" "totp" "webauthn" "mobile_push" ]; + default = ""; + example = "webauthn"; + description = mdDoc '' + Default 2FA method for new users and fallback for preferred but disabled methods. + ''; + }; + + server = { + host = mkOption { + type = types.str; + default = "localhost"; + example = "0.0.0.0"; + description = mdDoc "The address to listen on."; + }; + + port = mkOption { + type = types.port; + default = 9091; + description = mdDoc "The port to listen on."; + }; + }; + + log = { + level = mkOption { + type = types.enum [ "info" "debug" "trace" ]; + default = "debug"; + example = "info"; + description = mdDoc "Level of verbosity for logs: info, debug, trace."; + }; + + format = mkOption { + type = types.enum [ "json" "text" ]; + default = "json"; + example = "text"; + description = mdDoc "Format the logs are written as."; + }; + + file_path = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/log/authelia/authelia.log"; + description = mdDoc "File path where the logs will be written. If not set logs are written to stdout."; + }; + + keep_stdout = mkOption { + type = types.bool; + default = false; + example = true; + description = mdDoc "Whether to also log to stdout when a `file_path` is defined."; + }; + }; + + telemetry = { + metrics = { + enabled = mkOption { + type = types.bool; + default = false; + example = true; + description = mdDoc "Enable Metrics."; + }; + + address = mkOption { + type = types.str; + default = "tcp://127.0.0.1:9959"; + example = "tcp://0.0.0.0:8888"; + description = mdDoc "The address to listen on for metrics. This should be on a different port to the main `server.port` value."; + }; + }; + }; + }; + }; + }; + + settingsFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + example = [ "/etc/authelia/config.yml" "/etc/authelia/access-control.yml" "/etc/authelia/config/" ]; + description = mdDoc '' + Here you can provide authelia with configuration files or directories. + It is possible to give authelia multiple files and use the nix generated configuration + file set via {option}`services.authelia..settings`. + ''; + }; + }; + }; +in +{ + options.services.authelia.instances = with lib; mkOption { + default = { }; + type = types.attrsOf (types.submodule autheliaOpts); + description = mdDoc '' + Multi-domain protection currently requires multiple instances of Authelia. + If you don't require multiple instances of Authelia you can define just the one. + + https://www.authelia.com/roadmap/active/multi-domain-protection/ + ''; + example = '' + { + main = { + enable = true; + secrets.storageEncryptionKeyFile = "/etc/authelia/storageEncryptionKeyFile"; + secrets.jwtSecretFile = "/etc/authelia/jwtSecretFile"; + settings = { + theme = "light"; + default_2fa_method = "totp"; + log.level = "debug"; + server.disable_healthcheck = true; + }; + }; + preprod = { + enable = false; + secrets.storageEncryptionKeyFile = "/mnt/pre-prod/authelia/storageEncryptionKeyFile"; + secrets.jwtSecretFile = "/mnt/pre-prod/jwtSecretFile"; + settings = { + theme = "dark"; + default_2fa_method = "webauthn"; + server.host = "0.0.0.0"; + }; + }; + test.enable = true; + test.secrets.manual = true; + test.settings.theme = "grey"; + test.settings.server.disable_healthcheck = true; + test.settingsFiles = [ "/mnt/test/authelia" "/mnt/test-authelia.conf" ]; + }; + } + ''; + }; + + config = + let + mkInstanceServiceConfig = instance: + let + execCommand = "${instance.package}/bin/authelia"; + configFile = format.generate "config.yml" instance.settings; + configArg = "--config ${builtins.concatStringsSep "," (lib.concatLists [[configFile] instance.settingsFiles])}"; + in + { + description = "Authelia authentication and authorization server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = + (lib.filterAttrs (_: v: v != null) { + AUTHELIA_JWT_SECRET_FILE = instance.secrets.jwtSecretFile; + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = instance.secrets.storageEncryptionKeyFile; + AUTHELIA_SESSION_SECRET_FILE = instance.secrets.sessionSecretFile; + AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = instance.secrets.oidcIssuerPrivateKeyFile; + AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = instance.secrets.oidcHmacSecretFile; + }) + // instance.environmentVariables; + + preStart = "${execCommand} ${configArg} validate-config"; + serviceConfig = { + User = instance.user; + Group = instance.group; + ExecStart = "${execCommand} ${configArg}"; + Restart = "always"; + RestartSec = "5s"; + StateDirectory = "authelia-${instance.name}"; + StateDirectoryMode = "0700"; + + # Security options: + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + DeviceAllow = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + + PrivateTmp = true; + PrivateDevices = true; + PrivateUsers = true; + + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = "read-only"; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "noaccess"; + ProtectSystem = "strict"; + + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + + SystemCallArchitectures = "native"; + SystemCallErrorNumber = "EPERM"; + SystemCallFilter = [ + "@system-service" + "~@cpu-emulation" + "~@debug" + "~@keyring" + "~@memlock" + "~@obsolete" + "~@privileged" + "~@setuid" + ]; + }; + }; + mkInstanceUsersConfig = instance: { + groups."authelia-${instance.name}" = + lib.mkIf (instance.group == "authelia-${instance.name}") { + name = "authelia-${instance.name}"; + }; + users."authelia-${instance.name}" = + lib.mkIf (instance.user == "authelia-${instance.name}") { + name = "authelia-${instance.name}"; + isSystemUser = true; + group = instance.group; + }; + }; + instances = lib.attrValues cfg.instances; + in + { + assertions = lib.flatten (lib.flip lib.mapAttrsToList cfg.instances (name: instance: + [ + { + assertion = instance.secrets.manual || (instance.secrets.jwtSecretFile != null && instance.secrets.storageEncryptionKeyFile != null); + message = '' + Authelia requires a JWT Secret and a Storage Encryption Key to work. + Either set them like so: + services.authelia.${name}.secrets.jwtSecretFile = /my/path/to/jwtsecret; + services.authelia.${name}.secrets.storageEncryptionKeyFile = /my/path/to/encryptionkey; + Or set services.authelia.${name}.secrets.manual = true and provide them yourself via + environmentVariables or settingsFiles. + Do not include raw secrets in nix settings. + ''; + } + ] + )); + + systemd.services = lib.mkMerge + (map + (instance: lib.mkIf instance.enable { + "authelia-${instance.name}" = mkInstanceServiceConfig instance; + }) + instances); + users = lib.mkMerge + (map + (instance: lib.mkIf instance.enable (mkInstanceUsersConfig instance)) + instances); + }; +}