diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go index 67f4acb54..578bd1f0e 100644 --- a/authenticate/authenticate.go +++ b/authenticate/authenticate.go @@ -23,7 +23,11 @@ func ValidateOptions(o *config.Options) error { if _, err := cryptutil.NewAEADCipher(sharedKey); err != nil { return fmt.Errorf("authenticate: 'SHARED_SECRET' invalid: %w", err) } - if _, err := cryptutil.NewAEADCipherFromBase64(o.CookieSecret); err != nil { + cookieSecret, err := o.GetCookieSecret() + if err != nil { + return fmt.Errorf("authenticate: 'COOKIE_SECRET' invalid: %w", err) + } + if _, err := cryptutil.NewAEADCipher(cookieSecret); err != nil { return fmt.Errorf("authenticate: 'COOKIE_SECRET' invalid %w", err) } if o.AuthenticateCallbackPath == "" { diff --git a/authenticate/identity.go b/authenticate/identity.go index b42059579..55a123779 100644 --- a/authenticate/identity.go +++ b/authenticate/identity.go @@ -19,7 +19,10 @@ func defaultGetIdentityProvider(options *config.Options, idpID string) (identity } redirectURL.Path = options.AuthenticateCallbackPath - idp := options.GetIdentityProviderForID(idpID) + idp, err := options.GetIdentityProviderForID(idpID) + if err != nil { + return nil, err + } return identity.NewAuthenticator(oauth.Options{ RedirectURL: redirectURL, ProviderName: idp.GetType(), diff --git a/authenticate/state.go b/authenticate/state.go index c63f25096..0f580bca2 100644 --- a/authenticate/state.go +++ b/authenticate/state.go @@ -101,7 +101,7 @@ func newAuthenticateStateFromConfig(cfg *config.Config) (*authenticateState, err } // private state encoder setup, used to encrypt oauth2 tokens - state.cookieSecret, err = base64.StdEncoding.DecodeString(cfg.Options.CookieSecret) + state.cookieSecret, err = cfg.Options.GetCookieSecret() if err != nil { return nil, err } @@ -131,7 +131,11 @@ func newAuthenticateStateFromConfig(cfg *config.Config) (*authenticateState, err state.sessionStore = cookieStore state.sessionLoaders = []sessions.SessionLoader{headerStore, cookieStore} state.jwk = new(jose.JSONWebKeySet) - if cfg.Options.SigningKey != "" { + signingKey, err := cfg.Options.GetSigningKey() + if err != nil { + return nil, err + } + if signingKey != "" { decodedCert, err := base64.StdEncoding.DecodeString(cfg.Options.SigningKey) if err != nil { return nil, fmt.Errorf("authenticate: failed to decode signing key: %w", err) diff --git a/authorize/authorize.go b/authorize/authorize.go index ea980c8b1..ccee02be6 100644 --- a/authorize/authorize.go +++ b/authorize/authorize.go @@ -103,10 +103,15 @@ func newPolicyEvaluator(opts *config.Options, store *store.Store) (*evaluator.Ev return nil, fmt.Errorf("authorize: invalid authenticate url: %w", err) } + signingKey, err := opts.GetSigningKey() + if err != nil { + return nil, fmt.Errorf("authorize: invalid signing key: %w", err) + } + return evaluator.New(ctx, store, evaluator.WithPolicies(opts.GetAllPolicies()), evaluator.WithClientCA(clientCA), - evaluator.WithSigningKey(opts.SigningKey), + evaluator.WithSigningKey(signingKey), evaluator.WithAuthenticateURL(authenticateURL.String()), evaluator.WithGoogleCloudServerlessAuthenticationServiceAccount(opts.GetGoogleCloudServerlessAuthenticationServiceAccount()), evaluator.WithJWTClaimsHeaders(opts.JWTClaimsHeaders), diff --git a/authorize/check_response.go b/authorize/check_response.go index 95e246b6d..f390beb0b 100644 --- a/authorize/check_response.go +++ b/authorize/check_response.go @@ -166,7 +166,11 @@ func (a *Authorize) requireLoginResponse( checkRequestURL.Scheme = "https" q.Set(urlutil.QueryRedirectURI, checkRequestURL.String()) - q.Set(urlutil.QueryIdentityProviderID, opts.GetIdentityProviderForPolicy(request.Policy).GetId()) + idp, err := opts.GetIdentityProviderForPolicy(request.Policy) + if err != nil { + return nil, err + } + q.Set(urlutil.QueryIdentityProviderID, idp.GetId()) signinURL.RawQuery = q.Encode() redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String() @@ -210,7 +214,11 @@ func (a *Authorize) requireWebAuthnResponse( q.Set(urlutil.QueryDeviceType, webauthnutil.DefaultDeviceType) } q.Set(urlutil.QueryRedirectURI, checkRequestURL.String()) - q.Set(urlutil.QueryIdentityProviderID, opts.GetIdentityProviderForPolicy(request.Policy).GetId()) + idp, err := opts.GetIdentityProviderForPolicy(request.Policy) + if err != nil { + return nil, err + } + q.Set(urlutil.QueryIdentityProviderID, idp.GetId()) signinURL.RawQuery = q.Encode() redirectTo := urlutil.NewSignedURL(state.sharedKey, signinURL).String() diff --git a/config/config_source.go b/config/config_source.go index 71695aaf8..fd1d72bb1 100644 --- a/config/config_source.go +++ b/config/config_source.go @@ -222,14 +222,18 @@ func (src *FileWatcherSource) check(ctx context.Context, cfg *Config) { cfg.Options.CertFile, cfg.Options.ClientCAFile, cfg.Options.ClientCRLFile, + cfg.Options.ClientSecretFile, + cfg.Options.CookieSecretFile, cfg.Options.DataBrokerStorageCAFile, cfg.Options.DataBrokerStorageCertFile, cfg.Options.DataBrokerStorageCertKeyFile, cfg.Options.KeyFile, - cfg.Options.PolicyFile, - cfg.Options.MetricsClientCAFile, cfg.Options.MetricsCertificateFile, cfg.Options.MetricsCertificateKeyFile, + cfg.Options.MetricsClientCAFile, + cfg.Options.PolicyFile, + cfg.Options.SharedSecretFile, + cfg.Options.SigningKeyFile, } for _, pair := range cfg.Options.CertificateFiles { diff --git a/config/identity.go b/config/identity.go index 69169f69f..7fc1a1cbd 100644 --- a/config/identity.go +++ b/config/identity.go @@ -6,11 +6,14 @@ import ( // GetIdentityProviderForID returns the identity provider associated with the given IDP id. // If none is found the default provider is returned. -func (o *Options) GetIdentityProviderForID(idpID string) *identity.Provider { +func (o *Options) GetIdentityProviderForID(idpID string) (*identity.Provider, error) { for _, policy := range o.GetAllPolicies() { - idp := o.GetIdentityProviderForPolicy(&policy) //nolint + idp, err := o.GetIdentityProviderForPolicy(&policy) //nolint + if err != nil { + return nil, err + } if idp.GetId() == idpID { - return idp + return idp, nil } } @@ -19,10 +22,15 @@ func (o *Options) GetIdentityProviderForID(idpID string) *identity.Provider { // GetIdentityProviderForPolicy gets the identity provider associated with the given policy. // If policy is nil, or changes none of the default settings, the default provider is returned. -func (o *Options) GetIdentityProviderForPolicy(policy *Policy) *identity.Provider { +func (o *Options) GetIdentityProviderForPolicy(policy *Policy) (*identity.Provider, error) { + clientSecret, err := o.GetClientSecret() + if err != nil { + return nil, err + } + idp := &identity.Provider{ ClientId: o.ClientID, - ClientSecret: o.ClientSecret, + ClientSecret: clientSecret, Type: o.Provider, Scopes: o.Scopes, ServiceAccount: o.ServiceAccount, @@ -38,5 +46,5 @@ func (o *Options) GetIdentityProviderForPolicy(policy *Policy) *identity.Provide } } idp.Id = idp.Hash() - return idp + return idp, nil } diff --git a/config/options.go b/config/options.go index 38ac7cae2..fc707e629 100644 --- a/config/options.go +++ b/config/options.go @@ -71,7 +71,8 @@ type Options struct { // SharedKey is the shared secret authorization key used to mutually authenticate // requests between services. - SharedKey string `mapstructure:"shared_secret" yaml:"shared_secret,omitempty"` + SharedKey string `mapstructure:"shared_secret" yaml:"shared_secret,omitempty"` + SharedSecretFile string `mapstructure:"shared_secret_file" yaml:"shared_secret_file,omitempty"` // Services is a list enabled service mode. If none are selected, "all" is used. // Available options are : "all", "authenticate", "proxy". @@ -132,21 +133,23 @@ type Options struct { // 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"` + CookieName string `mapstructure:"cookie_name" yaml:"cookie_name,omitempty"` + CookieSecret string `mapstructure:"cookie_secret" yaml:"cookie_secret,omitempty"` + CookieSecretFile string `mapstructure:"cookie_secret_file" yaml:"cookie_secret_file,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"` + ClientID string `mapstructure:"idp_client_id" yaml:"idp_client_id,omitempty"` + ClientSecret string `mapstructure:"idp_client_secret" yaml:"idp_client_secret,omitempty"` + ClientSecretFile string `mapstructure:"idp_client_secret_file" yaml:"idp_client_secret_file,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"` @@ -173,7 +176,8 @@ type Options struct { // SigningKey is the private key used to add a JWT-signature to upstream requests. // https://www.pomerium.com/docs/topics/getting-users-identity.html - SigningKey string `mapstructure:"signing_key" yaml:"signing_key,omitempty"` + SigningKey string `mapstructure:"signing_key" yaml:"signing_key,omitempty"` + SigningKeyFile string `mapstructure:"signing_key_file" yaml:"signing_key_file,omitempty"` HeadersEnv string `yaml:",omitempty"` // SetResponseHeaders to set on all proxied requests. Add a 'disable' key map to turn off. @@ -895,12 +899,16 @@ func (o *Options) GetOauthOptions() (oauth.Options, error) { redirectURL = redirectURL.ResolveReference(&url.URL{ Path: o.AuthenticateCallbackPath, }) + clientSecret, err := o.GetClientSecret() + if err != nil { + return oauth.Options{}, err + } return oauth.Options{ RedirectURL: redirectURL, ProviderName: o.Provider, ProviderURL: o.ProviderURL, ClientID: o.ClientID, - ClientSecret: o.ClientSecret, + ClientSecret: clientSecret, Scopes: o.Scopes, ServiceAccount: o.ServiceAccount, }, nil @@ -991,6 +999,13 @@ func (o *Options) GetCertificates() ([]tls.Certificate, error) { // GetSharedKey gets the decoded shared key. func (o *Options) GetSharedKey() ([]byte, error) { sharedKey := o.SharedKey + if o.SharedSecretFile != "" { + bs, err := os.ReadFile(o.SharedSecretFile) + if err != nil { + return nil, err + } + sharedKey = string(bs) + } // mutual auth between services on the same host can be generated at runtime if IsAll(o.Services) && o.SharedKey == "" && o.DataBrokerStorageType == StorageInMemoryName { sharedKey = randomSharedKey @@ -1174,6 +1189,49 @@ func (o *Options) GetAllRouteableHTTPDomainsForTLSServerName(tlsServerName strin return domains.ToSlice(), nil } +// GetClientSecret gets the client secret. +func (o *Options) GetClientSecret() (string, error) { + if o == nil { + return "", nil + } + if o.ClientSecretFile != "" { + bs, err := os.ReadFile(o.ClientSecretFile) + if err != nil { + return "", err + } + return string(bs), nil + } + return o.ClientSecret, nil +} + +// GetCookieSecret gets the decoded cookie secret. +func (o *Options) GetCookieSecret() ([]byte, error) { + cookieSecret := o.CookieSecret + if o.CookieSecretFile != "" { + bs, err := os.ReadFile(o.CookieSecretFile) + if err != nil { + return nil, err + } + cookieSecret = string(bs) + } + return base64.StdEncoding.DecodeString(cookieSecret) +} + +// GetSigningKey gets the signing key. +func (o *Options) GetSigningKey() (string, error) { + if o == nil { + return "", nil + } + if o.SigningKeyFile != "" { + bs, err := os.ReadFile(o.SigningKeyFile) + if err != nil { + return "", err + } + return string(bs), nil + } + return o.SigningKey, nil +} + // Checksum returns the checksum of the current options struct func (o *Options) Checksum() uint64 { return hashutil.MustHash(o) diff --git a/databroker/cache.go b/databroker/cache.go index 5ec7f660d..eff0be494 100644 --- a/databroker/cache.go +++ b/databroker/cache.go @@ -158,13 +158,18 @@ func (c *DataBroker) update(ctx context.Context, cfg *config.Config) error { return fmt.Errorf("databroker: invalid oauth options: %w", err) } + clientSecret, err := cfg.Options.GetClientSecret() + if err != nil { + return fmt.Errorf("databroker: error retrieving IPD client secret: %w", err) + } + directoryProvider := directory.GetProvider(directory.Options{ ServiceAccount: cfg.Options.ServiceAccount, Provider: cfg.Options.Provider, ProviderURL: cfg.Options.ProviderURL, QPS: cfg.Options.GetQPS(), ClientID: cfg.Options.ClientID, - ClientSecret: cfg.Options.ClientSecret, + ClientSecret: clientSecret, }) c.mu.Lock() c.directoryProvider = directoryProvider diff --git a/proxy/proxy.go b/proxy/proxy.go index f37b96df0..becfb1e04 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -38,7 +38,11 @@ func ValidateOptions(o *config.Options) error { return fmt.Errorf("proxy: invalid 'SHARED_SECRET': %w", err) } - if _, err := cryptutil.NewAEADCipherFromBase64(o.CookieSecret); err != nil { + cookieSecret, err := o.GetCookieSecret() + if err != nil { + return fmt.Errorf("proxy: invalid 'COOKIE_SECRET': %w", err) + } + if _, err := cryptutil.NewAEADCipher(cookieSecret); err != nil { return fmt.Errorf("proxy: invalid 'COOKIE_SECRET': %w", err) } diff --git a/proxy/state.go b/proxy/state.go index 9f2673063..6fe95cf8c 100644 --- a/proxy/state.go +++ b/proxy/state.go @@ -2,7 +2,6 @@ package proxy import ( "crypto/cipher" - "encoding/base64" "net/url" "sync/atomic" @@ -51,7 +50,7 @@ func newProxyStateFromConfig(cfg *config.Config) (*proxyState, error) { return nil, err } - state.cookieSecret, err = base64.StdEncoding.DecodeString(cfg.Options.CookieSecret) + state.cookieSecret, err = cfg.Options.GetCookieSecret() if err != nil { return nil, err }