grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / security / authelia.nix
blob1cc137341e11300d39a1dc13de252cab579dda00
1 { lib
2 , pkgs
3 , config
4 , ...
5 }:
7 let
8   cfg = config.services.authelia;
10   format = pkgs.formats.yaml { };
12   autheliaOpts = with lib; { name, ... }: {
13     options = {
14       enable = mkEnableOption "Authelia instance";
16       name = mkOption {
17         type = types.str;
18         default = name;
19         description = ''
20           Name is used as a suffix for the service name, user, and group.
21           By default it takes the value you use for `<instance>` in:
22           {option}`services.authelia.<instance>`
23         '';
24       };
26       package = mkPackageOption pkgs "authelia" { };
28       user = mkOption {
29         default = "authelia-${name}";
30         type = types.str;
31         description = "The name of the user for this authelia instance.";
32       };
34       group = mkOption {
35         default = "authelia-${name}";
36         type = types.str;
37         description = "The name of the group for this authelia instance.";
38       };
40       secrets = mkOption {
41         description = ''
42           It is recommended you keep your secrets separate from the configuration.
43           It's especially important to keep the raw secrets out of your nix configuration,
44           as the values will be preserved in your nix store.
45           This attribute allows you to configure the location of secret files to be loaded at runtime.
47           https://www.authelia.com/configuration/methods/secrets/
48         '';
49         default = { };
50         type = types.submodule {
51           options = {
52             manual = mkOption {
53               default = false;
54               example = true;
55               description = ''
56                 Configuring authelia's secret files via the secrets attribute set
57                 is intended to be convenient and help catch cases where values are required
58                 to run at all.
59                 If a user wants to set these values themselves and bypass the validation they can set this value to true.
60               '';
61               type = types.bool;
62             };
64             # required
65             jwtSecretFile = mkOption {
66               type = types.nullOr types.path;
67               default = null;
68               description = ''
69                 Path to your JWT secret used during identity verificaton.
70               '';
71             };
73             oidcIssuerPrivateKeyFile = mkOption {
74               type = types.nullOr types.path;
75               default = null;
76               description = ''
77                 Path to your private key file used to encrypt OIDC JWTs.
78               '';
79             };
81             oidcHmacSecretFile = mkOption {
82               type = types.nullOr types.path;
83               default = null;
84               description = ''
85                 Path to your HMAC secret used to sign OIDC JWTs.
86               '';
87             };
89             sessionSecretFile = mkOption {
90               type = types.nullOr types.path;
91               default = null;
92               description = ''
93                 Path to your session secret. Only used when redis is used as session storage.
94               '';
95             };
97             # required
98             storageEncryptionKeyFile = mkOption {
99               type = types.nullOr types.path;
100               default = null;
101               description = ''
102                 Path to your storage encryption key.
103               '';
104             };
105           };
106         };
107       };
109       environmentVariables = mkOption {
110         type = types.attrsOf types.str;
111         description = ''
112           Additional environment variables to provide to authelia.
113           If you are providing secrets please consider the options under {option}`services.authelia.<instance>.secrets`
114           or make sure you use the `_FILE` suffix.
115           If you provide the raw secret rather than the location of a secret file that secret will be preserved in the nix store.
116           For more details: https://www.authelia.com/configuration/methods/secrets/
117         '';
118         default = { };
119       };
121       settings = mkOption {
122         description = ''
123           Your Authelia config.yml as a Nix attribute set.
124           There are several values that are defined and documented in nix such as `default_2fa_method`,
125           but additional items can also be included.
127           https://github.com/authelia/authelia/blob/master/config.template.yml
128         '';
129         default = { };
130         example = ''
131           {
132             theme = "light";
133             default_2fa_method = "totp";
134             log.level = "debug";
135             server.disable_healthcheck = true;
136           }
137         '';
138         type = types.submodule {
139           freeformType = format.type;
140           options = {
141             theme = mkOption {
142               type = types.enum [ "light" "dark" "grey" "auto" ];
143               default = "light";
144               example = "dark";
145               description = "The theme to display.";
146             };
148             default_2fa_method = mkOption {
149               type = types.enum [ "" "totp" "webauthn" "mobile_push" ];
150               default = "";
151               example = "webauthn";
152               description = ''
153                 Default 2FA method for new users and fallback for preferred but disabled methods.
154               '';
155             };
157             server = {
158               address = mkOption {
159                 type = types.str;
160                 default = "tcp://:9091/";
161                 example = "unix:///var/run/authelia.sock?path=authelia&umask=0117";
162                 description = "The address to listen on.";
163               };
164             };
166             log = {
167               level = mkOption {
168                 type = types.enum [ "trace" "debug" "info" "warn" "error" ];
169                 default = "debug";
170                 example = "info";
171                 description = "Level of verbosity for logs.";
172               };
174               format = mkOption {
175                 type = types.enum [ "json" "text" ];
176                 default = "json";
177                 example = "text";
178                 description = "Format the logs are written as.";
179               };
181               file_path = mkOption {
182                 type = types.nullOr types.path;
183                 default = null;
184                 example = "/var/log/authelia/authelia.log";
185                 description = "File path where the logs will be written. If not set logs are written to stdout.";
186               };
188               keep_stdout = mkOption {
189                 type = types.bool;
190                 default = false;
191                 example = true;
192                 description = "Whether to also log to stdout when a `file_path` is defined.";
193               };
194             };
196             telemetry = {
197               metrics = {
198                 enabled = mkOption {
199                   type = types.bool;
200                   default = false;
201                   example = true;
202                   description = "Enable Metrics.";
203                 };
205                 address = mkOption {
206                   type = types.str;
207                   default = "tcp://127.0.0.1:9959";
208                   example = "tcp://0.0.0.0:8888";
209                   description = "The address to listen on for metrics. This should be on a different port to the main `server.port` value.";
210                 };
211               };
212             };
213           };
214         };
215       };
217       settingsFiles = mkOption {
218         type = types.listOf types.path;
219         default = [ ];
220         example = [ "/etc/authelia/config.yml" "/etc/authelia/access-control.yml" "/etc/authelia/config/" ];
221         description = ''
222           Here you can provide authelia with configuration files or directories.
223           It is possible to give authelia multiple files and use the nix generated configuration
224           file set via {option}`services.authelia.<instance>.settings`.
225         '';
226       };
227     };
228   };
230   writeOidcJwksConfigFile = oidcIssuerPrivateKeyFile: pkgs.writeText "oidc-jwks.yaml" ''
231     identity_providers:
232       oidc:
233         jwks:
234           - key: {{ secret "${oidcIssuerPrivateKeyFile}" | mindent 10 "|" | msquote }}
235   '';
237   # Remove an attribute in a nested set
238   # https://discourse.nixos.org/t/modify-an-attrset-in-nix/29919/5
239   removeAttrByPath = set: pathList:
240     lib.updateManyAttrsByPath [{
241       path = lib.init pathList;
242       update = old:
243         lib.filterAttrs (n: v: n != (lib.last pathList)) old;
244     }]
245       set;
248   options.services.authelia.instances = with lib; mkOption {
249     default = { };
250     type = types.attrsOf (types.submodule autheliaOpts);
251     description = ''
252       Multi-domain protection currently requires multiple instances of Authelia.
253       If you don't require multiple instances of Authelia you can define just the one.
255       https://www.authelia.com/roadmap/active/multi-domain-protection/
256     '';
257     example = ''
258       {
259         main = {
260           enable = true;
261           secrets.storageEncryptionKeyFile = "/etc/authelia/storageEncryptionKeyFile";
262           secrets.jwtSecretFile = "/etc/authelia/jwtSecretFile";
263           settings = {
264             theme = "light";
265             default_2fa_method = "totp";
266             log.level = "debug";
267             server.disable_healthcheck = true;
268           };
269         };
270         preprod = {
271           enable = false;
272           secrets.storageEncryptionKeyFile = "/mnt/pre-prod/authelia/storageEncryptionKeyFile";
273           secrets.jwtSecretFile = "/mnt/pre-prod/jwtSecretFile";
274           settings = {
275             theme = "dark";
276             default_2fa_method = "webauthn";
277             server.host = "0.0.0.0";
278           };
279         };
280         test.enable = true;
281         test.secrets.manual = true;
282         test.settings.theme = "grey";
283         test.settings.server.disable_healthcheck = true;
284         test.settingsFiles = [ "/mnt/test/authelia" "/mnt/test-authelia.conf" ];
285         };
286       }
287     '';
288   };
290   config =
291     let
292       mkInstanceServiceConfig = instance:
293         let
294           cleanedSettings =
295             if (instance.settings.server?host || instance.settings.server?port || instance.settings.server?path) then
296             # Old settings are used: display a warning and remove the default value of server.address
297             # as authelia does not allow both old and new settings to be set
298               lib.warn "Please replace services.authelia.instances.${instance.name}.settings.{host,port,path} with services.authelia.instances.${instance.name}.settings.address, before release 5.0.0"
299                 (removeAttrByPath instance.settings [ "server" "address" ])
300             else
301               instance.settings;
303           execCommand = "${instance.package}/bin/authelia";
304           configFile = format.generate "config.yml" cleanedSettings;
305           oidcJwksConfigFile = lib.optional (instance.secrets.oidcIssuerPrivateKeyFile != null) (writeOidcJwksConfigFile instance.secrets.oidcIssuerPrivateKeyFile);
306           configArg = "--config ${builtins.concatStringsSep "," (lib.concatLists [[configFile] instance.settingsFiles oidcJwksConfigFile])}";
307         in
308         {
309           description = "Authelia authentication and authorization server";
310           wantedBy = [ "multi-user.target" ];
311           after = [ "network.target" ];
312           environment =
313             (lib.filterAttrs (_: v: v != null) {
314               X_AUTHELIA_CONFIG_FILTERS = lib.mkIf (oidcJwksConfigFile != [ ]) "template";
315               AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE = instance.secrets.jwtSecretFile;
316               AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = instance.secrets.storageEncryptionKeyFile;
317               AUTHELIA_SESSION_SECRET_FILE = instance.secrets.sessionSecretFile;
318               AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = instance.secrets.oidcHmacSecretFile;
319             })
320             // instance.environmentVariables;
322           preStart = "${execCommand} ${configArg} validate-config";
323           serviceConfig = {
324             User = instance.user;
325             Group = instance.group;
326             ExecStart = "${execCommand} ${configArg}";
327             Restart = "always";
328             RestartSec = "5s";
329             StateDirectory = "authelia-${instance.name}";
330             StateDirectoryMode = "0700";
332             # Security options:
333             AmbientCapabilities = "";
334             CapabilityBoundingSet = "";
335             DeviceAllow = "";
336             LockPersonality = true;
337             MemoryDenyWriteExecute = true;
338             NoNewPrivileges = true;
340             PrivateTmp = true;
341             PrivateDevices = true;
342             PrivateUsers = true;
344             ProtectClock = true;
345             ProtectControlGroups = true;
346             ProtectHome = "read-only";
347             ProtectHostname = true;
348             ProtectKernelLogs = true;
349             ProtectKernelModules = true;
350             ProtectKernelTunables = true;
351             ProtectProc = "noaccess";
352             ProtectSystem = "strict";
354             RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
355             RestrictNamespaces = true;
356             RestrictRealtime = true;
357             RestrictSUIDSGID = true;
359             SystemCallArchitectures = "native";
360             SystemCallErrorNumber = "EPERM";
361             SystemCallFilter = [
362               "@system-service"
363               "~@cpu-emulation"
364               "~@debug"
365               "~@keyring"
366               "~@memlock"
367               "~@obsolete"
368               "~@privileged"
369               "~@setuid"
370             ];
371           };
372         };
373       mkInstanceUsersConfig = instance: {
374         groups."authelia-${instance.name}" =
375           lib.mkIf (instance.group == "authelia-${instance.name}") {
376             name = "authelia-${instance.name}";
377           };
378         users."authelia-${instance.name}" =
379           lib.mkIf (instance.user == "authelia-${instance.name}") {
380             name = "authelia-${instance.name}";
381             isSystemUser = true;
382             group = instance.group;
383           };
384       };
385       instances = lib.attrValues cfg.instances;
386     in
387     {
388       assertions = lib.flatten (lib.flip lib.mapAttrsToList cfg.instances (name: instance:
389         [
390           {
391             assertion = instance.secrets.manual || (instance.secrets.jwtSecretFile != null && instance.secrets.storageEncryptionKeyFile != null);
392             message = ''
393               Authelia requires a JWT Secret and a Storage Encryption Key to work.
394               Either set them like so:
395               services.authelia.${name}.secrets.jwtSecretFile = /my/path/to/jwtsecret;
396               services.authelia.${name}.secrets.storageEncryptionKeyFile = /my/path/to/encryptionkey;
397               Or set services.authelia.${name}.secrets.manual = true and provide them yourself via
398               environmentVariables or settingsFiles.
399               Do not include raw secrets in nix settings.
400             '';
401           }
402         ]
403       ));
405       systemd.services = lib.mkMerge
406         (map
407           (instance: lib.mkIf instance.enable {
408             "authelia-${instance.name}" = mkInstanceServiceConfig instance;
409           })
410           instances);
411       users = lib.mkMerge
412         (map
413           (instance: lib.mkIf instance.enable (mkInstanceUsersConfig instance))
414           instances);
415     };