lol

Merge pull request #15283 from jml/oauth2proxy-moduleu

oauth2_proxy: create new module for service

authored by

Joachim Fasting and committed by
GitHub
e2e2840a 3a4ff5fc

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