grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / security / oauth2-proxy.nix
bloba897f04ea6333aa534906a889cc327850910c74b
1 { config, lib, pkgs, ... }:
3 let
4   cfg = config.services.oauth2-proxy;
6   # oauth2-proxy provides many options that are only relevant if you are using
7   # a certain provider. This set maps from provider name to a function that
8   # takes the configuration and returns a string that can be inserted into the
9   # command-line to launch oauth2-proxy.
10   providerSpecificOptions = {
11     azure = cfg: {
12       azure-tenant = cfg.azure.tenant;
13       resource = cfg.azure.resource;
14     };
16     github = cfg: { github = {
17       inherit (cfg.github) org team;
18     }; };
20     google = cfg: { google = with cfg.google; lib.optionalAttrs (groups != []) {
21       admin-email = adminEmail;
22       service-account = serviceAccountJSON;
23       group = groups;
24     }; };
25   };
27   authenticatedEmailsFile = pkgs.writeText "authenticated-emails" cfg.email.addresses;
29   getProviderOptions = cfg: provider: providerSpecificOptions.${provider} or (_: {}) cfg;
31   allConfig = with cfg; {
32     inherit (cfg) provider scope upstream;
33     approval-prompt = approvalPrompt;
34     basic-auth-password = basicAuthPassword;
35     client-id = clientID;
36     client-secret = clientSecret;
37     custom-templates-dir = customTemplatesDir;
38     email-domain = email.domains;
39     http-address = httpAddress;
40     login-url = loginURL;
41     pass-access-token = passAccessToken;
42     pass-basic-auth = passBasicAuth;
43     pass-host-header = passHostHeader;
44     reverse-proxy = reverseProxy;
45     proxy-prefix = proxyPrefix;
46     profile-url = profileURL;
47     oidc-issuer-url = oidcIssuerUrl;
48     redeem-url = redeemURL;
49     redirect-url = redirectURL;
50     request-logging = requestLogging;
51     skip-auth-regex = skipAuthRegexes;
52     signature-key = signatureKey;
53     validate-url = validateURL;
54     htpasswd-file = htpasswd.file;
55     cookie = {
56       inherit (cookie) domain secure expire name secret refresh;
57       httponly = cookie.httpOnly;
58     };
59     set-xauthrequest = setXauthrequest;
60   } // lib.optionalAttrs (cfg.email.addresses != null) {
61     authenticated-emails-file = authenticatedEmailsFile;
62   } // lib.optionalAttrs (cfg.passBasicAuth) {
63     basic-auth-password = cfg.basicAuthPassword;
64   } // lib.optionalAttrs (cfg.htpasswd.file != null) {
65     display-htpasswd-file = cfg.htpasswd.displayForm;
66   } // lib.optionalAttrs tls.enable {
67     tls-cert-file = tls.certificate;
68     tls-key-file = tls.key;
69     https-address = tls.httpsAddress;
70   } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
72   mapConfig = key: attr:
73   lib.optionalString (attr != null && attr != []) (
74     if lib.isDerivation attr then mapConfig key (toString attr) else
75     if (builtins.typeOf attr) == "set" then lib.concatStringsSep " "
76       (lib.mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else
77     if (builtins.typeOf attr) == "list" then lib.concatMapStringsSep " " (mapConfig key) attr else
78     if (builtins.typeOf attr) == "bool" then "--${key}=${lib.boolToString attr}" else
79     if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else
80     "--${key}=${toString attr}");
82   configString = lib.concatStringsSep " " (lib.mapAttrsToList mapConfig allConfig);
85   options.services.oauth2-proxy = {
86     enable = lib.mkEnableOption "oauth2-proxy";
88     package = lib.mkPackageOption pkgs "oauth2-proxy" { };
90     ##############################################
91     # PROVIDER configuration
92     # Taken from: https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go
93     provider = lib.mkOption {
94       type = lib.types.enum [
95         "adfs"
96         "azure"
97         "bitbucket"
98         "digitalocean"
99         "facebook"
100         "github"
101         "gitlab"
102         "google"
103         "keycloak"
104         "keycloak-oidc"
105         "linkedin"
106         "login.gov"
107         "nextcloud"
108         "oidc"
109       ];
110       default = "google";
111       description = ''
112         OAuth provider.
113       '';
114     };
116     approvalPrompt = lib.mkOption {
117       type = lib.types.enum ["force" "auto"];
118       default = "force";
119       description = ''
120         OAuth approval_prompt.
121       '';
122     };
124     clientID = lib.mkOption {
125       type = lib.types.nullOr lib.types.str;
126       description = ''
127         The OAuth Client ID.
128       '';
129       example = "123456.apps.googleusercontent.com";
130     };
132     oidcIssuerUrl = lib.mkOption {
133       type = lib.types.nullOr lib.types.str;
134       default = null;
135       description = ''
136         The OAuth issuer URL.
137       '';
138       example = "https://login.microsoftonline.com/{TENANT_ID}/v2.0";
139     };
141     clientSecret = lib.mkOption {
142       type = lib.types.nullOr lib.types.str;
143       description = ''
144         The OAuth Client Secret.
145       '';
146     };
148     skipAuthRegexes = lib.mkOption {
149      type = lib.types.listOf lib.types.str;
150      default = [];
151      description = ''
152        Skip authentication for requests matching any of these regular
153        expressions.
154      '';
155     };
157     # XXX: Not clear whether these two options are mutually exclusive or not.
158     email = {
159       domains = lib.mkOption {
160         type = lib.types.listOf lib.types.str;
161         default = [];
162         description = ''
163           Authenticate emails with the specified domains. Use
164           `*` to authenticate any email.
165         '';
166       };
168       addresses = lib.mkOption {
169         type = lib.types.nullOr lib.types.lines;
170         default = null;
171         description = ''
172           Line-separated email addresses that are allowed to authenticate.
173         '';
174       };
175     };
177     loginURL = lib.mkOption {
178       type = lib.types.nullOr lib.types.str;
179       default = null;
180       description = ''
181         Authentication endpoint.
183         You only need to set this if you are using a self-hosted provider (e.g.
184         Github Enterprise). If you're using a publicly hosted provider
185         (e.g github.com), then the default works.
186       '';
187       example = "https://provider.example.com/oauth/authorize";
188     };
190     redeemURL = lib.mkOption {
191       type = lib.types.nullOr lib.types.str;
192       default = null;
193       description = ''
194         Token redemption endpoint.
196         You only need to set this if you are using a self-hosted provider (e.g.
197         Github Enterprise). If you're using a publicly hosted provider
198         (e.g github.com), then the default works.
199       '';
200       example = "https://provider.example.com/oauth/token";
201     };
203     validateURL = lib.mkOption {
204       type = lib.types.nullOr lib.types.str;
205       default = null;
206       description = ''
207         Access token validation endpoint.
209         You only need to set this if you are using a self-hosted provider (e.g.
210         Github Enterprise). If you're using a publicly hosted provider
211         (e.g github.com), then the default works.
212       '';
213       example = "https://provider.example.com/user/emails";
214     };
216     redirectURL = lib.mkOption {
217       # XXX: jml suspects this is always necessary, but the command-line
218       # doesn't require it so making it optional.
219       type = lib.types.nullOr lib.types.str;
220       default = null;
221       description = ''
222         The OAuth2 redirect URL.
223       '';
224       example = "https://internalapp.yourcompany.com/oauth2/callback";
225     };
227     azure = {
228       tenant = lib.mkOption {
229         type = lib.types.str;
230         default = "common";
231         description = ''
232           Go to a tenant-specific or common (tenant-independent) endpoint.
233         '';
234       };
236       resource = lib.mkOption {
237         type = lib.types.str;
238         description = ''
239           The resource that is protected.
240         '';
241       };
242     };
244     google = {
245       adminEmail = lib.mkOption {
246         type = lib.types.str;
247         description = ''
248           The Google Admin to impersonate for API calls.
250           Only users with access to the Admin APIs can access the Admin SDK
251           Directory API, thus the service account needs to impersonate one of
252           those users to access the Admin SDK Directory API.
254           See <https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account>.
255         '';
256       };
258       groups = lib.mkOption {
259         type = lib.types.listOf lib.types.str;
260         default = [];
261         description = ''
262           Restrict logins to members of these Google groups.
263         '';
264       };
266       serviceAccountJSON = lib.mkOption {
267         type = lib.types.path;
268         description = ''
269           The path to the service account JSON credentials.
270         '';
271       };
272     };
274     github = {
275       org = lib.mkOption {
276         type = lib.types.nullOr lib.types.str;
277         default = null;
278         description = ''
279           Restrict logins to members of this organisation.
280         '';
281       };
283       team = lib.mkOption {
284         type = lib.types.nullOr lib.types.str;
285         default = null;
286         description = ''
287           Restrict logins to members of this team.
288         '';
289       };
290     };
293     ####################################################
294     # UPSTREAM Configuration
295     upstream = lib.mkOption {
296       type = with lib.types; coercedTo str (x: [x]) (listOf str);
297       default = [];
298       description = ''
299         The http url(s) of the upstream endpoint or `file://`
300         paths for static files. Routing is based on the path.
301       '';
302     };
304     passAccessToken = lib.mkOption {
305       type = lib.types.bool;
306       default = false;
307       description = ''
308         Pass OAuth access_token to upstream via X-Forwarded-Access-Token header.
309       '';
310     };
312     passBasicAuth = lib.mkOption {
313       type = lib.types.bool;
314       default = true;
315       description = ''
316         Pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream.
317       '';
318     };
320     basicAuthPassword = lib.mkOption {
321       type = lib.types.nullOr lib.types.str;
322       default = null;
323       description = ''
324         The password to set when passing the HTTP Basic Auth header.
325       '';
326     };
328     passHostHeader = lib.mkOption {
329       type = lib.types.bool;
330       default = true;
331       description = ''
332         Pass the request Host Header to upstream.
333       '';
334     };
336     signatureKey = lib.mkOption {
337       type = lib.types.nullOr lib.types.str;
338       default = null;
339       description = ''
340         GAP-Signature request signature key.
341       '';
342       example = "sha1:secret0";
343     };
345     cookie = {
346       domain = lib.mkOption {
347         type = lib.types.nullOr lib.types.str;
348         default = null;
349         description = ''
350           Optional cookie domains to force cookies to (ie: `.yourcompany.com`).
351           The longest domain matching the request's host will be used (or the shortest
352           cookie domain if there is no match).
353         '';
354         example = ".yourcompany.com";
355       };
357       expire = lib.mkOption {
358         type = lib.types.str;
359         default = "168h0m0s";
360         description = ''
361           Expire timeframe for cookie.
362         '';
363       };
365       httpOnly = lib.mkOption {
366         type = lib.types.bool;
367         default = true;
368         description = ''
369           Set HttpOnly cookie flag.
370         '';
371       };
373       name = lib.mkOption {
374         type = lib.types.str;
375         default = "_oauth2_proxy";
376         description = ''
377           The name of the cookie that the oauth_proxy creates.
378         '';
379       };
381       refresh = lib.mkOption {
382         # XXX: Unclear what the behavior is when this is not specified.
383         type = lib.types.nullOr lib.types.str;
384         default = null;
385         description = ''
386           Refresh the cookie after this duration; 0 to disable.
387         '';
388         example = "168h0m0s";
389       };
391       secret = lib.mkOption {
392         type = lib.types.nullOr lib.types.str;
393         description = ''
394           The seed string for secure cookies.
395         '';
396       };
398       secure = lib.mkOption {
399         type = lib.types.bool;
400         default = true;
401         description = ''
402           Set secure (HTTPS) cookie flag.
403         '';
404       };
405     };
407     ####################################################
408     # OAUTH2 PROXY configuration
410     httpAddress = lib.mkOption {
411       type = lib.types.str;
412       default = "http://127.0.0.1:4180";
413       description = ''
414         HTTPS listening address.  This module does not expose the port by
415         default. If you want this URL to be accessible to other machines, please
416         add the port to `networking.firewall.allowedTCPPorts`.
417       '';
418     };
420     htpasswd = {
421       file = lib.mkOption {
422         type = lib.types.nullOr lib.types.path;
423         default = null;
424         description = ''
425           Additionally authenticate against a htpasswd file. Entries must be
426           created with `htpasswd -s` for SHA encryption.
427         '';
428       };
430       displayForm = lib.mkOption {
431         type = lib.types.bool;
432         default = true;
433         description = ''
434           Display username / password login form if an htpasswd file is provided.
435         '';
436       };
437     };
439     customTemplatesDir = lib.mkOption {
440       type = lib.types.nullOr lib.types.path;
441       default = null;
442       description = ''
443         Path to custom HTML templates.
444       '';
445     };
447     reverseProxy = lib.mkOption {
448       type = lib.types.bool;
449       default = false;
450       description = ''
451         In case when running behind a reverse proxy, controls whether headers
452         like `X-Real-Ip` are accepted. Usage behind a reverse
453         proxy will require this flag to be set to avoid logging the reverse
454         proxy IP address.
455       '';
456     };
458     proxyPrefix = lib.mkOption {
459       type = lib.types.str;
460       default = "/oauth2";
461       description = ''
462         The url root path that this proxy should be nested under.
463       '';
464     };
466     tls = {
467       enable = lib.mkOption {
468         type = lib.types.bool;
469         default = false;
470         description = ''
471           Whether to serve over TLS.
472         '';
473       };
475       certificate = lib.mkOption {
476         type = lib.types.path;
477         description = ''
478           Path to certificate file.
479         '';
480       };
482       key = lib.mkOption {
483         type = lib.types.path;
484         description = ''
485           Path to private key file.
486         '';
487       };
489       httpsAddress = lib.mkOption {
490         type = lib.types.str;
491         default = ":443";
492         description = ''
493           `addr:port` to listen on for HTTPS clients.
495           Remember to add `port` to
496           `allowedTCPPorts` if you want other machines to be
497           able to connect to it.
498         '';
499       };
500     };
502     requestLogging = lib.mkOption {
503       type = lib.types.bool;
504       default = true;
505       description = ''
506         Log requests to stdout.
507       '';
508     };
510     ####################################################
511     # UNKNOWN
513     # XXX: Is this mandatory? Is it part of another group? Is it part of the provider specification?
514     scope = lib.mkOption {
515       # XXX: jml suspects this is always necessary, but the command-line
516       # doesn't require it so making it optional.
517       type = lib.types.nullOr lib.types.str;
518       default = null;
519       description = ''
520         OAuth scope specification.
521       '';
522     };
524     profileURL = lib.mkOption {
525       type = lib.types.nullOr lib.types.str;
526       default = null;
527       description = ''
528         Profile access endpoint.
529       '';
530     };
532     setXauthrequest = lib.mkOption {
533       type = lib.types.nullOr lib.types.bool;
534       default = false;
535       description = ''
536         Set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode). Setting this to 'null' means using the upstream default (false).
537       '';
538     };
540     extraConfig = lib.mkOption {
541       default = {};
542       type = lib.types.attrsOf lib.types.anything;
543       description = ''
544         Extra config to pass to oauth2-proxy.
545       '';
546     };
548     keyFile = lib.mkOption {
549       type = lib.types.nullOr lib.types.path;
550       default = null;
551       description = ''
552         oauth2-proxy allows passing sensitive configuration via environment variables.
553         Make a file that contains lines like
554         OAUTH2_PROXY_CLIENT_SECRET=asdfasdfasdf.apps.googleuserscontent.com
555         and specify the path here.
556       '';
557       example = "/run/keys/oauth2-proxy";
558     };
559   };
561   imports = [
562     (lib.mkRenamedOptionModule [ "services" "oauth2_proxy" ] [ "services" "oauth2-proxy" ])
563   ];
565   config = lib.mkIf cfg.enable {
566     services.oauth2-proxy = lib.mkIf (cfg.keyFile != null) {
567       clientID = lib.mkDefault null;
568       clientSecret = lib.mkDefault null;
569       cookie.secret = lib.mkDefault null;
570     };
572     users.users.oauth2-proxy = {
573       description = "OAuth2 Proxy";
574       isSystemUser = true;
575       group = "oauth2-proxy";
576     };
578     users.groups.oauth2-proxy = {};
580     systemd.services.oauth2-proxy =
581       let needsKeycloak = lib.elem cfg.provider ["keycloak" "keycloak-oidc"]
582                           && config.services.keycloak.enable;
583       in {
584         description = "OAuth2 Proxy";
585         path = [ cfg.package ];
586         wantedBy = [ "multi-user.target" ];
587         wants = [ "network-online.target" ] ++ lib.optionals needsKeycloak [ "keycloak.service" ];
588         after = [ "network-online.target" ] ++ lib.optionals needsKeycloak [ "keycloak.service" ];
589         restartTriggers = [ cfg.keyFile ];
590         serviceConfig = {
591           User = "oauth2-proxy";
592           Restart = "always";
593           ExecStart = "${lib.getExe cfg.package} ${configString}";
594           EnvironmentFile = lib.mkIf (cfg.keyFile != null) cfg.keyFile;
595         };
596       };
597   };