nixpkgs/nixos/modules/services/security/oauth2_proxy.nix
2017-10-06 16:52:22 -07:00

520 lines
15 KiB
Nix

# NixOS module for oauth2_proxy.
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.oauth2_proxy;
# Use like:
# repeatedArgs (arg: "--arg=${arg}") args
repeatedArgs = concatMapStringsSep " ";
# oauth2_proxy provides many options that are only relevant if you are using
# a certain provider. This set maps from provider name to a function that
# takes the configuration and returns a string that can be inserted into the
# command-line to launch oauth2_proxy.
providerSpecificOptions = {
azure = cfg: ''
--azure-tenant=${cfg.azure.tenant} \
--resource=${cfg.azure.resource} \
'';
github = cfg: ''
${optionalString (!isNull cfg.github.org) "--github-org=${cfg.github.org}"} \
${optionalString (!isNull cfg.github.team) "--github-org=${cfg.github.team}"} \
'';
google = cfg: ''
--google-admin-email=${cfg.google.adminEmail} \
--google-service-account=${cfg.google.serviceAccountJSON} \
${repeatedArgs (group: "--google-group=${group}") cfg.google.groups} \
'';
};
authenticatedEmailsFile = pkgs.writeText "authenticated-emails" cfg.email.addresses;
getProviderOptions = cfg: provider: providerSpecificOptions.${provider} or (_: "") cfg;
mkCommandLine = cfg: ''
--provider='${cfg.provider}' \
${optionalString (!isNull cfg.email.addresses) "--authenticated-emails-file='${authenticatedEmailsFile}'"} \
--approval-prompt='${cfg.approvalPrompt}' \
${optionalString (cfg.passBasicAuth && !isNull cfg.basicAuthPassword) "--basic-auth-password='${cfg.basicAuthPassword}'"} \
--client-id='${cfg.clientID}' \
--client-secret='${cfg.clientSecret}' \
${optionalString (!isNull cfg.cookie.domain) "--cookie-domain='${cfg.cookie.domain}'"} \
--cookie-expire='${cfg.cookie.expire}' \
--cookie-httponly=${boolToString cfg.cookie.httpOnly} \
--cookie-name='${cfg.cookie.name}' \
--cookie-secret='${cfg.cookie.secret}' \
--cookie-secure=${boolToString cfg.cookie.secure} \
${optionalString (!isNull cfg.cookie.refresh) "--cookie-refresh='${cfg.cookie.refresh}'"} \
${optionalString (!isNull cfg.customTemplatesDir) "--custom-templates-dir='${cfg.customTemplatesDir}'"} \
${repeatedArgs (x: "--email-domain='${x}'") cfg.email.domains} \
--http-address='${cfg.httpAddress}' \
${optionalString (!isNull cfg.htpasswd.file) "--htpasswd-file='${cfg.htpasswd.file}' --display-htpasswd-form=${boolToString cfg.htpasswd.displayForm}"} \
${optionalString (!isNull cfg.loginURL) "--login-url='${cfg.loginURL}'"} \
--pass-access-token=${boolToString cfg.passAccessToken} \
--pass-basic-auth=${boolToString cfg.passBasicAuth} \
--pass-host-header=${boolToString cfg.passHostHeader} \
--proxy-prefix='${cfg.proxyPrefix}' \
${optionalString (!isNull cfg.profileURL) "--profile-url='${cfg.profileURL}'"} \
${optionalString (!isNull cfg.redeemURL) "--redeem-url='${cfg.redeemURL}'"} \
${optionalString (!isNull cfg.redirectURL) "--redirect-url='${cfg.redirectURL}'"} \
--request-logging=${boolToString cfg.requestLogging} \
${optionalString (!isNull cfg.scope) "--scope='${cfg.scope}'"} \
${repeatedArgs (x: "--skip-auth-regex='${x}'") cfg.skipAuthRegexes} \
${optionalString (!isNull cfg.signatureKey) "--signature-key='${cfg.signatureKey}'"} \
--upstream='${cfg.upstream}' \
${optionalString (!isNull cfg.validateURL) "--validate-url='${cfg.validateURL}'"} \
${optionalString cfg.tls.enable "--tls-cert='${cfg.tls.certificate}' --tls-key='${cfg.tls.key}' --https-address='${cfg.tls.httpsAddress}'"} \
'' + getProviderOptions cfg cfg.provider;
in
{
options.services.oauth2_proxy = {
enable = mkEnableOption "oauth2_proxy";
package = mkOption {
type = types.package;
default = pkgs.oauth2_proxy;
defaultText = "pkgs.oauth2_proxy";
description = ''
The package that provides oauth2_proxy.
'';
};
##############################################
# PROVIDER configuration
provider = mkOption {
type = types.enum [
"google"
"github"
"azure"
"gitlab"
"linkedin"
"myusa"
];
default = "google";
description = ''
OAuth provider.
'';
};
approvalPrompt = mkOption {
type = types.enum ["force" "auto"];
default = "force";
description = ''
OAuth approval_prompt.
'';
};
clientID = mkOption {
type = types.str;
description = ''
The OAuth Client ID.
'';
example = "123456.apps.googleusercontent.com";
};
clientSecret = mkOption {
type = types.str;
description = ''
The OAuth Client Secret.
'';
};
skipAuthRegexes = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Skip authentication for requests matching any of these regular
expressions.
'';
};
# XXX: Not clear whether these two options are mutually exclusive or not.
email = {
domains = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Authenticate emails with the specified domains. Use
<literal>*</literal> to authenticate any email.
'';
};
addresses = mkOption {
type = types.nullOr types.lines;
default = null;
description = ''
Line-separated email addresses that are allowed to authenticate.
'';
};
};
loginURL = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Authentication endpoint.
You only need to set this if you are using a self-hosted provider (e.g.
Github Enterprise). If you're using a publicly hosted provider
(e.g github.com), then the default works.
'';
example = "https://provider.example.com/oauth/authorize";
};
redeemURL = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Token redemption endpoint.
You only need to set this if you are using a self-hosted provider (e.g.
Github Enterprise). If you're using a publicly hosted provider
(e.g github.com), then the default works.
'';
example = "https://provider.example.com/oauth/token";
};
validateURL = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Access token validation endpoint.
You only need to set this if you are using a self-hosted provider (e.g.
Github Enterprise). If you're using a publicly hosted provider
(e.g github.com), then the default works.
'';
example = "https://provider.example.com/user/emails";
};
redirectURL = mkOption {
# XXX: jml suspects this is always necessary, but the command-line
# doesn't require it so making it optional.
type = types.nullOr types.str;
default = null;
description = ''
The OAuth2 redirect URL.
'';
example = "https://internalapp.yourcompany.com/oauth2/callback";
};
azure = {
tenant = mkOption {
type = types.str;
default = "common";
description = ''
Go to a tenant-specific or common (tenant-independent) endpoint.
'';
};
resource = mkOption {
type = types.str;
description = ''
The resource that is protected.
'';
};
};
google = {
adminEmail = mkOption {
type = types.str;
description = ''
The Google Admin to impersonate for API calls.
Only users with access to the Admin APIs can access the Admin SDK
Directory API, thus the service account needs to impersonate one of
those users to access the Admin SDK Directory API.
See <link xlink:href="https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account" />.
'';
};
groups = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Restrict logins to members of these Google groups.
'';
};
serviceAccountJSON = mkOption {
type = types.path;
description = ''
The path to the service account JSON credentials.
'';
};
};
github = {
org = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Restrict logins to members of this organisation.
'';
};
team = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Restrict logins to members of this team.
'';
};
};
####################################################
# UPSTREAM Configuration
upstream = mkOption {
type = types.commas;
description = ''
The http url(s) of the upstream endpoint or <literal>file://</literal>
paths for static files. Routing is based on the path.
'';
};
passAccessToken = mkOption {
type = types.bool;
default = false;
description = ''
Pass OAuth access_token to upstream via X-Forwarded-Access-Token header.
'';
};
passBasicAuth = mkOption {
type = types.bool;
default = true;
description = ''
Pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream.
'';
};
basicAuthPassword = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
The password to set when passing the HTTP Basic Auth header.
'';
};
passHostHeader = mkOption {
type = types.bool;
default = true;
description = ''
Pass the request Host Header to upstream.
'';
};
signatureKey = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
GAP-Signature request signature key.
'';
example = "sha1:secret0";
};
cookie = {
domain = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
An optional cookie domain to force cookies to.
'';
example = ".yourcompany.com";
};
expire = mkOption {
type = types.str;
default = "168h0m0s";
description = ''
Expire timeframe for cookie.
'';
};
httpOnly = mkOption {
type = types.bool;
default = true;
description = ''
Set HttpOnly cookie flag.
'';
};
name = mkOption {
type = types.str;
default = "_oauth2_proxy";
description = ''
The name of the cookie that the oauth_proxy creates.
'';
};
refresh = mkOption {
# XXX: Unclear what the behavior is when this is not specified.
type = types.nullOr types.str;
default = null;
description = ''
Refresh the cookie after this duration; 0 to disable.
'';
example = "168h0m0s";
};
secret = mkOption {
type = types.str;
description = ''
The seed string for secure cookies.
'';
};
secure = mkOption {
type = types.bool;
default = true;
description = ''
Set secure (HTTPS) cookie flag.
'';
};
};
####################################################
# OAUTH2 PROXY configuration
httpAddress = mkOption {
type = types.str;
default = "http://127.0.0.1:4180";
description = ''
HTTPS listening address. This module does not expose the port by
default. If you want this URL to be accessible to other machines, please
add the port to <literal>networking.firewall.allowedTCPPorts</literal>.
'';
};
htpasswd = {
file = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Additionally authenticate against a htpasswd file. Entries must be
created with <literal>htpasswd -s</literal> for SHA encryption.
'';
};
displayForm = mkOption {
type = types.bool;
default = true;
description = ''
Display username / password login form if an htpasswd file is provided.
'';
};
};
customTemplatesDir = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to custom HTML templates.
'';
};
proxyPrefix = mkOption {
type = types.str;
default = "/oauth2";
description = ''
The url root path that this proxy should be nested under.
'';
};
tls = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to serve over TLS.
'';
};
certificate = mkOption {
type = types.path;
description = ''
Path to certificate file.
'';
};
key = mkOption {
type = types.path;
description = ''
Path to private key file.
'';
};
httpsAddress = mkOption {
type = types.str;
default = ":443";
description = ''
<literal>addr:port</literal> to listen on for HTTPS clients.
Remember to add <literal>port</literal> to
<literal>allowedTCPPorts</literal> if you want other machines to be
able to connect to it.
'';
};
};
requestLogging = mkOption {
type = types.bool;
default = true;
description = ''
Log requests to stdout.
'';
};
####################################################
# UNKNOWN
# XXX: Is this mandatory? Is it part of another group? Is it part of the provider specification?
scope = mkOption {
# XXX: jml suspects this is always necessary, but the command-line
# doesn't require it so making it optional.
type = types.nullOr types.str;
default = null;
description = ''
OAuth scope specification.
'';
};
profileURL = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Profile access endpoint.
'';
};
};
config = mkIf cfg.enable {
users.extraUsers.oauth2_proxy = {
description = "OAuth2 Proxy";
};
systemd.services.oauth2_proxy = {
description = "OAuth2 Proxy";
path = [ cfg.package ];
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
User = "oauth2_proxy";
Restart = "always";
ExecStart = "${cfg.package.bin}/bin/oauth2_proxy ${mkCommandLine cfg}";
};
};
};
}