mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-06 10:21:05 +02:00
The `autocert_ca` and `autocert_email` options have been added to be able to configure CAs that support the ACME protocol as an alternative to Let's Encrypt. Fix ProtoBuf definition for additional autocert options Fix PR comments and add ACME EAB configuration Add configuration option for trusted CAs when talking ACME Fix linter issues copy edits render updated reference to docs Add test for autocert manager configuration Add tests for autocert configuration options Fix CI build issues Don't set empty acme.EAB struct if configuration not set Remove required email when setting custom CA When using a non-default CA it's no longer required to specify an email address. I required this before, because it seemed to cause an issue in which no certificate was issued. The root cause was something different, rendering the hard email requirement pointless. It's still beneficial to specify an email, though. I changed the text in the docs to explain that. Update generated docs Fix failing tests by recreation of a new ACMEManager The default ACMEManager object was reused in multiple tests, resulting in unexpected states when tests run in parallel. By using a new instance for every test, this is no longer an issue.
1350 lines
49 KiB
Go
1350 lines
49 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/spf13/viper"
|
|
|
|
"github.com/pomerium/pomerium/internal/directory/azure"
|
|
"github.com/pomerium/pomerium/internal/directory/github"
|
|
"github.com/pomerium/pomerium/internal/directory/gitlab"
|
|
"github.com/pomerium/pomerium/internal/directory/google"
|
|
"github.com/pomerium/pomerium/internal/directory/okta"
|
|
"github.com/pomerium/pomerium/internal/directory/onelogin"
|
|
"github.com/pomerium/pomerium/internal/hashutil"
|
|
"github.com/pomerium/pomerium/internal/identity/oauth"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/telemetry"
|
|
"github.com/pomerium/pomerium/internal/telemetry/metrics"
|
|
"github.com/pomerium/pomerium/internal/urlutil"
|
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
"github.com/pomerium/pomerium/pkg/grpc/config"
|
|
)
|
|
|
|
// DisableHeaderKey is the key used to check whether to disable setting header
|
|
const DisableHeaderKey = "disable"
|
|
|
|
const (
|
|
idpCustomScopesDocLink = "https://www.pomerium.io/reference/#identity-provider-scopes"
|
|
idpCustomScopesWarnMsg = "config: using custom scopes may result in undefined behavior, see: " + idpCustomScopesDocLink
|
|
)
|
|
|
|
// DefaultAlternativeAddr is the address used is two services are competing over
|
|
// the same listener. Typically this is invisible to the end user (e.g. localhost)
|
|
// gRPC server, or is used for healthchecks (authorize only service)
|
|
const DefaultAlternativeAddr = ":5443"
|
|
|
|
// EnvoyAdminURL indicates where the envoy control plane is listening
|
|
var EnvoyAdminURL = &url.URL{Host: "127.0.0.1:9901", Scheme: "http"}
|
|
|
|
// The randomSharedKey is used if no shared key is supplied in all-in-one mode.
|
|
var randomSharedKey = cryptutil.NewBase64Key()
|
|
|
|
// Options are the global environmental flags used to set up pomerium's services.
|
|
// Use NewXXXOptions() methods for a safely initialized data structure.
|
|
type Options struct {
|
|
// InstallationID is used to indicate a unique installation of pomerium. Useful for telemetry.
|
|
InstallationID string `mapstructure:"installation_id" yaml:"installation_id,omitempty"`
|
|
|
|
// Debug outputs human-readable logs to Stdout.
|
|
Debug bool `mapstructure:"pomerium_debug" yaml:"pomerium_debug,omitempty"`
|
|
|
|
// LogLevel sets the global override for log level. All Loggers will use at least this value.
|
|
// Possible options are "info","warn","debug" and "error". Defaults to "info".
|
|
LogLevel string `mapstructure:"log_level" yaml:"log_level,omitempty"`
|
|
|
|
// ProxyLogLevel sets the log level for the proxy service.
|
|
// Possible options are "info","warn", and "error". Defaults to the value of `LogLevel`.
|
|
ProxyLogLevel string `mapstructure:"proxy_log_level" yaml:"proxy_log_level,omitempty"`
|
|
|
|
// SharedKey is the shared secret authorization key used to mutually authenticate
|
|
// requests between services.
|
|
SharedKey string `mapstructure:"shared_secret" yaml:"shared_secret,omitempty"`
|
|
|
|
// Services is a list enabled service mode. If none are selected, "all" is used.
|
|
// Available options are : "all", "authenticate", "proxy".
|
|
Services string `mapstructure:"services" yaml:"services,omitempty"`
|
|
|
|
// Addr specifies the host and port on which the server should serve
|
|
// HTTPS requests. If empty, ":443" (localhost:443) is used.
|
|
Addr string `mapstructure:"address" yaml:"address,omitempty"`
|
|
|
|
// InsecureServer when enabled disables all transport security.
|
|
// In this mode, Pomerium is susceptible to man-in-the-middle attacks.
|
|
// This should be used only for testing.
|
|
InsecureServer bool `mapstructure:"insecure_server" yaml:"insecure_server,omitempty"`
|
|
|
|
// DNSLookupFamily is the DNS IP address resolution policy.
|
|
// If this setting is not specified, the value defaults to AUTO.
|
|
DNSLookupFamily string `mapstructure:"dns_lookup_family" yaml:"dns_lookup_family,omitempty"`
|
|
|
|
CertificateFiles []certificateFilePair `mapstructure:"certificates" yaml:"certificates,omitempty"`
|
|
|
|
// Cert and Key is the x509 certificate used to create the HTTPS server.
|
|
Cert string `mapstructure:"certificate" yaml:"certificate,omitempty"`
|
|
Key string `mapstructure:"certificate_key" yaml:"certificate_key,omitempty"`
|
|
|
|
// CertFile and KeyFile is the x509 certificate used to hydrate TLSCertificate
|
|
CertFile string `mapstructure:"certificate_file" yaml:"certificate_file,omitempty"`
|
|
KeyFile string `mapstructure:"certificate_key_file" yaml:"certificate_key_file,omitempty"`
|
|
|
|
// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
|
|
// to HTTPS redirect server on. If empty, no redirect server is started.
|
|
HTTPRedirectAddr string `mapstructure:"http_redirect_addr" yaml:"http_redirect_addr,omitempty"`
|
|
|
|
// Timeout settings : https://github.com/pomerium/pomerium/issues/40
|
|
ReadTimeout time.Duration `mapstructure:"timeout_read" yaml:"timeout_read,omitempty"`
|
|
WriteTimeout time.Duration `mapstructure:"timeout_write" yaml:"timeout_write,omitempty"`
|
|
IdleTimeout time.Duration `mapstructure:"timeout_idle" yaml:"timeout_idle,omitempty"`
|
|
|
|
// Policies define per-route configuration and access control policies.
|
|
Policies []Policy `mapstructure:"policy"`
|
|
PolicyFile string `mapstructure:"policy_file" yaml:"policy_file,omitempty"`
|
|
Routes []Policy `mapstructure:"routes"`
|
|
|
|
// AdditionalPolicies are any additional policies added to the options.
|
|
AdditionalPolicies []Policy `yaml:"-"`
|
|
|
|
// AuthenticateURL represents the externally accessible http endpoints
|
|
// used for authentication requests and callbacks
|
|
AuthenticateURLString string `mapstructure:"authenticate_service_url" yaml:"authenticate_service_url,omitempty"`
|
|
// SignOutRedirectURL represents the url that user will be redirected to after signing out.
|
|
SignOutRedirectURLString string `mapstructure:"signout_redirect_url" yaml:"signout_redirect_url,omitempty"`
|
|
|
|
// AuthenticateCallbackPath is the path to the HTTP endpoint that will
|
|
// receive the response from your identity provider. The value must exactly
|
|
// match one of the authorized redirect URIs for the OAuth 2.0 client.
|
|
// Defaults to: `/oauth2/callback`
|
|
AuthenticateCallbackPath string `mapstructure:"authenticate_callback_path" yaml:"authenticate_callback_path,omitempty"`
|
|
|
|
// Session/Cookie management
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
|
CookieName string `mapstructure:"cookie_name" yaml:"cookie_name,omitempty"`
|
|
CookieSecret string `mapstructure:"cookie_secret" yaml:"cookie_secret,omitempty"`
|
|
CookieDomain string `mapstructure:"cookie_domain" yaml:"cookie_domain,omitempty"`
|
|
CookieSecure bool `mapstructure:"cookie_secure" yaml:"cookie_secure,omitempty"`
|
|
CookieHTTPOnly bool `mapstructure:"cookie_http_only" yaml:"cookie_http_only,omitempty"`
|
|
CookieExpire time.Duration `mapstructure:"cookie_expire" yaml:"cookie_expire,omitempty"`
|
|
|
|
// Identity provider configuration variables as specified by RFC6749
|
|
// https://openid.net/specs/openid-connect-basic-1_0.html#RFC6749
|
|
ClientID string `mapstructure:"idp_client_id" yaml:"idp_client_id,omitempty"`
|
|
ClientSecret string `mapstructure:"idp_client_secret" yaml:"idp_client_secret,omitempty"`
|
|
Provider string `mapstructure:"idp_provider" yaml:"idp_provider,omitempty"`
|
|
ProviderURL string `mapstructure:"idp_provider_url" yaml:"idp_provider_url,omitempty"`
|
|
Scopes []string `mapstructure:"idp_scopes" yaml:"idp_scopes,omitempty"`
|
|
ServiceAccount string `mapstructure:"idp_service_account" yaml:"idp_service_account,omitempty"`
|
|
// Identity provider refresh directory interval/timeout settings.
|
|
RefreshDirectoryTimeout time.Duration `mapstructure:"idp_refresh_directory_timeout" yaml:"idp_refresh_directory_timeout,omitempty"`
|
|
RefreshDirectoryInterval time.Duration `mapstructure:"idp_refresh_directory_interval" yaml:"idp_refresh_directory_interval,omitempty"`
|
|
QPS float64 `mapstructure:"idp_qps" yaml:"idp_qps"`
|
|
|
|
// RequestParams are custom request params added to the signin request as
|
|
// part of an Oauth2 code flow.
|
|
//
|
|
// https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml
|
|
// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
|
|
RequestParams map[string]string `mapstructure:"idp_request_params" yaml:"idp_request_params,omitempty"`
|
|
|
|
// AuthorizeURLString is the routable destination of the authorize service's
|
|
// gRPC endpoint. NOTE: As many load balancers do not support
|
|
// externally routed gRPC so this may be an internal location.
|
|
AuthorizeURLString string `mapstructure:"authorize_service_url" yaml:"authorize_service_url,omitempty"`
|
|
AuthorizeURLStrings []string `mapstructure:"authorize_service_urls" yaml:"authorize_service_urls,omitempty"`
|
|
|
|
// Settings to enable custom behind-the-ingress service communication
|
|
OverrideCertificateName string `mapstructure:"override_certificate_name" yaml:"override_certificate_name,omitempty"`
|
|
CA string `mapstructure:"certificate_authority" yaml:"certificate_authority,omitempty"`
|
|
CAFile string `mapstructure:"certificate_authority_file" yaml:"certificate_authority_file,omitempty"`
|
|
|
|
// SigningKey is the private key used to add a JWT-signature to upstream requests.
|
|
// https://www.pomerium.io/docs/topics/getting-users-identity.html
|
|
SigningKey string `mapstructure:"signing_key" yaml:"signing_key,omitempty"`
|
|
|
|
HeadersEnv string `yaml:",omitempty"`
|
|
// SetResponseHeaders to set on all proxied requests. Add a 'disable' key map to turn off.
|
|
SetResponseHeaders map[string]string `yaml:",omitempty"`
|
|
|
|
// List of JWT claims to insert as x-pomerium-claim-* headers on proxied requests
|
|
JWTClaimsHeaders JWTClaimHeaders `mapstructure:"jwt_claims_headers" yaml:"jwt_claims_headers,omitempty"`
|
|
|
|
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout" yaml:"default_upstream_timeout,omitempty"`
|
|
|
|
// Address/Port to bind to for prometheus metrics
|
|
MetricsAddr string `mapstructure:"metrics_address" yaml:"metrics_address,omitempty"`
|
|
// - require basic auth for prometheus metrics, base64 encoded user:pass string
|
|
MetricsBasicAuth string `mapstructure:"metrics_basic_auth" yaml:"metrics_basic_auth,omitempty"`
|
|
// - TLS options
|
|
MetricsCertificate string `mapstructure:"metrics_certificate" yaml:"metrics_certificate,omitempty"`
|
|
MetricsCertificateKey string `mapstructure:"metrics_certificate_key" yaml:"metrics_certificate_key,omitempty"`
|
|
MetricsCertificateFile string `mapstructure:"metrics_certificate_file" yaml:"metrics_certificate_file,omitempty"`
|
|
MetricsCertificateKeyFile string `mapstructure:"metrics_certificate_key_file" yaml:"metrics_certificate_key_file,omitempty"`
|
|
MetricsClientCA string `mapstructure:"metrics_client_ca" yaml:"metrics_client_ca,omitempty"`
|
|
MetricsClientCAFile string `mapstructure:"metrics_client_ca_file" yaml:"metrics_client_ca_file,omitempty"`
|
|
|
|
// Tracing shared settings
|
|
TracingProvider string `mapstructure:"tracing_provider" yaml:"tracing_provider,omitempty"`
|
|
TracingSampleRate float64 `mapstructure:"tracing_sample_rate" yaml:"tracing_sample_rate,omitempty"`
|
|
|
|
// Datadog tracing address
|
|
TracingDatadogAddress string `mapstructure:"tracing_datadog_address" yaml:"tracing_datadog_address,omitempty"`
|
|
|
|
// Jaeger
|
|
//
|
|
// CollectorEndpoint is the full url to the Jaeger HTTP Thrift collector.
|
|
// For example, http://localhost:14268/api/traces
|
|
TracingJaegerCollectorEndpoint string `mapstructure:"tracing_jaeger_collector_endpoint" yaml:"tracing_jaeger_collector_endpoint,omitempty"`
|
|
// AgentEndpoint instructs exporter to send spans to jaeger-agent at this address.
|
|
// For example, localhost:6831.
|
|
|
|
// Zipkin
|
|
//
|
|
// ZipkinEndpoint configures the zipkin collector URI
|
|
// Example: http://zipkin:9411/api/v2/spans
|
|
TracingJaegerAgentEndpoint string `mapstructure:"tracing_jaeger_agent_endpoint" yaml:"tracing_jaeger_agent_endpoint,omitempty"`
|
|
ZipkinEndpoint string `mapstructure:"tracing_zipkin_endpoint" yaml:"tracing_zipkin_endpoint"`
|
|
|
|
// GRPC Service Settings
|
|
|
|
// GRPCAddr specifies the host and port on which the server should serve
|
|
// gRPC requests. If running in all-in-one mode, ":5443" (localhost:5443) is used.
|
|
GRPCAddr string `mapstructure:"grpc_address" yaml:"grpc_address,omitempty"`
|
|
|
|
// GRPCInsecure disables transport security.
|
|
// If running in all-in-one mode, defaults to true.
|
|
GRPCInsecure bool `mapstructure:"grpc_insecure" yaml:"grpc_insecure,omitempty"`
|
|
|
|
GRPCClientTimeout time.Duration `mapstructure:"grpc_client_timeout" yaml:"grpc_client_timeout,omitempty"`
|
|
GRPCClientDNSRoundRobin bool `mapstructure:"grpc_client_dns_roundrobin" yaml:"grpc_client_dns_roundrobin,omitempty"`
|
|
|
|
// ForwardAuthEndpoint allows for a given route to be used as a forward-auth
|
|
// endpoint instead of a reverse proxy. Some third-party proxies that do not
|
|
// have rich access control capabilities (nginx, envoy, ambassador, traefik)
|
|
// allow you to delegate and authenticate each request to your website
|
|
// with an external server or service. Pomerium can be configured to accept
|
|
// these requests with this switch
|
|
ForwardAuthURLString string `mapstructure:"forward_auth_url" yaml:"forward_auth_url,omitempty"`
|
|
|
|
// DataBrokerURLString is the routable destination of the databroker service's gRPC endpiont.
|
|
DataBrokerURLString string `mapstructure:"databroker_service_url" yaml:"databroker_service_url,omitempty"`
|
|
DataBrokerURLStrings []string `mapstructure:"databroker_service_urls" yaml:"databroker_service_urls,omitempty"`
|
|
// DataBrokerStorageType is the storage backend type that databroker will use.
|
|
// Supported type: memory, redis
|
|
DataBrokerStorageType string `mapstructure:"databroker_storage_type" yaml:"databroker_storage_type,omitempty"`
|
|
// DataBrokerStorageConnectionString is the data source name for storage backend.
|
|
DataBrokerStorageConnectionString string `mapstructure:"databroker_storage_connection_string" yaml:"databroker_storage_connection_string,omitempty"`
|
|
DataBrokerStorageCertFile string `mapstructure:"databroker_storage_cert_file" yaml:"databroker_storage_cert_file,omitempty"`
|
|
DataBrokerStorageCertKeyFile string `mapstructure:"databroker_storage_key_file" yaml:"databroker_storage_key_file,omitempty"`
|
|
DataBrokerStorageCAFile string `mapstructure:"databroker_storage_ca_file" yaml:"databroker_storage_ca_file,omitempty"`
|
|
DataBrokerStorageCertSkipVerify bool `mapstructure:"databroker_storage_tls_skip_verify" yaml:"databroker_storage_tls_skip_verify,omitempty"`
|
|
|
|
// ClientCA is the base64-encoded certificate authority to validate client mTLS certificates against.
|
|
ClientCA string `mapstructure:"client_ca" yaml:"client_ca,omitempty"`
|
|
// ClientCAFile points to a file that contains the certificate authority to validate client mTLS certificates against.
|
|
ClientCAFile string `mapstructure:"client_ca_file" yaml:"client_ca_file,omitempty"`
|
|
// ClientCRL is the base64-encoded certificate revocation list for client mTLS certificates.
|
|
ClientCRL string `mapstructure:"client_crl" yaml:"client_crl,omitempty"`
|
|
// ClientCRLFile points to a file that contains the certificate revocation list for client mTLS certificates.
|
|
ClientCRLFile string `mapstructure:"client_crl_file" yaml:"client_crl_file,omitempty"`
|
|
|
|
// GoogleCloudServerlessAuthenticationServiceAccount is the service account to use for GCP serverless authentication.
|
|
// If unset, the GCP metadata server will be used to query for identity tokens.
|
|
GoogleCloudServerlessAuthenticationServiceAccount string `mapstructure:"google_cloud_serverless_authentication_service_account" yaml:"google_cloud_serverless_authentication_service_account,omitempty"` //nolint
|
|
|
|
// UseProxyProtocol configures the HTTP listener to require the HAProxy proxy protocol (either v1 or v2) on incoming requests.
|
|
UseProxyProtocol bool `mapstructure:"use_proxy_protocol" yaml:"use_proxy_protocol,omitempty" json:"use_proxy_protocol,omitempty"`
|
|
|
|
viper *viper.Viper
|
|
|
|
AutocertOptions `mapstructure:",squash" yaml:",inline"`
|
|
|
|
// SkipXffAppend instructs proxy not to append its IP address to x-forwarded-for header.
|
|
// see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers.html?highlight=skip_xff_append#x-forwarded-for
|
|
SkipXffAppend bool `mapstructure:"skip_xff_append" yaml:"skip_xff_append,omitempty" json:"skip_xff_append,omitempty"`
|
|
// XffNumTrustedHops determines the trusted client address from x-forwarded-for addresses.
|
|
// see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers.html?highlight=xff_num_trusted_hops#x-forwarded-for
|
|
XffNumTrustedHops uint32 `mapstructure:"xff_num_trusted_hops" yaml:"xff_num_trusted_hops,omitempty" json:"xff_num_trusted_hops,omitempty"`
|
|
|
|
// Envoy bootstrap admin options. These do not support dynamic updates.
|
|
EnvoyAdminAccessLogPath string `mapstructure:"envoy_admin_access_log_path" yaml:"envoy_admin_access_log_path"`
|
|
EnvoyAdminProfilePath string `mapstructure:"envoy_admin_profile_path" yaml:"envoy_admin_profile_path"`
|
|
EnvoyAdminAddress string `mapstructure:"envoy_admin_address" yaml:"envoy_admin_address"`
|
|
|
|
// ProgrammaticRedirectDomainWhitelist restricts the allowed redirect URLs when using programmatic login.
|
|
ProgrammaticRedirectDomainWhitelist []string `mapstructure:"programmatic_redirect_domain_whitelist" yaml:"programmatic_redirect_domain_whitelist,omitempty" json:"programmatic_redirect_domain_whitelist,omitempty"` //nolint
|
|
|
|
// CodecType is the codec to use for downstream connections.
|
|
CodecType CodecType `mapstructure:"codec_type" yaml:"codec_type"`
|
|
|
|
AuditKey *PublicKeyEncryptionKeyOptions `mapstructure:"audit_key"`
|
|
}
|
|
|
|
type certificateFilePair struct {
|
|
// CertFile and KeyFile is the x509 certificate used to hydrate TLSCertificate
|
|
CertFile string `mapstructure:"cert" yaml:"cert,omitempty"`
|
|
KeyFile string `mapstructure:"key" yaml:"key,omitempty"`
|
|
}
|
|
|
|
// DefaultOptions are the default configuration options for pomerium
|
|
var defaultOptions = Options{
|
|
Debug: false,
|
|
LogLevel: "info",
|
|
Services: "all",
|
|
CookieHTTPOnly: true,
|
|
CookieSecure: true,
|
|
CookieExpire: 14 * time.Hour,
|
|
CookieName: "_pomerium",
|
|
DefaultUpstreamTimeout: 30 * time.Second,
|
|
SetResponseHeaders: map[string]string{
|
|
"X-Frame-Options": "SAMEORIGIN",
|
|
"X-XSS-Protection": "1; mode=block",
|
|
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
|
},
|
|
Addr: ":443",
|
|
ReadTimeout: 30 * time.Second,
|
|
WriteTimeout: 0, // support streaming by default
|
|
IdleTimeout: 5 * time.Minute,
|
|
GRPCAddr: ":443",
|
|
GRPCClientTimeout: 10 * time.Second, // Try to withstand transient service failures for a single request
|
|
GRPCClientDNSRoundRobin: true,
|
|
AuthenticateCallbackPath: "/oauth2/callback",
|
|
TracingSampleRate: 0.0001,
|
|
RefreshDirectoryInterval: 10 * time.Minute,
|
|
RefreshDirectoryTimeout: 1 * time.Minute,
|
|
QPS: 1.0,
|
|
|
|
AutocertOptions: AutocertOptions{
|
|
Folder: dataDir(),
|
|
},
|
|
DataBrokerStorageType: "memory",
|
|
SkipXffAppend: false,
|
|
XffNumTrustedHops: 0,
|
|
EnvoyAdminAccessLogPath: os.DevNull,
|
|
EnvoyAdminProfilePath: os.DevNull,
|
|
EnvoyAdminAddress: "127.0.0.1:9901",
|
|
ProgrammaticRedirectDomainWhitelist: []string{"localhost"},
|
|
}
|
|
|
|
// NewDefaultOptions returns a copy the default options. It's the caller's
|
|
// responsibility to do a follow up Validate call.
|
|
func NewDefaultOptions() *Options {
|
|
newOpts := defaultOptions
|
|
newOpts.viper = viper.New()
|
|
return &newOpts
|
|
}
|
|
|
|
// newOptionsFromConfig builds the main binary's configuration options by parsing
|
|
// environmental variables and config file
|
|
func newOptionsFromConfig(configFile string) (*Options, error) {
|
|
o, err := optionsFromViper(configFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config: options from config file %q: %w", configFile, err)
|
|
}
|
|
serviceName := telemetry.ServiceName(o.Services)
|
|
metrics.AddPolicyCountCallback(serviceName, func() int64 {
|
|
return int64(len(o.GetAllPolicies()))
|
|
})
|
|
|
|
return o, nil
|
|
}
|
|
|
|
func optionsFromViper(configFile string) (*Options, error) {
|
|
// start a copy of the default options
|
|
o := NewDefaultOptions()
|
|
v := o.viper
|
|
// Load up config
|
|
err := bindEnvs(o, v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to bind options to env vars: %w", err)
|
|
}
|
|
|
|
if configFile != "" {
|
|
v.SetConfigFile(configFile)
|
|
if err := v.ReadInConfig(); err != nil {
|
|
return nil, fmt.Errorf("failed to read config: %w", err)
|
|
}
|
|
}
|
|
|
|
var metadata mapstructure.Metadata
|
|
if err := v.Unmarshal(o, ViperPolicyHooks, func(c *mapstructure.DecoderConfig) { c.Metadata = &metadata }); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
}
|
|
checkUnusedConfigFields(configFile, metadata.Unused)
|
|
|
|
// This is necessary because v.Unmarshal will overwrite .viper field.
|
|
o.viper = v
|
|
|
|
if err := o.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validation error %w", err)
|
|
}
|
|
return o, nil
|
|
}
|
|
|
|
func checkUnusedConfigFields(configFile string, unused []string) {
|
|
keys := make([]string, 0, len(unused))
|
|
for _, k := range unused {
|
|
if !strings.HasPrefix(k, "policy[") { // policy's embedded protobuf structs are decoded by separate hook and are unknown to mapstructure
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
if len(keys) == 0 {
|
|
return
|
|
}
|
|
log.Warn(context.Background()).Str("config_file", configFile).Strs("keys", keys).Msg("config contained unknown keys that were ignored")
|
|
}
|
|
|
|
// parsePolicy initializes policy to the options from either base64 environmental
|
|
// variables or from a file
|
|
func (o *Options) parsePolicy() error {
|
|
var policies []Policy
|
|
if err := o.viper.UnmarshalKey("policy", &policies, ViperPolicyHooks); err != nil {
|
|
return err
|
|
}
|
|
if len(policies) != 0 {
|
|
o.Policies = policies
|
|
}
|
|
|
|
var routes []Policy
|
|
if err := o.viper.UnmarshalKey("routes", &routes, ViperPolicyHooks); err != nil {
|
|
return err
|
|
}
|
|
if len(routes) != 0 {
|
|
o.Routes = routes
|
|
}
|
|
|
|
// Finish initializing policies
|
|
for i := range o.Policies {
|
|
p := &o.Policies[i]
|
|
if err := p.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i := range o.Routes {
|
|
p := &o.Routes[i]
|
|
if err := p.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i := range o.AdditionalPolicies {
|
|
p := &o.AdditionalPolicies[i]
|
|
if err := p.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *Options) viperSet(key string, value interface{}) {
|
|
o.viper.Set(key, value)
|
|
}
|
|
|
|
func (o *Options) viperIsSet(key string) bool {
|
|
return o.viper.IsSet(key)
|
|
}
|
|
|
|
// parseHeaders handles unmarshalling any custom headers correctly from the
|
|
// environment or viper's parsed keys
|
|
func (o *Options) parseHeaders(ctx context.Context) error {
|
|
var headers map[string]string
|
|
if o.HeadersEnv != "" {
|
|
// Handle JSON by default via viper
|
|
if headers = o.viper.GetStringMapString("HeadersEnv"); len(headers) == 0 {
|
|
// Try to parse "Key1:Value1,Key2:Value2" syntax
|
|
headerSlice := strings.Split(o.HeadersEnv, ",")
|
|
for n := range headerSlice {
|
|
headerFields := strings.SplitN(headerSlice[n], ":", 2)
|
|
if len(headerFields) == 2 {
|
|
headers[headerFields[0]] = headerFields[1]
|
|
} else {
|
|
// Something went wrong
|
|
return fmt.Errorf("failed to decode headers from '%s'", o.HeadersEnv)
|
|
}
|
|
}
|
|
|
|
}
|
|
o.SetResponseHeaders = headers
|
|
return nil
|
|
}
|
|
|
|
if o.viperIsSet("set_response_headers") {
|
|
if err := o.viper.UnmarshalKey("set_response_headers", &headers); err != nil {
|
|
return fmt.Errorf("header %s failed to parse: %w", o.viper.Get("set_response_headers"), err)
|
|
}
|
|
o.SetResponseHeaders = headers
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bindEnvs binds a viper instance to each env var of an Options struct based
|
|
// on the mapstructure tag
|
|
func bindEnvs(o *Options, v *viper.Viper) error {
|
|
tagName := `mapstructure`
|
|
t := reflect.TypeOf(*o)
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
envName := field.Tag.Get(tagName)
|
|
err := v.BindEnv(envName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bind field '%s' to env var '%s': %w", field.Name, envName, err)
|
|
}
|
|
|
|
}
|
|
|
|
// Statically bind fields
|
|
err := v.BindEnv("Policy", "POLICY")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bind field 'Policy' to env var 'POLICY': %w", err)
|
|
}
|
|
err = v.BindEnv("HeadersEnv", "HEADERS")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bind field 'HeadersEnv' to env var 'HEADERS': %w", err)
|
|
}
|
|
// autocert options
|
|
ao := reflect.TypeOf(o.AutocertOptions)
|
|
for i := 0; i < ao.NumField(); i++ {
|
|
field := ao.Field(i)
|
|
envName := field.Tag.Get(tagName)
|
|
err := v.BindEnv(envName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to bind field '%s' to env var '%s': %w", field.Name, envName, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate ensures the Options fields are valid, and hydrated.
|
|
func (o *Options) Validate() error {
|
|
ctx := context.TODO()
|
|
if !IsValidService(o.Services) {
|
|
return fmt.Errorf("config: %s is an invalid service type", o.Services)
|
|
}
|
|
|
|
switch o.DataBrokerStorageType {
|
|
case StorageInMemoryName:
|
|
case StorageRedisName:
|
|
if o.DataBrokerStorageConnectionString == "" {
|
|
return errors.New("config: missing databroker storage backend dsn")
|
|
}
|
|
default:
|
|
return errors.New("config: unknown databroker storage backend type")
|
|
}
|
|
|
|
_, err := o.GetSharedKey()
|
|
if err != nil {
|
|
return fmt.Errorf("config: invalid shared secret: %w", err)
|
|
}
|
|
|
|
if o.AuthenticateURLString != "" {
|
|
_, err := urlutil.ParseAndValidateURL(o.AuthenticateURLString)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad authenticate-url %s : %w", o.AuthenticateURLString, err)
|
|
}
|
|
}
|
|
|
|
if o.SignOutRedirectURLString != "" {
|
|
_, err := urlutil.ParseAndValidateURL(o.SignOutRedirectURLString)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad signout-redirect-url %s : %w", o.SignOutRedirectURLString, err)
|
|
}
|
|
}
|
|
|
|
if o.AuthorizeURLString != "" {
|
|
_, err := urlutil.ParseAndValidateURL(o.AuthorizeURLString)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad authorize-url %s : %w", o.AuthorizeURLString, err)
|
|
}
|
|
}
|
|
|
|
if o.DataBrokerURLString != "" {
|
|
_, err := urlutil.ParseAndValidateURL(o.DataBrokerURLString)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad databroker service url %s : %w", o.DataBrokerURLString, err)
|
|
}
|
|
}
|
|
|
|
if o.ForwardAuthURLString != "" {
|
|
_, err := urlutil.ParseAndValidateURL(o.ForwardAuthURLString)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad forward-auth-url %s : %w", o.ForwardAuthURLString, err)
|
|
}
|
|
}
|
|
|
|
if o.PolicyFile != "" {
|
|
return errors.New("config: policy file setting is deprecated")
|
|
}
|
|
if err := o.parsePolicy(); err != nil {
|
|
return fmt.Errorf("config: failed to parse policy: %w", err)
|
|
}
|
|
|
|
if err := o.parseHeaders(ctx); err != nil {
|
|
return fmt.Errorf("config: failed to parse headers: %w", err)
|
|
}
|
|
|
|
hasCert := false
|
|
|
|
if o.Cert != "" || o.Key != "" {
|
|
_, err := cryptutil.CertificateFromBase64(o.Cert, o.Key)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad cert base64 %w", err)
|
|
}
|
|
hasCert = true
|
|
}
|
|
|
|
for _, c := range o.CertificateFiles {
|
|
_, err := cryptutil.CertificateFromBase64(c.CertFile, c.KeyFile)
|
|
if err != nil {
|
|
_, err = cryptutil.CertificateFromFile(c.CertFile, c.KeyFile)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad cert entry, base64 or file reference invalid. %w", err)
|
|
}
|
|
hasCert = true
|
|
}
|
|
|
|
if o.CertFile != "" || o.KeyFile != "" {
|
|
_, err := cryptutil.CertificateFromFile(o.CertFile, o.KeyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad cert file %w", err)
|
|
}
|
|
hasCert = true
|
|
}
|
|
|
|
if o.DataBrokerStorageCertFile != "" || o.DataBrokerStorageCertKeyFile != "" {
|
|
_, err := cryptutil.CertificateFromFile(o.DataBrokerStorageCertFile, o.DataBrokerStorageCertKeyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad databroker cert file %w", err)
|
|
}
|
|
}
|
|
|
|
if o.DataBrokerStorageCAFile != "" {
|
|
if _, err := os.Stat(o.DataBrokerStorageCAFile); err != nil {
|
|
return fmt.Errorf("config: bad databroker ca file: %w", err)
|
|
}
|
|
}
|
|
|
|
if o.ClientCA != "" {
|
|
if _, err := base64.StdEncoding.DecodeString(o.ClientCA); err != nil {
|
|
return fmt.Errorf("config: bad client ca base64: %w", err)
|
|
}
|
|
}
|
|
|
|
if o.ClientCAFile != "" {
|
|
_, err := ioutil.ReadFile(o.ClientCAFile)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad client ca file: %w", err)
|
|
}
|
|
}
|
|
|
|
if o.ClientCRL != "" {
|
|
_, err = cryptutil.CRLFromBase64(o.ClientCRL)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad client crl base64: %w", err)
|
|
}
|
|
}
|
|
|
|
if o.ClientCRLFile != "" {
|
|
_, err = cryptutil.CRLFromFile(o.ClientCRLFile)
|
|
if err != nil {
|
|
return fmt.Errorf("config: bad client crl file: %w", err)
|
|
}
|
|
}
|
|
|
|
// if no service account was defined, there should not be any policies that
|
|
// assert group membership (except for azure which can be derived from the client
|
|
// id, secret and provider url)
|
|
if o.ServiceAccount == "" && o.Provider != "azure" {
|
|
for _, p := range o.GetAllPolicies() {
|
|
if len(p.AllowedGroups) != 0 {
|
|
return fmt.Errorf("config: `allowed_groups` requires `idp_service_account`")
|
|
}
|
|
}
|
|
}
|
|
|
|
// strip quotes from redirect address (#811)
|
|
o.HTTPRedirectAddr = strings.Trim(o.HTTPRedirectAddr, `"'`)
|
|
|
|
if !o.InsecureServer && !hasCert && !o.AutocertOptions.Enable {
|
|
log.Warn(ctx).Msg("neither `autocert`, " +
|
|
"`insecure_server` or manually provided certificates were provided, server will be using a self-signed certificate")
|
|
}
|
|
|
|
switch o.Provider {
|
|
case azure.Name, github.Name, gitlab.Name, google.Name, okta.Name, onelogin.Name:
|
|
if len(o.Scopes) > 0 {
|
|
log.Warn(ctx).Msg(idpCustomScopesWarnMsg)
|
|
}
|
|
default:
|
|
}
|
|
|
|
if err := ValidateDNSLookupFamily(o.DNSLookupFamily); err != nil {
|
|
return fmt.Errorf("config: %w", err)
|
|
}
|
|
|
|
if o.MetricsAddr != "" {
|
|
if err := ValidateMetricsAddress(o.MetricsAddr); err != nil {
|
|
return fmt.Errorf("config: invalid metrics_addr: %w", err)
|
|
}
|
|
}
|
|
|
|
// validate metrics basic auth
|
|
if o.MetricsBasicAuth != "" {
|
|
str, err := base64.StdEncoding.DecodeString(o.MetricsBasicAuth)
|
|
if err != nil {
|
|
return fmt.Errorf("config: metrics_basic_auth must be a base64 encoded string")
|
|
}
|
|
|
|
if !strings.Contains(string(str), ":") {
|
|
return fmt.Errorf("config: metrics_basic_auth should contain a user name and password separated by a colon")
|
|
}
|
|
}
|
|
|
|
if o.MetricsCertificate != "" && o.MetricsCertificateKey != "" {
|
|
_, err := cryptutil.CertificateFromBase64(o.MetricsCertificate, o.MetricsCertificateKey)
|
|
if err != nil {
|
|
return fmt.Errorf("config: invalid metrics_certificate or metrics_certificate_key: %w", err)
|
|
}
|
|
}
|
|
|
|
if o.MetricsCertificateFile != "" && o.MetricsCertificateKeyFile != "" {
|
|
_, err := cryptutil.CertificateFromFile(o.MetricsCertificateFile, o.MetricsCertificateKeyFile)
|
|
if err != nil {
|
|
return fmt.Errorf("config: invalid metrics_certificate_file or metrics_certificate_key_file: %w", err)
|
|
}
|
|
}
|
|
|
|
// validate the Autocert options
|
|
err = o.AutocertOptions.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAuthenticateURL returns the AuthenticateURL in the options or 127.0.0.1.
|
|
func (o *Options) GetAuthenticateURL() (*url.URL, error) {
|
|
rawurl := o.AuthenticateURLString
|
|
if rawurl == "" {
|
|
rawurl = "https://127.0.0.1"
|
|
}
|
|
return urlutil.ParseAndValidateURL(rawurl)
|
|
}
|
|
|
|
// GetAuthorizeURLs returns the AuthorizeURLs in the options or 127.0.0.1:5443.
|
|
func (o *Options) GetAuthorizeURLs() ([]*url.URL, error) {
|
|
if IsAll(o.Services) && o.AuthorizeURLString == "" && len(o.AuthorizeURLStrings) == 0 {
|
|
u, err := urlutil.ParseAndValidateURL("http://127.0.0.1" + DefaultAlternativeAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []*url.URL{u}, nil
|
|
}
|
|
return o.getURLs(append([]string{o.AuthorizeURLString}, o.AuthorizeURLStrings...)...)
|
|
}
|
|
|
|
// GetDataBrokerURLs returns the DataBrokerURLs in the options or 127.0.0.1:5443.
|
|
func (o *Options) GetDataBrokerURLs() ([]*url.URL, error) {
|
|
if IsAll(o.Services) && o.DataBrokerURLString == "" && len(o.DataBrokerURLStrings) == 0 {
|
|
u, err := urlutil.ParseAndValidateURL("http://127.0.0.1" + DefaultAlternativeAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []*url.URL{u}, nil
|
|
}
|
|
return o.getURLs(append([]string{o.DataBrokerURLString}, o.DataBrokerURLStrings...)...)
|
|
}
|
|
|
|
func (o *Options) getURLs(strs ...string) ([]*url.URL, error) {
|
|
var urls []*url.URL
|
|
if o != nil {
|
|
for _, str := range strs {
|
|
if str == "" {
|
|
continue
|
|
}
|
|
u, err := urlutil.ParseAndValidateURL(str)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
urls = append(urls, u)
|
|
}
|
|
}
|
|
if len(urls) == 0 {
|
|
u, _ := url.Parse("http://127.0.0.1" + DefaultAlternativeAddr)
|
|
urls = append(urls, u)
|
|
}
|
|
return urls, nil
|
|
}
|
|
|
|
// GetForwardAuthURL returns the ForwardAuthURL.
|
|
func (o *Options) GetForwardAuthURL() (*url.URL, error) {
|
|
rawurl := o.ForwardAuthURLString
|
|
if rawurl == "" {
|
|
return nil, nil
|
|
}
|
|
return urlutil.ParseAndValidateURL(rawurl)
|
|
}
|
|
|
|
// GetGRPCAddr gets the gRPC address.
|
|
func (o *Options) GetGRPCAddr() string {
|
|
// to avoid port collision when running on localhost
|
|
if IsAll(o.Services) && o.GRPCAddr == defaultOptions.GRPCAddr {
|
|
return DefaultAlternativeAddr
|
|
}
|
|
return o.GRPCAddr
|
|
}
|
|
|
|
// GetGRPCInsecure gets whether or not gRPC is insecure.
|
|
func (o *Options) GetGRPCInsecure() bool {
|
|
if IsAll(o.Services) {
|
|
return true
|
|
}
|
|
return o.GRPCInsecure
|
|
}
|
|
|
|
// GetSignOutRedirectURL gets the SignOutRedirectURL.
|
|
func (o *Options) GetSignOutRedirectURL() (*url.URL, error) {
|
|
rawurl := o.SignOutRedirectURLString
|
|
if rawurl == "" {
|
|
return nil, nil
|
|
}
|
|
return urlutil.ParseAndValidateURL(rawurl)
|
|
}
|
|
|
|
// GetMetricsCertificate returns the metrics certificate to use for TLS. `nil` will be
|
|
// returned if there is no certificate.
|
|
func (o *Options) GetMetricsCertificate() (*tls.Certificate, error) {
|
|
if o.MetricsCertificate != "" && o.MetricsCertificateKey != "" {
|
|
return cryptutil.CertificateFromBase64(o.MetricsCertificate, o.MetricsCertificateKey)
|
|
}
|
|
if o.MetricsCertificateFile != "" && o.MetricsCertificateKeyFile != "" {
|
|
return cryptutil.CertificateFromFile(o.MetricsCertificateFile, o.MetricsCertificateKeyFile)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// GetOauthOptions gets the oauth.Options for the given config options.
|
|
func (o *Options) GetOauthOptions() (oauth.Options, error) {
|
|
redirectURL, err := o.GetAuthenticateURL()
|
|
if err != nil {
|
|
return oauth.Options{}, err
|
|
}
|
|
redirectURL = redirectURL.ResolveReference(&url.URL{
|
|
Path: o.AuthenticateCallbackPath,
|
|
})
|
|
return oauth.Options{
|
|
RedirectURL: redirectURL,
|
|
ProviderName: o.Provider,
|
|
ProviderURL: o.ProviderURL,
|
|
ClientID: o.ClientID,
|
|
ClientSecret: o.ClientSecret,
|
|
Scopes: o.Scopes,
|
|
ServiceAccount: o.ServiceAccount,
|
|
}, nil
|
|
}
|
|
|
|
// GetAllPolicies gets all the policies in the options.
|
|
func (o *Options) GetAllPolicies() []Policy {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
policies := make([]Policy, 0, len(o.Policies)+len(o.Routes)+len(o.AdditionalPolicies))
|
|
policies = append(policies, o.Policies...)
|
|
policies = append(policies, o.Routes...)
|
|
policies = append(policies, o.AdditionalPolicies...)
|
|
return policies
|
|
}
|
|
|
|
// GetMetricsBasicAuth gets the metrics basic auth username and password.
|
|
func (o *Options) GetMetricsBasicAuth() (username, password string, ok bool) {
|
|
if o.MetricsBasicAuth == "" {
|
|
return "", "", false
|
|
}
|
|
|
|
bs, err := base64.StdEncoding.DecodeString(o.MetricsBasicAuth)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
|
|
idx := bytes.Index(bs, []byte{':'})
|
|
if idx == -1 {
|
|
return "", "", false
|
|
}
|
|
|
|
return string(bs[:idx]), string(bs[idx+1:]), true
|
|
}
|
|
|
|
// GetClientCA returns the client certificate authority. If neither client_ca nor client_ca_file is specified nil will
|
|
// be returned.
|
|
func (o *Options) GetClientCA() ([]byte, error) {
|
|
if o.ClientCA != "" {
|
|
return base64.StdEncoding.DecodeString(o.ClientCA)
|
|
}
|
|
if o.ClientCAFile != "" {
|
|
return ioutil.ReadFile(o.ClientCAFile)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// GetDataBrokerCertificate gets the optional databroker certificate. This method will return nil if no certificate is
|
|
// specified.
|
|
func (o *Options) GetDataBrokerCertificate() (*tls.Certificate, error) {
|
|
if o.DataBrokerStorageCertFile == "" || o.DataBrokerStorageCertKeyFile == "" {
|
|
return nil, nil
|
|
}
|
|
return cryptutil.CertificateFromFile(o.DataBrokerStorageCertFile, o.DataBrokerStorageCertKeyFile)
|
|
}
|
|
|
|
// GetCertificates gets all the certificates from the options.
|
|
func (o *Options) GetCertificates() ([]tls.Certificate, error) {
|
|
var certs []tls.Certificate
|
|
if o.Cert != "" && o.Key != "" {
|
|
cert, err := cryptutil.CertificateFromBase64(o.Cert, o.Key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config: invalid base64 certificate: %w", err)
|
|
}
|
|
certs = append(certs, *cert)
|
|
}
|
|
for _, c := range o.CertificateFiles {
|
|
cert, err := cryptutil.CertificateFromBase64(c.CertFile, c.KeyFile)
|
|
if err != nil {
|
|
cert, err = cryptutil.CertificateFromFile(c.CertFile, c.KeyFile)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config: invalid certificate entry: %w", err)
|
|
}
|
|
certs = append(certs, *cert)
|
|
}
|
|
if o.CertFile != "" && o.KeyFile != "" {
|
|
cert, err := cryptutil.CertificateFromFile(o.CertFile, o.KeyFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config: bad cert file %w", err)
|
|
}
|
|
certs = append(certs, *cert)
|
|
}
|
|
return certs, nil
|
|
}
|
|
|
|
// GetSharedKey gets the decoded shared key.
|
|
func (o *Options) GetSharedKey() ([]byte, error) {
|
|
sharedKey := o.SharedKey
|
|
// mutual auth between services on the same host can be generated at runtime
|
|
if IsAll(o.Services) && o.SharedKey == "" && o.DataBrokerStorageType == StorageInMemoryName {
|
|
sharedKey = randomSharedKey
|
|
}
|
|
if sharedKey == "" {
|
|
return nil, errors.New("empty shared secret")
|
|
}
|
|
if strings.TrimSpace(sharedKey) != sharedKey {
|
|
return nil, errors.New("shared secret contains whitespace")
|
|
}
|
|
return base64.StdEncoding.DecodeString(sharedKey)
|
|
}
|
|
|
|
// GetGoogleCloudServerlessAuthenticationServiceAccount gets the GoogleCloudServerlessAuthenticationServiceAccount.
|
|
func (o *Options) GetGoogleCloudServerlessAuthenticationServiceAccount() string {
|
|
if o.GoogleCloudServerlessAuthenticationServiceAccount == "" && o.Provider == "google" {
|
|
return o.ServiceAccount
|
|
}
|
|
return o.GoogleCloudServerlessAuthenticationServiceAccount
|
|
}
|
|
|
|
// GetSetResponseHeaders gets the SetResponseHeaders.
|
|
func (o *Options) GetSetResponseHeaders() map[string]string {
|
|
if _, ok := o.SetResponseHeaders[DisableHeaderKey]; ok {
|
|
return map[string]string{}
|
|
}
|
|
return o.SetResponseHeaders
|
|
}
|
|
|
|
// GetQPS gets the QPS.
|
|
func (o *Options) GetQPS() float64 {
|
|
if o.QPS < 1 {
|
|
return 1
|
|
}
|
|
return o.QPS
|
|
}
|
|
|
|
// GetCodecType gets a codec type.
|
|
func (o *Options) GetCodecType() CodecType {
|
|
if IsAll(o.Services) && o.CodecType == CodecTypeAuto {
|
|
return CodecTypeHTTP1
|
|
}
|
|
return o.CodecType
|
|
}
|
|
|
|
// Checksum returns the checksum of the current options struct
|
|
func (o *Options) Checksum() uint64 {
|
|
return hashutil.MustHash(o)
|
|
}
|
|
|
|
func (o Options) indexCerts(ctx context.Context) certsIndex {
|
|
idx := make(certsIndex)
|
|
|
|
if o.CertFile != "" {
|
|
cert, err := cryptutil.ParsePEMCertificateFromFile(o.CertFile)
|
|
if err != nil {
|
|
log.Error(ctx).Err(err).Str("file", o.CertFile).Msg("parsing local cert: skipped")
|
|
} else {
|
|
idx.addCert(cert)
|
|
}
|
|
} else if o.Cert != "" {
|
|
if data, err := base64.StdEncoding.DecodeString(o.Cert); err != nil {
|
|
log.Error(ctx).Err(err).Msg("bad base64 for local cert: skipped")
|
|
} else if cert, err := cryptutil.ParsePEMCertificate(data); err != nil {
|
|
log.Error(ctx).Err(err).Msg("parsing local cert: skipped")
|
|
} else {
|
|
idx.addCert(cert)
|
|
}
|
|
}
|
|
|
|
for _, c := range o.CertificateFiles {
|
|
cert, err := cryptutil.ParsePEMCertificateFromFile(c.CertFile)
|
|
if err != nil {
|
|
log.Error(ctx).Err(err).Str("file", c.CertFile).Msg("parsing local cert: skipped")
|
|
} else {
|
|
idx.addCert(cert)
|
|
}
|
|
}
|
|
return idx
|
|
}
|
|
|
|
func (o *Options) applyExternalCerts(ctx context.Context, certs []*config.Settings_Certificate) {
|
|
idx := o.indexCerts(ctx)
|
|
for _, c := range certs {
|
|
cert, err := cryptutil.ParsePEMCertificate(c.CertBytes)
|
|
if err != nil {
|
|
log.Error(ctx).Err(err).Msg("parsing cert from databroker: skipped")
|
|
continue
|
|
}
|
|
if overlaps, name := idx.matchCert(cert); overlaps {
|
|
log.Error(ctx).Err(err).Str("domain", name).Msg("overlaps with local certs: skipped")
|
|
continue
|
|
}
|
|
cfp := certificateFilePair{
|
|
CertFile: c.CertFile,
|
|
KeyFile: c.KeyFile,
|
|
}
|
|
if cfp.CertFile == "" {
|
|
cfp.CertFile = base64.StdEncoding.EncodeToString(c.CertBytes)
|
|
}
|
|
if cfp.KeyFile == "" {
|
|
cfp.KeyFile = base64.StdEncoding.EncodeToString(c.KeyBytes)
|
|
}
|
|
o.CertificateFiles = append(o.CertificateFiles, cfp)
|
|
}
|
|
}
|
|
|
|
// ApplySettings modifies the config options using the given protobuf settings.
|
|
func (o *Options) ApplySettings(ctx context.Context, settings *config.Settings) {
|
|
if settings == nil {
|
|
return
|
|
}
|
|
|
|
if settings.InstallationId != nil {
|
|
o.InstallationID = settings.GetInstallationId()
|
|
}
|
|
if settings.Debug != nil {
|
|
o.Debug = settings.GetDebug()
|
|
}
|
|
if settings.LogLevel != nil {
|
|
o.LogLevel = settings.GetLogLevel()
|
|
}
|
|
if settings.ProxyLogLevel != nil {
|
|
o.ProxyLogLevel = settings.GetProxyLogLevel()
|
|
}
|
|
if settings.SharedSecret != nil {
|
|
o.SharedKey = settings.GetSharedSecret()
|
|
}
|
|
if settings.Services != nil {
|
|
o.Services = settings.GetServices()
|
|
}
|
|
if settings.Address != nil {
|
|
o.Addr = settings.GetAddress()
|
|
}
|
|
if settings.InsecureServer != nil {
|
|
o.InsecureServer = settings.GetInsecureServer()
|
|
}
|
|
if settings.DnsLookupFamily != nil {
|
|
o.DNSLookupFamily = settings.GetDnsLookupFamily()
|
|
}
|
|
o.applyExternalCerts(ctx, settings.GetCertificates())
|
|
if settings.HttpRedirectAddr != nil {
|
|
o.HTTPRedirectAddr = settings.GetHttpRedirectAddr()
|
|
}
|
|
if settings.TimeoutRead != nil {
|
|
o.ReadTimeout = settings.GetTimeoutRead().AsDuration()
|
|
}
|
|
if settings.TimeoutWrite != nil {
|
|
o.WriteTimeout = settings.GetTimeoutWrite().AsDuration()
|
|
}
|
|
if settings.TimeoutIdle != nil {
|
|
o.IdleTimeout = settings.GetTimeoutIdle().AsDuration()
|
|
}
|
|
if settings.AuthenticateServiceUrl != nil {
|
|
o.AuthenticateURLString = settings.GetAuthenticateServiceUrl()
|
|
}
|
|
if settings.AuthenticateCallbackPath != nil {
|
|
o.AuthenticateCallbackPath = settings.GetAuthenticateCallbackPath()
|
|
}
|
|
if settings.CookieName != nil {
|
|
o.CookieName = settings.GetCookieName()
|
|
}
|
|
if settings.CookieSecret != nil {
|
|
o.CookieSecret = settings.GetCookieSecret()
|
|
}
|
|
if settings.CookieDomain != nil {
|
|
o.CookieDomain = settings.GetCookieDomain()
|
|
}
|
|
if settings.CookieSecure != nil {
|
|
o.CookieSecure = settings.GetCookieSecure()
|
|
}
|
|
if settings.CookieHttpOnly != nil {
|
|
o.CookieHTTPOnly = settings.GetCookieHttpOnly()
|
|
}
|
|
if settings.CookieExpire != nil {
|
|
o.CookieExpire = settings.GetCookieExpire().AsDuration()
|
|
}
|
|
if settings.IdpClientId != nil {
|
|
o.ClientID = settings.GetIdpClientId()
|
|
}
|
|
if settings.IdpClientSecret != nil {
|
|
o.ClientSecret = settings.GetIdpClientSecret()
|
|
}
|
|
if settings.IdpProvider != nil {
|
|
o.Provider = settings.GetIdpProvider()
|
|
}
|
|
if settings.IdpProviderUrl != nil {
|
|
o.ProviderURL = settings.GetIdpProviderUrl()
|
|
}
|
|
if len(settings.Scopes) > 0 {
|
|
o.Scopes = settings.Scopes
|
|
}
|
|
if settings.IdpServiceAccount != nil {
|
|
o.ServiceAccount = settings.GetIdpServiceAccount()
|
|
}
|
|
if settings.IdpRefreshDirectoryTimeout != nil {
|
|
o.RefreshDirectoryTimeout = settings.GetIdpRefreshDirectoryTimeout().AsDuration()
|
|
}
|
|
if settings.IdpRefreshDirectoryInterval != nil {
|
|
o.RefreshDirectoryInterval = settings.GetIdpRefreshDirectoryInterval().AsDuration()
|
|
}
|
|
if settings.RequestParams != nil && len(settings.RequestParams) > 0 {
|
|
o.RequestParams = settings.RequestParams
|
|
}
|
|
if len(settings.AuthorizeServiceUrls) > 0 {
|
|
o.AuthorizeURLStrings = settings.GetAuthorizeServiceUrls()
|
|
}
|
|
if settings.OverrideCertificateName != nil {
|
|
o.OverrideCertificateName = settings.GetOverrideCertificateName()
|
|
}
|
|
if settings.CertificateAuthority != nil {
|
|
o.CA = settings.GetCertificateAuthority()
|
|
}
|
|
if settings.CertificateAuthorityFile != nil {
|
|
o.CAFile = settings.GetCertificateAuthorityFile()
|
|
}
|
|
if settings.SigningKey != nil {
|
|
o.SigningKey = settings.GetSigningKey()
|
|
}
|
|
if settings.SetResponseHeaders != nil && len(settings.SetResponseHeaders) > 0 {
|
|
o.SetResponseHeaders = settings.SetResponseHeaders
|
|
}
|
|
if len(settings.JwtClaimsHeaders) > 0 {
|
|
o.JWTClaimsHeaders = settings.GetJwtClaimsHeaders()
|
|
}
|
|
if settings.DefaultUpstreamTimeout != nil {
|
|
o.DefaultUpstreamTimeout = settings.GetDefaultUpstreamTimeout().AsDuration()
|
|
}
|
|
if settings.MetricsAddress != nil {
|
|
o.MetricsAddr = settings.GetMetricsAddress()
|
|
}
|
|
if settings.MetricsBasicAuth != nil {
|
|
o.MetricsBasicAuth = settings.GetMetricsBasicAuth()
|
|
}
|
|
if len(settings.GetMetricsCertificate().GetCertBytes()) > 0 {
|
|
o.MetricsCertificate = base64.StdEncoding.EncodeToString(settings.GetMetricsCertificate().GetCertBytes())
|
|
}
|
|
if len(settings.GetMetricsCertificate().GetKeyBytes()) > 0 {
|
|
o.MetricsCertificateKey = base64.StdEncoding.EncodeToString(settings.GetMetricsCertificate().GetKeyBytes())
|
|
}
|
|
if settings.GetMetricsCertificate().GetCertFile() != "" {
|
|
o.MetricsCertificateFile = settings.GetMetricsCertificate().GetCertFile()
|
|
}
|
|
if settings.GetMetricsCertificate().GetKeyFile() != "" {
|
|
o.MetricsCertificateKeyFile = settings.GetMetricsCertificate().GetKeyFile()
|
|
}
|
|
if settings.GetMetricsClientCa() != "" {
|
|
o.MetricsClientCA = settings.GetMetricsClientCa()
|
|
}
|
|
if settings.GetMetricsClientCaFile() != "" {
|
|
o.MetricsClientCAFile = settings.GetMetricsClientCaFile()
|
|
}
|
|
if settings.TracingProvider != nil {
|
|
o.TracingProvider = settings.GetTracingProvider()
|
|
}
|
|
if settings.TracingSampleRate != nil {
|
|
o.TracingSampleRate = settings.GetTracingSampleRate()
|
|
}
|
|
if settings.TracingJaegerCollectorEndpoint != nil {
|
|
o.TracingJaegerCollectorEndpoint = settings.GetTracingJaegerCollectorEndpoint()
|
|
}
|
|
if settings.TracingJaegerAgentEndpoint != nil {
|
|
o.TracingJaegerAgentEndpoint = settings.GetTracingJaegerAgentEndpoint()
|
|
}
|
|
if settings.TracingZipkinEndpoint != nil {
|
|
o.ZipkinEndpoint = settings.GetTracingZipkinEndpoint()
|
|
}
|
|
if settings.GrpcAddress != nil {
|
|
o.GRPCAddr = settings.GetGrpcAddress()
|
|
}
|
|
if settings.GrpcInsecure != nil {
|
|
o.GRPCInsecure = settings.GetGrpcInsecure()
|
|
}
|
|
if settings.ForwardAuthUrl != nil {
|
|
o.ForwardAuthURLString = settings.GetForwardAuthUrl()
|
|
}
|
|
if len(settings.DatabrokerServiceUrls) > 0 {
|
|
o.DataBrokerURLStrings = settings.GetDatabrokerServiceUrls()
|
|
}
|
|
if settings.ClientCa != nil {
|
|
o.ClientCA = settings.GetClientCa()
|
|
}
|
|
if settings.ClientCaFile != nil {
|
|
o.ClientCAFile = settings.GetClientCaFile()
|
|
}
|
|
if settings.GoogleCloudServerlessAuthenticationServiceAccount != nil {
|
|
o.GoogleCloudServerlessAuthenticationServiceAccount = settings.GetGoogleCloudServerlessAuthenticationServiceAccount()
|
|
}
|
|
if settings.Autocert != nil {
|
|
o.AutocertOptions.Enable = settings.GetAutocert()
|
|
}
|
|
if settings.AutocertCa != nil {
|
|
o.AutocertOptions.CA = settings.GetAutocertCa()
|
|
}
|
|
if settings.AutocertEmail != nil {
|
|
o.AutocertOptions.Email = settings.GetAutocertEmail()
|
|
}
|
|
if settings.AutocertEabKeyId != nil {
|
|
o.AutocertOptions.EABKeyID = settings.GetAutocertEabKeyId()
|
|
}
|
|
if settings.AutocertEabMacKey != nil {
|
|
o.AutocertOptions.EABMACKey = settings.GetAutocertEabMacKey()
|
|
}
|
|
if settings.AutocertUseStaging != nil {
|
|
o.AutocertOptions.UseStaging = settings.GetAutocertUseStaging()
|
|
}
|
|
if settings.AutocertMustStaple != nil {
|
|
o.AutocertOptions.MustStaple = settings.GetAutocertMustStaple()
|
|
}
|
|
if settings.AutocertDir != nil {
|
|
o.AutocertOptions.Folder = settings.GetAutocertDir()
|
|
}
|
|
if settings.AutocertTrustedCa != nil {
|
|
o.AutocertOptions.TrustedCA = settings.GetAutocertTrustedCa()
|
|
}
|
|
if settings.AutocertTrustedCaFile != nil {
|
|
o.AutocertOptions.TrustedCAFile = settings.GetAutocertTrustedCaFile()
|
|
}
|
|
if settings.SkipXffAppend != nil {
|
|
o.SkipXffAppend = settings.GetSkipXffAppend()
|
|
}
|
|
if settings.XffNumTrustedHops != nil {
|
|
o.XffNumTrustedHops = settings.GetXffNumTrustedHops()
|
|
}
|
|
if len(settings.ProgrammaticRedirectDomainWhitelist) > 0 {
|
|
o.ProgrammaticRedirectDomainWhitelist = settings.GetProgrammaticRedirectDomainWhitelist()
|
|
}
|
|
if settings.AuditKey != nil {
|
|
o.AuditKey = &PublicKeyEncryptionKeyOptions{
|
|
ID: settings.AuditKey.GetId(),
|
|
Data: base64.StdEncoding.EncodeToString(settings.AuditKey.GetData()),
|
|
}
|
|
}
|
|
if settings.CodecType != nil {
|
|
o.CodecType = CodecTypeFromEnvoy(settings.GetCodecType())
|
|
}
|
|
if settings.ClientCrl != nil {
|
|
o.ClientCRL = settings.GetClientCrl()
|
|
}
|
|
if settings.ClientCrlFile != nil {
|
|
o.ClientCRLFile = settings.GetClientCrlFile()
|
|
}
|
|
}
|
|
|
|
func dataDir() string {
|
|
homeDir, _ := os.UserHomeDir()
|
|
if homeDir == "" {
|
|
homeDir = "."
|
|
}
|
|
baseDir := filepath.Join(homeDir, ".local", "share")
|
|
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
|
|
baseDir = xdgData
|
|
}
|
|
return filepath.Join(baseDir, "pomerium")
|
|
}
|
|
|
|
func compareByteSliceSlice(a, b [][]byte) int {
|
|
sz := min(len(a), len(b))
|
|
for i := 0; i < sz; i++ {
|
|
switch bytes.Compare(a[i], b[i]) {
|
|
case -1:
|
|
return -1
|
|
case 1:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case len(a) < len(b):
|
|
return -1
|
|
case len(b) < len(a):
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func min(x, y int) int {
|
|
if x < y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
// AtomicOptions are Options that can be access atomically.
|
|
type AtomicOptions struct {
|
|
value atomic.Value
|
|
}
|
|
|
|
// NewAtomicOptions creates a new AtomicOptions.
|
|
func NewAtomicOptions() *AtomicOptions {
|
|
ao := new(AtomicOptions)
|
|
ao.Store(new(Options))
|
|
return ao
|
|
}
|
|
|
|
// Load loads the options.
|
|
func (a *AtomicOptions) Load() *Options {
|
|
return a.value.Load().(*Options)
|
|
}
|
|
|
|
// Store stores the options.
|
|
func (a *AtomicOptions) Store(options *Options) {
|
|
a.value.Store(options)
|
|
}
|