From 074bc0e63cedd4e8d8766af5df39337ed7175aa5 Mon Sep 17 00:00:00 2001 From: Bobby Date: Tue, 15 Jan 2019 15:24:05 -0800 Subject: [PATCH] cmd/promerium : support TLS configuration from environmental variables (#12) * Add ability to set TLS configuration from environmental variables. * Add support for enabling debug mode from environmental variables. --- authenticate/authenticate_test.go | 4 ++ cmd/pomerium/main.go | 73 ++++++++++++++++++++++++++++--- cmd/pomerium/main_test.go | 62 ++++++++++++++++++++++++++ env.example | 12 +++++ internal/https/https.go | 58 +++++++++++++++++------- proxy/proxy_test.go | 3 ++ 6 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 cmd/pomerium/main_test.go diff --git a/authenticate/authenticate_test.go b/authenticate/authenticate_test.go index c08b3a0d2..43ce70aa4 100644 --- a/authenticate/authenticate_test.go +++ b/authenticate/authenticate_test.go @@ -8,6 +8,10 @@ import ( "time" ) +func init() { + os.Clearenv() +} + func testOptions() *Options { redirectURL, _ := url.Parse("https://example.com/oauth2/callback") return &Options{ diff --git a/cmd/pomerium/main.go b/cmd/pomerium/main.go index 306e7aad5..f18165c4e 100644 --- a/cmd/pomerium/main.go +++ b/cmd/pomerium/main.go @@ -6,6 +6,7 @@ import ( "net/http" "os" + "github.com/pomerium/envconfig" "github.com/pomerium/pomerium/internal/https" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/options" @@ -16,20 +17,25 @@ import ( ) var ( - debugFlag = flag.Bool("debug", false, "run server in debug mode, changes log output to STDOUT and level to info") - versionFlag = flag.Bool("version", false, "prints the version") + debugFlag = flag.Bool("debug", false, "run server in debug mode, changes log output to STDOUT and level to info") + versionFlag = flag.Bool("version", false, "prints the version") + // validServics = []string{"all", "proxy", "authenticate"} ) func main() { + mainOpts, err := optionsFromEnvConfig() + if err != nil { + log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse authenticator settings") + } flag.Parse() - if *debugFlag { + if *debugFlag || mainOpts.Debug { log.SetDebugMode() } if *versionFlag { fmt.Printf("%s", version.FullVersion()) os.Exit(0) } - log.Info().Str("version", version.FullVersion()).Str("user-agent", version.UserAgent()).Msg("cmd/pomerium") + log.Debug().Str("version", version.FullVersion()).Str("user-agent", version.UserAgent()).Msg("cmd/pomerium") authOpts, err := authenticate.OptionsFromEnvConfig() if err != nil { log.Fatal().Err(err).Msg("cmd/pomerium : failed to parse authenticator settings") @@ -57,6 +63,61 @@ func main() { topMux := http.NewServeMux() topMux.Handle(authOpts.RedirectURL.Host+"/", authenticator.Handler()) topMux.Handle("/", p.Handler()) - log.Fatal().Err(https.ListenAndServeTLS(nil, topMux)) - + httpOpts := &https.Options{ + Addr: mainOpts.Addr, + Cert: mainOpts.Cert, + Key: mainOpts.Key, + CertFile: mainOpts.CertFile, + KeyFile: mainOpts.KeyFile, + } + log.Fatal().Err(https.ListenAndServeTLS(httpOpts, topMux)).Msg("cmd/pomerium : fatal") +} + +// 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 enables more verbose logging, and outputs human-readable logs to Stdout. + // Set with POMERIUM_DEBUG + Debug bool `envconfig:"POMERIUM_DEBUG"` + // Services is a list enabled service mode. If none are selected, "all" is used. + // Available options are : "all", "authenticate", "proxy". + Services string `envconfig:"SERVICES"` + // Addr specifies the host and port on which the server should serve + // HTTPS requests. If empty, ":https" is used. + Addr string `envconfig:"ADDRESS"` + // Cert and Key specifies the base64 encoded TLS certificates to use. + Cert string `envconfig:"CERTIFICATE"` + Key string `envconfig:"CERTIFICATE_KEY"` + // CertFile and KeyFile specifies the TLS certificates to use. + CertFile string `envconfig:"CERTIFICATE_FILE"` + KeyFile string `envconfig:"CERTIFICATE_KEY_FILE"` +} + +var defaultOptions = &Options{ + Debug: false, + Services: "all", +} + +// optionsFromEnvConfig builds the authentication service's configuration +// options from provided environmental variables +func optionsFromEnvConfig() (*Options, error) { + o := defaultOptions + if err := envconfig.Process("", o); err != nil { + return nil, err + } + return o, nil +} + +// isValidService checks to see if a service is a valid service mode +func isValidService(service string) bool { + switch service { + case + "all", + "proxy", + "authenticate": + return true + } + return false } diff --git a/cmd/pomerium/main_test.go b/cmd/pomerium/main_test.go new file mode 100644 index 000000000..ef02fe0d2 --- /dev/null +++ b/cmd/pomerium/main_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "os" + "reflect" + "testing" +) + +func init() { + os.Clearenv() +} +func Test_optionsFromEnvConfig(t *testing.T) { + tests := []struct { + name string + want *Options + envKey string + envValue string + wantErr bool + }{ + {"good default with no env settings", defaultOptions, "", "", false}, + {"good service", defaultOptions, "SERVICES", "all", false}, + {"bad debug boolean", nil, "POMERIUM_DEBUG", "yes", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envKey != "" { + os.Setenv(tt.envKey, tt.envValue) + defer os.Unsetenv(tt.envKey) + } + got, err := optionsFromEnvConfig() + if (err != nil) != tt.wantErr { + t.Errorf("optionsFromEnvConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("optionsFromEnvConfig() = got %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isValidService(t *testing.T) { + tests := []struct { + name string + service string + want bool + }{ + {"proxy", "proxy", true}, + {"all", "all", true}, + {"authenticate", "authenticate", true}, + {"authenticate bad case", "AuThenticate", false}, + {"authorize not yet implemented", "authorize", false}, + {"jiberish", "xd23", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isValidService(tt.service); got != tt.want { + t.Errorf("isValidService() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/env.example b/env.example index e82c39c42..a65c44067 100644 --- a/env.example +++ b/env.example @@ -1,5 +1,17 @@ #!/bin/bash +# Main configuration flags +# export ADDRESS=":8443" # optional, default is 443 +# export POMERIUM_DEBUG=true # optional, default is false +# export SERVICE="all" # optional, default is all. + +# Certificates can be loaded as files or base64 encoded bytes. If neither is set, a +# pomerium will attempt to locate a pair in the root directory +export CERTIFICATE="xxxxxx" # base64 encoded cert, eg. `base64 -i cert.pem` +export CERTIFICATE_KEY="xxxx" # base64 encoded key, eg. `base64 -i privkey.pem` +export CERTIFICATE_FILE="./cert.pem" # optional, defaults to `./cert.pem` +export CERTIFICATE_KEY_FILE="./privkey.pem" # optional, defaults to `./certprivkey.pem` + # The URL that the identity provider will call back after authenticating the user export REDIRECT_URL="https://sso-auth.corp.example.com/oauth2/callback" # Allow users with emails from the following domain post-fix (e.g. example.com) diff --git a/internal/https/https.go b/internal/https/https.go index 6cb7d92f8..1ae610d48 100644 --- a/internal/https/https.go +++ b/internal/https/https.go @@ -2,6 +2,7 @@ package https // import "github.com/pomerium/pomerium/internal/https" import ( "crypto/tls" + "encoding/base64" "fmt" "net" "net/http" @@ -12,12 +13,15 @@ import ( "github.com/pomerium/pomerium/internal/fileutil" ) -// Options contains the configurations settings for a TLS http server +// Options contains the configurations settings for a TLS http server. type Options struct { // Addr specifies the host and port on which the server should serve // HTTPS requests. If empty, ":https" is used. Addr string + // Cert and Key specifies the base64 encoded TLS certificates to use. + Cert string + Key string // CertFile and KeyFile specifies the TLS certificates to use. CertFile string KeyFile string @@ -41,10 +45,10 @@ func (opt *Options) applyDefaults() { if opt.Addr == "" { opt.Addr = defaultOptions.Addr } - if opt.CertFile == "" { + if opt.Cert == "" && opt.CertFile == "" { opt.CertFile = defaultOptions.CertFile } - if opt.KeyFile == "" { + if opt.Key == "" && opt.KeyFile == "" { opt.KeyFile = defaultOptions.KeyFile } } @@ -57,12 +61,20 @@ func ListenAndServeTLS(opt *Options, handler http.Handler) error { } else { opt.applyDefaults() } - - config, err := newDefaultTLSConfig(opt.CertFile, opt.KeyFile) + var cert *tls.Certificate + var err error + if opt.Cert != "" && opt.Key != "" { + cert, err = decodeCertificate(opt.Cert, opt.Key) + } else { + cert, err = readCertificateFile(opt.CertFile, opt.KeyFile) + } + if err != nil { + return fmt.Errorf("https: failed loading x509 certificate: %v", err) + } + config, err := newDefaultTLSConfig(cert) if err != nil { return fmt.Errorf("https: setting up TLS config: %v", err) } - ln, err := net.Listen("tcp", opt.Addr) if err != nil { return err @@ -85,28 +97,40 @@ func ListenAndServeTLS(opt *Options, handler http.Handler) error { return server.Serve(ln) } -// newDefaultTLSConfig creates a new TLS config based on the certificate files given. -func newDefaultTLSConfig(certFile string, certKeyFile string) (*tls.Config, error) { +func decodeCertificate(cert, key string) (*tls.Certificate, error) { + decodedCert, err := base64.StdEncoding.DecodeString(cert) + if err != nil { + return nil, fmt.Errorf("failed to decode certificate cert %v: %v", decodedCert, err) + } + decodedKey, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("failed to decode certificate key %v: %v", decodedKey, err) + } + x509, err := tls.X509KeyPair(decodedCert, decodedKey) + return &x509, err +} + +func readCertificateFile(certFile, certKeyFile string) (*tls.Certificate, error) { certReadable, err := fileutil.IsReadableFile(certFile) if err != nil { - return nil, fmt.Errorf("TLS certificate in %q: %q", certFile, err) + return nil, fmt.Errorf("TLS certificate in %v: %v", certFile, err) } if !certReadable { - return nil, fmt.Errorf("certificate file %q not readable", certFile) + return nil, fmt.Errorf("certificate file %v not readable", certFile) } keyReadable, err := fileutil.IsReadableFile(certKeyFile) if err != nil { - return nil, fmt.Errorf("TLS key in %q: %v", certKeyFile, err) + return nil, fmt.Errorf("TLS key in %v: %v", certKeyFile, err) } if !keyReadable { - return nil, fmt.Errorf("certificate key file %q not readable", certKeyFile) + return nil, fmt.Errorf("certificate key file %v not readable", certKeyFile) } - cert, err := tls.LoadX509KeyPair(certFile, certKeyFile) - if err != nil { - return nil, err - } + return &cert, err +} +// newDefaultTLSConfig creates a new TLS config based on the certificate files given. +func newDefaultTLSConfig(cert *tls.Certificate) (*tls.Config, error) { tlsConfig := &tls.Config{ CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, @@ -118,7 +142,7 @@ func newDefaultTLSConfig(certFile string, certKeyFile string) (*tls.Config, erro }, MinVersion: tls.VersionTLS12, PreferServerCipherSuites: true, - Certificates: []tls.Certificate{cert}, + Certificates: []tls.Certificate{*cert}, } tlsConfig.BuildNameToCertificate() return tlsConfig, nil diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 88260243c..5ccccefd7 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -11,6 +11,9 @@ import ( "testing" ) +func init() { + os.Clearenv() +} func TestOptionsFromEnvConfig(t *testing.T) { tests := []struct { name string