at master 18 kB view raw
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}