pomerium/config/policy.go
Caleb Doxsey a70254ab76
kubernetes apiserver integration (#1063)
* sessions: support bearer tokens in authorization

* wip

* remove dead code

* refactor signed jwt code

* use function

* update per comments

* fix test
2020-07-14 08:33:24 -06:00

213 lines
8.5 KiB
Go

package config
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"os"
"time"
"github.com/cespare/xxhash/v2"
"github.com/mitchellh/hashstructure"
"github.com/pomerium/pomerium/internal/urlutil"
"github.com/pomerium/pomerium/pkg/cryptutil"
)
// Policy contains route specific configuration and access settings.
type Policy struct {
From string `mapstructure:"from" yaml:"from"`
To string `mapstructure:"to" yaml:"to"`
// Identity related policy
AllowedUsers []string `mapstructure:"allowed_users" yaml:"allowed_users,omitempty" json:"allowed_users,omitempty"`
AllowedGroups []string `mapstructure:"allowed_groups" yaml:"allowed_groups,omitempty" json:"allowed_groups,omitempty"`
AllowedDomains []string `mapstructure:"allowed_domains" yaml:"allowed_domains,omitempty" json:"allowed_domains,omitempty"`
Source *StringURL `yaml:",omitempty" json:"source,omitempty" hash:"ignore"`
Destination *url.URL `yaml:",omitempty" json:"destination,omitempty" hash:"ignore"`
// Additional route matching options
Prefix string `mapstructure:"prefix" yaml:"prefix,omitempty" json:"prefix,omitempty"`
Path string `mapstructure:"path" yaml:"path,omitempty" json:"path,omitempty"`
Regex string `mapstructure:"regex" yaml:"regex,omitempty" json:"regex,omitempty"`
// Allow unauthenticated HTTP OPTIONS requests as per the CORS spec
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests
CORSAllowPreflight bool `mapstructure:"cors_allow_preflight" yaml:"cors_allow_preflight,omitempty"`
// Allow any public request to access this route. **Bypasses authentication**
AllowPublicUnauthenticatedAccess bool `mapstructure:"allow_public_unauthenticated_access" yaml:"allow_public_unauthenticated_access,omitempty"`
// UpstreamTimeout is the route specific timeout. Must be less than the global
// timeout. If unset, route will fallback to the proxy's DefaultUpstreamTimeout.
UpstreamTimeout time.Duration `mapstructure:"timeout" yaml:"timeout,omitempty"`
// Enable proxying of websocket connections by removing the default timeout handler.
// Caution: Enabling this feature could result in abuse via DOS attacks.
AllowWebsockets bool `mapstructure:"allow_websockets" yaml:"allow_websockets,omitempty"`
// TLSSkipVerify controls whether a client verifies the server's certificate
// chain and host name.
// If TLSSkipVerify is true, 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.
TLSSkipVerify bool `mapstructure:"tls_skip_verify" yaml:"tls_skip_verify,omitempty"`
// TLSServerName overrides the hostname in the `to` field. This is useful
// if your backend is an HTTPS server with a valid certificate, but you
// want to communicate to the backend with an internal hostname (e.g.
// Docker container name).
TLSServerName string `mapstructure:"tls_server_name" yaml:"tls_server_name,omitempty"`
// TLSCustomCA defines the root certificate to use with a given
// route when verifying server certificates.
TLSCustomCA string `mapstructure:"tls_custom_ca" yaml:"tls_custom_ca,omitempty"`
TLSCustomCAFile string `mapstructure:"tls_custom_ca_file" yaml:"tls_custom_ca_file,omitempty"`
// Contains the x.509 client certificate to present to the downstream
// host.
TLSClientCert string `mapstructure:"tls_client_cert" yaml:"tls_client_cert,omitempty"`
TLSClientKey string `mapstructure:"tls_client_key" yaml:"tls_client_key,omitempty"`
TLSClientCertFile string `mapstructure:"tls_client_cert_file" yaml:"tls_client_cert_file,omitempty"`
TLSClientKeyFile string `mapstructure:"tls_client_key_file" yaml:"tls_client_key_file,omitempty"`
ClientCertificate *tls.Certificate `yaml:",omitempty" hash:"ignore"`
// SetRequestHeaders adds a collection of headers to the downstream request
// in the form of key value pairs. Note bene, this will overwrite the
// value of any existing value of a given header key.
SetRequestHeaders map[string]string `mapstructure:"set_request_headers" yaml:"set_request_headers,omitempty"`
// RemoveRequestHeaders removes a collection of headers from a downstream request.
// Note that this has lower priority than `SetRequestHeaders`, if you specify `X-Custom-Header` in both
// `SetRequestHeaders` and `RemoveRequestHeaders`, then the header won't be removed.
RemoveRequestHeaders []string `mapstructure:"remove_request_headers" yaml:"remove_request_headers,omitempty"`
// PreserveHostHeader disables host header rewriting.
//
// This option only takes affect if the destination is a DNS name. If the destination is an IP address,
// use SetRequestHeaders to explicitly set the "Host" header.
//
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header
PreserveHostHeader bool `mapstructure:"preserve_host_header" yaml:"preserve_host_header,omitempty"`
// PassIdentityHeaders controls whether to add a user's identity headers to the downstream request.
// These includes:
//
// - X-Pomerium-Jwt-Assertion
// - X-Pomerium-Claim-*
//
PassIdentityHeaders bool `mapstructure:"pass_identity_headers" yaml:"pass_identity_headers,omitempty"`
// KubernetesServiceAccountToken is the kubernetes token to use for upstream requests.
KubernetesServiceAccountToken string `mapstructure:"kubernetes_service_account_token" yaml:"kubernetes_service_account_token,omitempty"`
}
// Validate checks the validity of a policy.
func (p *Policy) Validate() error {
var err error
source, err := urlutil.ParseAndValidateURL(p.From)
if err != nil {
return fmt.Errorf("config: policy bad source url %w", err)
}
// Make sure there's no path set on the from url
if !(source.Path == "" || source.Path == "/") {
return fmt.Errorf("config: policy source url (%s) contains a path, but it should be set using the path field instead",
source.String())
}
p.Source = &StringURL{source}
p.Destination, err = urlutil.ParseAndValidateURL(p.To)
if err != nil {
return fmt.Errorf("config: policy bad destination url %w", err)
}
// Only allow public access if no other whitelists are in place
if p.AllowPublicUnauthenticatedAccess && (p.AllowedDomains != nil || p.AllowedGroups != nil || p.AllowedUsers != nil) {
return fmt.Errorf("config: policy route marked as public but contains whitelists")
}
if (p.TLSClientCert == "" && p.TLSClientKey != "") || (p.TLSClientCert != "" && p.TLSClientKey == "") ||
(p.TLSClientCertFile == "" && p.TLSClientKeyFile != "") || (p.TLSClientCertFile != "" && p.TLSClientKeyFile == "") {
return fmt.Errorf("config: client certificate key and cert both must be non-empty")
}
if p.TLSClientCert != "" && p.TLSClientKey != "" {
p.ClientCertificate, err = cryptutil.CertificateFromBase64(p.TLSClientCert, p.TLSClientKey)
if err != nil {
return fmt.Errorf("config: couldn't decode client cert %w", err)
}
} else if p.TLSClientCertFile != "" && p.TLSClientKeyFile != "" {
p.ClientCertificate, err = cryptutil.CertificateFromFile(p.TLSClientCertFile, p.TLSClientKeyFile)
if err != nil {
return fmt.Errorf("config: couldn't load client cert file %w", err)
}
}
if p.TLSCustomCA != "" {
_, err := base64.StdEncoding.DecodeString(p.TLSCustomCA)
if err != nil {
return fmt.Errorf("config: couldn't decode custom ca: %w", err)
}
} else if p.TLSCustomCAFile != "" {
_, err := os.Stat(p.TLSCustomCAFile)
if err != nil {
return fmt.Errorf("config: couldn't load client ca file: %w", err)
}
}
return nil
}
// Checksum returns the xxhash hash for the policy.
func (p *Policy) Checksum() uint64 {
cs, _ := hashstructure.Hash(p, &hashstructure.HashOptions{
Hasher: xxhash.New(),
})
return cs
}
// RouteID returns a unique identifier for a route
func (p *Policy) RouteID() uint64 {
id := routeID{
Source: p.Source,
Destination: p.Destination,
Prefix: p.Prefix,
Path: p.Path,
Regex: p.Regex,
}
cs, _ := hashstructure.Hash(id, &hashstructure.HashOptions{
Hasher: xxhash.New(),
})
return cs
}
func (p *Policy) String() string {
if p.Source == nil || p.Destination == nil {
return fmt.Sprintf("%s → %s", p.From, p.To)
}
return fmt.Sprintf("%s → %s", p.Source.String(), p.Destination.String())
}
// StringURL stores a URL as a string in json.
type StringURL struct {
*url.URL
}
// MarshalJSON returns the URLs host as json.
func (u *StringURL) MarshalJSON() ([]byte, error) {
return json.Marshal(u.String())
}
type routeID struct {
Source *StringURL
Destination *url.URL
Prefix string
Path string
Regex string
}