pomerium/internal/config/options.go
Bobby DeSimone bade9f50e6
internal/httputil: use error structs for http errors (#159)
The existing implementation used a ErrorResponse method to propogate
and create http error messages. Since we added functionality to
troubleshoot, signout, and do other tasks following an http error
it's useful to use Error struct in place of method arguments.

This fixes #157 where a troubleshooting links were appearing on pages
that it didn't make sense on (e.g. pages without valid sessions).
2019-06-03 20:00:37 -07:00

411 lines
13 KiB
Go

package config
import (
"encoding/base64"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"reflect"
"time"
"github.com/mitchellh/hashstructure"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/policy"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
// DisableHeaderKey is the key used to check whether to disable setting header
const DisableHeaderKey = "disable"
// Options are the global environmental flags used to set up pomerium's services.
// If a base64 encoded certificate and key are not provided as environmental variables,
// or if a file location is not provided, the server will attempt to find a matching keypair
// in the local directory as `./cert.pem` and `./privkey.pem` respectively.
type Options struct {
// Debug outputs human-readable logs to Stdout.
Debug bool `mapstructure:"pomerium_debug"`
// LogLevel sets the global override for log level. All Loggers will use at least this value.
// Possible options are "info","warn", and "error". Defaults to "debug".
LogLevel string `mapstructure:"log_level"`
// SharedKey is the shared secret authorization key used to mutually authenticate
// requests between services.
SharedKey string `mapstructure:"shared_secret"`
// Services is a list enabled service mode. If none are selected, "all" is used.
// Available options are : "all", "authenticate", "proxy".
Services string `mapstructure:"services"`
// Addr specifies the host and port on which the server should serve
// HTTPS requests. If empty, ":https" is used.
Addr string `mapstructure:"address"`
// Cert and Key specifies the base64 encoded TLS certificates to use.
Cert string `mapstructure:"certificate"`
Key string `mapstructure:"certificate_key"`
// CertFile and KeyFile specifies the TLS certificates to use.
CertFile string `mapstructure:"certificate_file"`
KeyFile string `mapstructure:"certificate_key_file"`
// HttpRedirectAddr, if set, specifies the host and port to run the HTTP
// to HTTPS redirect server on. For example, ":http" would start a server
// on port 80. If empty, no redirect server is started.
HTTPRedirectAddr string `mapstructure:"http_redirect_addr"`
// Timeout settings : https://github.com/pomerium/pomerium/issues/40
ReadTimeout time.Duration `mapstructure:"timeout_read"`
WriteTimeout time.Duration `mapstructure:"timeout_write"`
ReadHeaderTimeout time.Duration `mapstructure:"timeout_read_header"`
IdleTimeout time.Duration `mapstructure:"timeout_idle"`
// Policy is a base64 encoded yaml blob which enumerates
// per-route access control policies.
PolicyEnv string
PolicyFile string `mapstructure:"policy_file"`
// AuthenticateURL represents the externally accessible http endpoints
// used for authentication requests and callbacks
AuthenticateURLString string `mapstructure:"authenticate_service_url"`
AuthenticateURL *url.URL `hash:"ignore"`
// Session/Cookie management
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
CookieName string `mapstructure:"cookie_name"`
CookieSecret string `mapstructure:"cookie_secret"`
CookieDomain string `mapstructure:"cookie_domain"`
CookieSecure bool `mapstructure:"cookie_secure"`
CookieHTTPOnly bool `mapstructure:"cookie_http_only"`
CookieExpire time.Duration `mapstructure:"cookie_expire"`
CookieRefresh time.Duration `mapstructure:"cookie_refresh"`
// 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"`
ClientSecret string `mapstructure:"idp_client_secret"`
Provider string `mapstructure:"idp_provider"`
ProviderURL string `mapstructure:"idp_provider_url"`
Scopes []string `mapstructure:"idp_scopes"`
ServiceAccount string `mapstructure:"idp_service_account"`
Policies []policy.Policy
// Administrators contains a set of emails with users who have super user
// (sudo) access including the ability to impersonate other users' access
Administrators []string `mapstructure:"administrators"`
// AuthenticateInternalAddr is used override the routable destination of
// authenticate service's GRPC endpoint.
// NOTE: As many load balancers do not support externally routed gRPC so
// this may be an internal location.
AuthenticateInternalAddrString string `mapstructure:"authenticate_internal_url"`
AuthenticateInternalAddr *url.URL
// AuthorizeURL 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"`
AuthorizeURL *url.URL
// Settings to enable custom behind-the-ingress service communication
OverrideCertificateName string `mapstructure:"override_certificate_name"`
CA string `mapstructure:"certificate_authority"`
CAFile string `mapstructure:"certificate_authority_file"`
// SigningKey is a base64 encoded private key used to add a JWT-signature.
// https://www.pomerium.io/docs/signed-headers.html
SigningKey string `mapstructure:"signing_key"`
// Headers to set on all proxied requests. Add a 'disable' key map to turn off.
Headers map[string]string `mapstructure:"headers"`
// RefreshCooldown limits the rate a user can refresh her session
RefreshCooldown time.Duration `mapstructure:"refresh_cooldown"`
// Sub-routes
Routes map[string]string `mapstructure:"routes"`
DefaultUpstreamTimeout time.Duration `mapstructure:"default_upstream_timeout"`
// Enable proxying of websocket connections. Defaults to "false".
// Caution: Enabling this feature could result in abuse via DOS attacks.
AllowWebsockets bool `mapstructure:"allow_websockets"`
}
// NewOptions returns a new options struct with default values
func NewOptions() *Options {
o := &Options{
Debug: false,
LogLevel: "debug",
Services: "all",
CookieHTTPOnly: true,
CookieSecure: true,
CookieExpire: time.Duration(14) * time.Hour,
CookieRefresh: time.Duration(30) * time.Minute,
CookieName: "_pomerium",
DefaultUpstreamTimeout: time.Duration(30) * time.Second,
Headers: map[string]string{
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
},
Addr: ":https",
CertFile: filepath.Join(findPwd(), "cert.pem"),
KeyFile: filepath.Join(findPwd(), "privkey.pem"),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 0, // support streaming by default
IdleTimeout: 5 * time.Minute,
AuthenticateURL: new(url.URL),
AuthenticateInternalAddr: new(url.URL),
AuthorizeURL: new(url.URL),
RefreshCooldown: time.Duration(5 * time.Minute),
AllowWebsockets: false,
}
return o
}
// OptionsFromViper builds the main binary's configuration
// options by parsing environmental variables and config file
func OptionsFromViper(configFile string) (*Options, error) {
o := NewOptions()
// Load up config
o.bindEnvs()
if configFile != "" {
log.Info().
Str("file", configFile).
Msg("loading config from file")
viper.SetConfigFile(configFile)
err := viper.ReadInConfig()
if err != nil {
return nil, fmt.Errorf("failed to read config: %s", err)
}
}
err := viper.Unmarshal(o)
if err != nil {
return nil, fmt.Errorf("failed to load options from config: %s", err)
}
// Turn URL strings into url structs
err = o.parseURLs()
if err != nil {
return nil, fmt.Errorf("failed to parse URLs: %s", err)
}
// Load and initialize policy
err = o.parsePolicy()
if err != nil {
return nil, fmt.Errorf("failed to parse Policy: %s", err)
}
if o.Debug {
log.SetDebugMode()
}
if o.LogLevel != "" {
log.SetLevel(o.LogLevel)
}
if _, disable := o.Headers[DisableHeaderKey]; disable {
o.Headers = make(map[string]string)
}
err = o.validate()
if err != nil {
return nil, err
}
log.Debug().
Str("config-checksum", o.Checksum()).
Msg("read configuration with checksum")
return o, nil
}
// validate ensures the Options fields are properly formed and present
func (o *Options) validate() error {
if !IsValidService(o.Services) {
return fmt.Errorf("%s is an invalid service type", o.Services)
}
if o.SharedKey == "" {
return errors.New("shared-key cannot be empty")
}
if len(o.Routes) != 0 {
return errors.New("routes setting is deprecated, use policy instead")
}
if o.PolicyFile != "" {
return errors.New("Setting POLICY_FILE is deprecated, use policy env var or config file instead")
}
return nil
}
// parsePolicy initializes policy
func (o *Options) parsePolicy() error {
var policies []policy.Policy
// Parse from base64 env var
if o.PolicyEnv != "" {
policyBytes, err := base64.StdEncoding.DecodeString(o.PolicyEnv)
if err != nil {
return fmt.Errorf("Could not decode POLICY env var: %s", err)
}
if err := yaml.Unmarshal(policyBytes, &policies); err != nil {
return fmt.Errorf("Could not parse POLICY env var: %s", err)
}
// Parse from file
} else {
err := viper.UnmarshalKey("policy", &policies)
if err != nil {
return err
}
}
// Finish initializing policies
for i := range policies {
err := (&policies[i]).Validate()
if err != nil {
return err
}
}
o.Policies = policies
return nil
}
// parseAndValidateURL wraps standard library's default url.Parse because it's much more
// lenient about what type of urls it accepts than pomerium can be.
func parseAndValidateURL(rawurl string) (*url.URL, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
if u.Host == "" {
return nil, fmt.Errorf("%s does have a valid hostname", rawurl)
}
if u.Scheme == "" || u.Scheme != "https" {
return nil, fmt.Errorf("%s does have a valid https scheme", rawurl)
}
return u, nil
}
// parseURLs parses URL strings into actual URL pointers
func (o *Options) parseURLs() error {
if o.AuthenticateURLString != "" {
AuthenticateURL, err := parseAndValidateURL(o.AuthenticateURLString)
if err != nil {
return fmt.Errorf("internal/config: bad authenticate-url %s : %v", o.AuthenticateURLString, err)
}
o.AuthenticateURL = AuthenticateURL
}
if o.AuthorizeURLString != "" {
AuthorizeURL, err := parseAndValidateURL(o.AuthorizeURLString)
if err != nil {
return fmt.Errorf("internal/config: bad authorize-url %s : %v", o.AuthorizeURLString, err)
}
o.AuthorizeURL = AuthorizeURL
}
if o.AuthenticateInternalAddrString != "" {
AuthenticateInternalAddr, err := parseAndValidateURL(o.AuthenticateInternalAddrString)
if err != nil {
return fmt.Errorf("internal/config: bad authenticate-internal-addr %s : %v", o.AuthenticateInternalAddrString, err)
}
o.AuthenticateInternalAddr = AuthenticateInternalAddr
}
return nil
}
// bindEnvs makes sure viper binds to each env var based on the mapstructure tag
func (o *Options) bindEnvs() {
tagName := `mapstructure`
t := reflect.TypeOf(*o)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
envName := field.Tag.Get(tagName)
viper.BindEnv(envName)
}
// Statically bind fields
viper.BindEnv("PolicyEnv", "POLICY")
}
// findPwd returns best guess at current working directory
func findPwd() string {
p, err := os.Getwd()
if err != nil {
return "."
}
return p
}
// IsValidService checks to see if a service is a valid service mode
func IsValidService(s string) bool {
switch s {
case
"all",
"proxy",
"authorize",
"authenticate":
return true
}
return false
}
// IsAuthenticate checks to see if we should be running the authenticate service
func IsAuthenticate(s string) bool {
switch s {
case
"all",
"authenticate":
return true
}
return false
}
// IsAuthorize checks to see if we should be running the authorize service
func IsAuthorize(s string) bool {
switch s {
case
"all",
"authorize":
return true
}
return false
}
// IsProxy checks to see if we should be running the proxy service
func IsProxy(s string) bool {
switch s {
case
"all",
"proxy":
return true
}
return false
}
// OptionsUpdater updates local state based on an Options struct
type OptionsUpdater interface {
UpdateOptions(*Options) error
}
// Checksum returns the checksum of the current options struct
func (o *Options) Checksum() string {
hash, err := hashstructure.Hash(o, nil)
if err != nil {
log.Warn().Msg("could not calculate Option checksum")
return "no checksum availablle"
}
return fmt.Sprintf("%x", hash)
}