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