depot/nix/pkgs/pomerium/module.nix

634 lines
32 KiB
Nix

{ depot, config, lib, pkgs, ... }:
with lib;
let
goDuration = types.mkOptionType {
name = "goDuration";
description = "Go duration (https://golang.org/pkg/time/#ParseDuration)";
check = x: types.str.check x && builtins.match "((-?[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+|0)" x != null;
inherit (types.str) merge;
};
in
{
options.services.pomerium = {
enable = mkEnableOption "the Pomerium authenticating reverse proxy";
bindLowPort = mkOption {
type = with types; bool;
default = true;
description = "If true, allows Pomerium to bind low-numbered ports (e.g. 80 and 443).";
};
configFile = mkOption {
type = with types; nullOr path;
default = null;
description = "Path to Pomerium config file.";
};
config = mkOption {
description = ''
The contents of Pomerium's config.yaml, in Nix expressions.
Specifying configFile will override this in its entirety.
'';
default = {};
type = with types; submodule {
options = {
# Shared Settings
address = mkOption {
type = with types; str;
default = ":443";
description = "Address specifies the host and port to serve HTTP requests from. If empty, :443 is used. Note, in all-in-one deployments, gRPC traffic will be served on loopback on port :5443.";
};
administrators = mkOption {
type = with types; listOf str;
default = [];
description = "Administrative users are super users (opens new window)that can sign-in as another user or group. User impersonation allows administrators to temporarily impersonate a different user.";
};
enable_user_impersonation = mkOption {
type = with types; bool;
default = false;
description = "Enabling user impersonation allows administrators to impersonate other user accounts. Prior to v0.11.0 this feature was enabled by default. It is now disabled by default.";
};
autocert = mkOption {
type = with types; bool;
default = false;
description = "Turning on autocert allows Pomerium to automatically retrieve, manage, and renew public facing TLS certificates from Let's Encrypt (opens new window)which includes managed routes and the authenticate service.";
};
autocert_must_staple = mkOption {
type = with types; bool;
default = true;
description = "If true, force autocert to request a certificate with the status_request extension (commonly called Must-Staple).";
};
autocert_dir = mkOption {
type = with types; path;
default = "/var/lib/pomerium/autocert";
description = "Autocert directory is the path which autocert will store x509 certificate data.";
};
autocert_use_staging = mkOption {
type = with types; bool;
default = false;
description = "Enabling this setting allows you to use Let's Encrypt's staging environment (opens new window)which has much more lax usage limits.";
};
# certificate/certificate_key, certificate_file/certificate_key_file deliberately not provided.
certificates = mkOption {
default = null;
description = "Certificates are the x509 public-key and private-key used to establish secure HTTP and gRPC connections. You can also use any of these settings in conjuction with Autocert to get OCSP stapling.";
type = with types; nullOr (listOf (submodule {
options.cert = mkOption {
type = with types; path;
description = "Path to x509 public-key";
};
options.key = mkOption {
type = with types; path;
description = "Path to x509 private-key";
};
}));
};
# client_ca deliberately not provided.
client_ca_file = mkOption {
type = with types; nullOr path;
default = null;
description = "The Client Certificate Authority is the x509 public-key used to validate mTLS client certificates. If not set, no client certificate will be required.";
};
cookie_name = mkOption {
type = with types; str;
default = "_pomerium";
description = "The name of the session cookie sent to clients";
};
# cookie_secret not provided; use COOKIE_SECRET instead.
cookie_domain = mkOption {
type = with types; nullOr str;
default = null;
description = "The scope of session cookies issued by Pomerium";
};
cookie_secure = mkOption {
type = with types; bool;
default = true;
description = "If true, instructs browsers to only send user session cookies over HTTPS.";
};
cookie_http_only = mkOption {
type = with types; bool;
default = true;
description = "If true, prevents JavaScript in browsers from reading user session cookies.";
};
cookie_expire = mkOption {
type = with types; goDuration;
default = "14h";
description = "Sets the lifetime of session cookies. After this interval, users must reauthenticate.";
};
pomerium_debug = mkOption {
type = with types; bool;
default = false;
description = "By default, JSON encoded logs are produced. Debug enables coloured, human-readable logs to be streamed to standard out.";
};
forward_auth_url = mkOption {
type = with types; nullOr str;
default = null;
description = "Forward authentication creates an endpoint that can be used with third-party proxies that do not have rich access control capabilities. Forward authentication allows you to delegate authentication and authorization for each request to Pomerium.";
};
timeout_read = mkOption {
type = with types; nullOr goDuration;
default = null;
description = "Sets the global read timeout (i.e. the time from when the connection is accepted to when the request body is fully read).";
};
timeout_write = mkOption {
type = with types; nullOr goDuration;
default = null;
description = "Sets the global write timeout (i.e. the time from when the request body is read to the end of the response being written).";
};
timeout_idle = mkOption {
type = with types; nullOr goDuration;
default = null;
description = "Sets the global idle timeout (i.e. the time an idle Keep-Alive connection will be kept).";
};
grpc_address = mkOption {
type = with types; nullOr str;
default = null;
description = "Sets the host and port to serve gRPC requests from.";
};
grpc_insecure = mkOption {
type = with types; nullOr bool;
default = null;
description = "Disables transport security for gRPC communication.";
};
grpc_client_timeout = mkOption {
type = with types; goDuration;
default = "10s";
description = "Maximum time before cancelling an upstream gRPC request. During transient failures, the proxy will retry upstreams for this duration. You should leave this high enough to handle backend service restart and rediscovery so that client requests do not fail.";
};
grpc_client_dns_roundrobin = mkOption {
type = with types; bool;
default = true;
description = "Enable gRPC DNS based round robin load balancing. This method uses DNS to resolve endpoints and does client side load balancing of all addresses returned by the DNS record.";
};
grpc_server_max_connection_age = mkOption {
type = with types; goDuration;
default = "5m";
description = "Set max connection age for GRPC servers. After this interval, servers ask clients to reconnect and perform any rediscovery for new/updated endpoints from DNS.";
};
grpc_server_max_connection_age_grace = mkOption {
type = with types; goDuration;
default = "5m";
description = "Additive period with grpc_server_max_connection_age, after which servers will force connections to close.";
};
http_redirect_addr = mkOption {
type = with types; nullOr str;
default = ":80";
description = "If set, the HTTP Redirect Address specifies the host and port to redirect http to https traffic on. If unset, no redirect server is started.";
};
insecure_server = mkOption {
type = with types; bool;
default = false;
description = "Turning on insecure server mode will result in pomerium starting, and operating without any protocol encryption in transit.";
};
dns_lookup_family = mkOption {
type = with types; enum ["V4_ONLY" "V6_ONLY" "AUTO"];
default = "AUTO";
description = "The DNS IP address resolution policy.";
};
metrics_address = mkOption {
type = with types; nullOr str;
default = null;
description = "Expose a prometheus endpoint on the specified port.";
};
log_level = mkOption {
type = with types; nullOr (enum ["debug" "info" "warn" "error"]);
default = null;
description = "Log level sets the global logging level for pomerium. Only logs of the desired level and above will be logged.";
};
proxy_log_level = mkOption {
type = with types; nullOr (enum ["debug" "info" "warn" "error"]);
default = null;
description = "Proxy log level sets the logging level for the pomerium proxy service access logs. Only logs of the desired level and above will be logged.";
};
services = mkOption {
type = with types; str; # lazy
default = "all";
description = ''Service mode sets which service(s) to run. If testing, you may want to set to all and run pomerium in "all-in-one mode." In production, you'll likely want to spin up several instances of each service mode for high availability.'';
};
# shared_secret deliberately omitted.
authenticate_callback_path = mkOption {
type = with types; str;
default = "/oauth2/callback";
description = "Authenticate callback path sets the path at which the authenticate service receives callback responses from your identity provider. The value must exactly match one of the authorized redirect URIs for the OAuth 2.0 client.";
};
authenticate_service_url = mkOption {
type = with types; str;
# TODO(lukegb): validate that this is set if we're running the authenticate service.
description = "Authenticate Service URL is the externally accessible URL for the authenticate service.";
};
idp_client_id = mkOption {
type = with types; nullOr str;
default = null;
# TODO(lukegb): validate that this is set if we're running the authenticate service.
description = "Client ID is the OAuth 2.0 Client Identifier retrieved from your identity provider.";
};
# idp_client_secret deliberately omitted.
idp_provider = mkOption {
type = with types; nullOr (enum [ "auth0" "azure" "google" "okta" "onelogin" "oidc" ]);
default = null;
# TODO(lukegb): validate that this is set if we're running the authenticate service.
description = "Provider is the short-hand name of a built-in OpenID Connect (oidc) identity provider to be used for authentication. To use a generic provider, set to oidc";
};
idp_scopes = mkOption {
type = with types; nullOr str;
default = null;
description = "Identity provider scopes correspond to access privilege scopes as defined in Section 3.3 of OAuth 2.0 RFC6749. The scopes associated with Access Tokens determine what resources will be available when they are used to access OAuth 2.0 protected endpoints.";
};
# idp_service_account deliberately omitted.
idp_provider_url = mkOption {
type = with types; nullOr str;
default = null;
description = "Provider URL is the base path to an identity provider's OpenID connect discovery document (opens new window).";
};
idp_request_params = mkOption {
type = with types; attrsOf str;
default = {};
description = "Request parameters to be added as part of a signin request using OAuth2 code flow.";
};
idp_refresh_directory_interval = mkOption {
type = with types; goDuration;
default = "10m";
description = "Interval between directory syncs";
};
idp_refresh_directory_timeout = mkOption {
type = with types; goDuration;
default = "1m";
description = "Maximum duration allowed for each directory sync run";
};
# certificate_authority deliberately not provided; use certificate_authority_file.
certificate_authority_file = mkOption {
type = with types; nullOr path;
default = null;
description = "Behind-the-ingress service communication uses custom or self-signed certificates.";
};
default_upstream_timeout = mkOption {
type = with types; goDuration;
default = "30s";
description = "Default timeout applied to a proxied route when no timeout key is specified by the policy.";
};
headers = mkOption {
type = with types; nullOr (attrsOf str);
default = null;
description = "HTTP headers which will be added to the proxied responses. Pomerium sets some conservative secure HTTP headers by default.";
};
jwt_claims_headers = mkOption {
type = with types; listOf str;
default = [];
description = "The JWT Claim Headers setting allows you to pass specific user session data down to upstream applications as HTTP request headers. Note, unlike the header x-pomerium-jwt-assertion these values are not signed by the authorization service.";
};
override_certificate_name = mkOption {
type = with types; nullOr str;
default = null;
description = "Secure service communication can fail if the external certificate does not match the internally routed service hostname/SNI. This setting allows you to override that value.";
};
refresh_cooldown = mkOption {
type = with types; goDuration;
default = "5m";
description = "Refresh cooldown is the minimum amount of time between allowed manually refreshed sessions.";
};
databroker_service_url = mkOption {
type = with types; nullOr str;
default = null;
description = "The data broker service URL points to a data broker which is responsible for storing associated authorization context (e.g. sessions, users and user groups).";
};
databroker_storage_type = mkOption {
type = with types; str;
default = "memory";
description = "The backend storage that databroker server will use.";
};
databroker_storage_connection_string = mkOption {
type = with types; nullOr str;
default = null;
description = "The connection string that the databroker service will use to connect to storage backend.";
};
databroker_storage_cert_file = mkOption {
type = with types; nullOr path;
default = null;
description = "The certificate used to connect to a storage backend.";
};
databroker_storage_key_file = mkOption {
type = with types; nullOr path;
default = null;
description = "The key used to connect to a storage backend.";
};
databroker_storage_ca_file = mkOption {
type = with types; nullOr path;
default = null;
description = "The CA certificate set used to verify storage backend connections.";
};
databroker_storage_tls_skip_verify = mkOption {
type = with types; bool;
default = false;
description = "If set, the TLS connection to the storage backend will not be verified.";
};
signout_redirect_url = mkOption {
type = with types; nullOr str;
default = null;
description = "Signout redirect url is the url user will be redirected to after signing out.";
};
policy = mkOption {
type = types.listOf (types.submodule {
options = {
allowed_domains = mkOption {
type = with types; nullOr (listOf str);
default = null;
description = "Allowed domains is a collection of whitelisted domains to authorize for a given route.";
};
allowed_groups = mkOption {
type = with types; nullOr (listOf str);
default = null;
description = "Allowed groups is a collection of whitelisted groups to authorize for a given route.";
};
allowed_idp_claims = mkOption {
type = with types; nullOr (listOf (attrsOf str));
default = null;
description = "Allowed IdP Claims is a collection of whitelisted claim key-value pairs to authorize for a given route.";
};
allowed_users = mkOption {
type = with types; nullOr (listOf str);
default = null;
description = "Allowed users is a collection of whitelisted users to authorize for a given route.";
};
cors_allow_preflight = mkOption {
type = with types; bool;
default = false;
description = "Allow unauthenticated HTTP OPTIONS requests as per the CORS spec.";
};
enable_google_cloud_serverless_authentication = mkOption {
type = with types; bool;
default = false;
description = "Enable sending a signed Authorization Header to upstream GCP services.";
};
from = mkOption {
type = with types; str;
description = "From is the externally accessible source of the proxied request.";
};
# kubernetes_service_account_token deliberately not provided.
kubernetes_service_account_token_file = mkOption {
type = with types; nullOr path;
default = null;
description = "Use this token to authenticate requests to a Kubernetes API server. Pomerium will impersonate the Pomerium user's identity, and Kubernetes RBAC can be applied to IdP user and groups.";
};
path = mkOption {
type = with types; nullOr str;
default = null;
description = "If set, the route will only match incoming requests with a path that is an exact match for the specified path.";
};
prefix = mkOption {
type = with types; nullOr str;
default = null;
description = "If set, the route will only match incoming requests with a path that begins with the specified prefix.";
};
prefix_rewrite = mkOption {
type = with types; nullOr str;
default = null;
description = "If set, indicates that during forwarding, the matched prefix (or path) should be swapped with this value.";
};
host_rewrite = mkOption {
type = with types; nullOr str;
default = null;
description = "Rewrite the Host header to this value.";
};
host_rewrite_header = mkOption {
type = with types; nullOr str;
default = null;
description = "Rewrite the Host header to the value of the named request header.";
};
host_path_regex_rewrite_pattern = mkOption {
type = with types; nullOr str;
default = null;
description = "Rewrite the Host header by matching this regex against the path and then using host_path_regex_rewrite_substitution.";
};
host_path_regex_rewrite_substitution = mkOption {
type = with types; nullOr str;
default = null;
# TODO(lukegb): assert that host_path_regex_rewrite_substitution implies host_path_regex_rewrite_pattern and vice versa.
description = "Rewrite the Host header by matching this regex against the path and then using host_path_regex_rewrite_substitution.";
};
allow_public_unauthenticated_access = mkOption {
type = with types; bool;
default = false;
description = "Use with caution: Allow all requests for a given route, bypassing authentication and authorization. Suitable for publicly exposed web services.";
};
allow_any_authenticated_user = mkOption {
type = with types; bool;
default = false;
description = "Use with caution: This setting will allow all requests for any user which is able to authenticate with our given identity provider. For instance, if you are using a corporate GSuite account, an unrelated gmail user will be able to access the underlying upstream.";
};
regex = mkOption {
type = with types; nullOr str;
default = null;
description = "If set, the route will only match incoming requests with a path that matches the specified regular expression. The supported syntax is the same as the Go regexp package which is based on re2.";
};
regex_rewrite_pattern = mkOption {
type = with types; nullOr str;
default = null;
# TODO(lukegb): add assertion that this implies regex_rewrite_substitution
description = "If set, the URL path will be rewritten according to the pattern and substitution, similar to prefix_rewrite.";
};
regex_rewrite_substitution = mkOption {
type = with types; nullOr str;
default = null;
# TODO(lukegb): add assertion that this implies regex_rewrite_substitution
description = "If set, the URL path will be rewritten according to the pattern and substitution, similar to prefix_rewrite.";
};
timeout = mkOption {
type = with types; nullOr goDuration;
default = null;
description = "Policy timeout establishes the per-route timeout value. Cannot exceed global timeout values.";
};
preserve_host_header = mkOption {
type = with types; bool;
default = false;
description = "When enabled, this option will pass the host header from the incoming request to the proxied host, instead of the destination hostname.";
};
set_request_headers = mkOption {
type = with types; attrsOf str;
default = {};
description = "Set Request Headers allows you to set static values for given request headers. This can be useful if you want to pass along additional information to downstream applications as headers, or set authentication header to the request.";
};
remove_request_headers = mkOption {
type = with types; listOf str;
default = [];
description = "Remove Request Headers allows you to remove given request headers. This can be useful if you want to prevent private information from being passed to downstream applications.";
};
to = mkOption {
type = with types; str;
description = "To is the destination of a proxied request. It can be an internal resource, or an external resource.";
};
tls_skip_verify = mkOption {
type = with types; bool;
default = false;
description = "TLS Skip Verification controls whether a client verifies the server's certificate chain and host name. If enabled, TLS accepts any certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.";
};
tls_server_name = mkOption {
type = with types; nullOr str;
default = null;
description = "TLS Server Name overrides the hostname specified in the to field. If set, this server name will be used to verify the certificate name. This is useful when the backend of your service is an TLS server with a valid certificate, but mismatched name.";
};
# tls_custom_ca deliberately not provided, use tls_custom_ca_file.
tls_custom_ca_file = mkOption {
type = with types; nullOr path;
default = null;
description = "TLS Custom Certificate Authority defines a set of root certificate authorities that clients use when verifying server certificates. This will replace, not append to, the system's trust store for a given route.";
};
# tls_client_cert/tls_client_key deliberately not provided, use tls_client_cert_file/tls_client_key_file.
tls_client_cert_file = mkOption {
type = with types; nullOr path;
default = null;
description = "Pomerium supports client certificates which can be used to enforce mutually authenticated and encrypted TLS connections (mTLS).";
};
tls_client_key_file = mkOption {
type = with types; nullOr path;
default = null;
description = "Pomerium supports client certificates which can be used to enforce mutually authenticated and encrypted TLS connections (mTLS).";
};
pass_identity_headers = mkOption {
type = with types; bool;
default = false;
description = "When enabled, this option will pass identity headers to upstream applications.";
};
allow_spdy = mkOption {
type = with types; bool;
default = false;
description = "If set, enables proxying of SPDY protocol upgrades.";
};
allow_websockets = mkOption {
type = with types; bool;
default = false;
description = "If set, enables proxying of websocket connections. NOTE: global timeouts are not enforced, although the policy-specific timeout is still enforced.";
};
timeout_read = mkOption {
type = with types; nullOr goDuration;
default = null;
description = "Sets the global read timeout (i.e. the time from when the connection is accepted to when the request body is fully read).";
};
timeout_write = mkOption {
type = with types; nullOr goDuration;
default = null;
description = "Sets the global write timeout (i.e. the time from when the request body is read to the end of the response being written).";
};
timeout_idle = mkOption {
type = with types; nullOr goDuration;
default = null;
description = "Sets the global idle timeout (i.e. the time an idle Keep-Alive connection will be kept).";
};
};
});
};
authorize_service_url = mkOption {
type = with types; nullOr str;
default = null;
description = "Authorize Service URL is the location of the internally accessible authorize service.";
};
# google_cloud_serverless_authentication_service_account deliberately not provided.
# signing_key deliberately not provided.
signing_key_algorithm = mkOption {
type = with types; nullOr str;
default = null;
description = "This setting specifies which signing algorithm to use when signing the upstream attestation JWT. Cryptographic algorithm choice is subtle, and beyond the scope of this document, but we suggest sticking to the default ES256 unless you have a good reason to use something else.";
};
};
};
};
secretsFile = mkOption {
type = with types; path;
description = "Path to file containing secrets for Pomerium, in systemd EnvironmentFile format.";
};
};
config = let
cfg = config.services.pomerium;
cfgFile = if cfg.configFile != null then cfg.configFile else (pkgs.writeText "pomerium.yaml" (generators.toYAML {} cfg.config));
in mkIf cfg.enable {
systemd.services.pomerium = {
description = "Pomerium authenticating reverse proxy";
wants = [ "network.target" ];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = pkgs.writeShellScript "run-pomerium" ''
mkdir -p "$AUTOCERT_DIR"
if [[ -v CREDENTIALS_DIRECTORY ]]; then
cd "$CREDENTIALS_DIRECTORY"
fi
exec ${depot.pkgs.pomerium}/bin/pomerium -config ${cfgFile}
'';
StateDirectory = "pomerium";
PrivateUsers = !cfg.bindLowPort; # breaks CAP_NET_BIND_SERVICE
MemoryDenyWriteExecute = false; # breaks LuaJIT
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
DevicePolicy = "closed";
ProtectSystem = "strict";
ProtectHome = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectKernelLogs = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
SystemCallArchitectures = "native";
EnvironmentFile = cfg.secretsFile;
AmbientCapabilities = lib.mkIf cfg.bindLowPort [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = lib.mkIf cfg.bindLowPort [ "CAP_NET_BIND_SERVICE" ];
Restart = "on-failure";
RestartSec = "2s";
};
};
};
}