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