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