diff --git a/Makefile b/Makefile index 03d8754..bd0d802 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor) BINARY_NAME=weep -VERSION=0.1.1 +VERSION=0.1.2 REGISTRY=$(REGISTRY) BRANCH=$(shell git rev-parse --abbrev-ref HEAD) diff --git a/README.md b/README.md index 65dd302..2e2ebd6 100644 --- a/README.md +++ b/README.md @@ -132,20 +132,21 @@ AWS_PROFILE=role1 aws s3 ls In most cases, `weep` can be built by running the `make` command in the repository root. `make release` (requires [`upx`](https://upx.github.io/)) will build and compress the binary for distribution. -### Embedding mTLS configuration +### Embedded configuration -`weep` binaries can be shipped with an embedded mutual TLS (mTLS) configuration to -avoid making users set this configuration. An example of such a configuration is included -in [mtls/mtls_paths.yaml](mtls/mtls_paths.yaml). +`weep` binaries can be shipped with an embedded configuration to allow shipping an "all-in-one" binary. +An example of such a configuration is included in [example-config.yaml](example-config.yaml). -To compile with an embedded config, set the `MTLS_CONFIG_FILE` environment variable at +To compile with an embedded config, set the `EMBEDDED_CONFIG_FILE` environment variable at build time. The value of this variable MUST be the **absolute path** of the configuration file **relative to the root of the module**: ```bash -MTLS_CONFIG_FILE=/mtls/mtls_paths.yaml make +EMBEDDED_CONFIG_FILE=/example-config.yaml make ``` +Note that the embedded configuration can be overridden by a configuration file in the locations listed above. + ### Docker #### Building and Running diff --git a/challenge/challenge.go b/challenge/challenge.go index ab15d1e..8451b09 100644 --- a/challenge/challenge.go +++ b/challenge/challenge.go @@ -16,8 +16,9 @@ import ( "strings" "time" + "github.com/spf13/viper" + "github.com/golang/glog" - "github.com/netflix/weep/config" "github.com/netflix/weep/util" log "github.com/sirupsen/logrus" ) @@ -57,9 +58,6 @@ func NewHTTPClient(consolemeUrl string) (*http.Client, error) { return nil, err } jar.SetCookies(consoleMeUrlParsed, cookies) - if err != nil { - return nil, err - } client := &http.Client{ Jar: jar, } @@ -79,8 +77,6 @@ func isWSL() bool { } func poll(pollingUrl string) (*ConsolemeChallengeResponse, error) { - var pollResponse ConsolemeChallengeResponse - var pollResponseBody []byte timeout := time.After(2 * time.Minute) tick := time.Tick(3 * time.Second) req, err := http.NewRequest("GET", pollingUrl, nil) @@ -95,25 +91,30 @@ func poll(pollingUrl string) (*ConsolemeChallengeResponse, error) { case <-timeout: return nil, errors.New("*** Unable to validate Challenge Response after 2 minutes. Quitting. ***") case <-tick: - resp, err := client.Do(req) + pollResponse, err := pollRequest(client, req) if err != nil { return nil, err } - defer resp.Body.Close() - if resp.Body != nil { - pollResponseBody, err = ioutil.ReadAll(resp.Body) - err := json.Unmarshal(pollResponseBody, &pollResponse) - if err != nil { - return nil, err - } - if pollResponse.Status == "success" { - return &pollResponse, nil - } + if pollResponse.Status == "success" { + return pollResponse, nil } } } } +func pollRequest(c *http.Client, r *http.Request) (*ConsolemeChallengeResponse, error) { + var pollResponse ConsolemeChallengeResponse + var pollResponseBody []byte + resp, err := c.Do(r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + pollResponseBody, err = ioutil.ReadAll(resp.Body) + err = json.Unmarshal(pollResponseBody, &pollResponse) + return &pollResponse, err +} + func getCredentialsPath() (string, error) { currentUser, err := user.Current() if err != nil { @@ -158,18 +159,21 @@ func RefreshChallenge() error { return nil } // Step 1: Make unauthed request to ConsoleMe challenge endpoint and get a challenge challenge - if config.Config.ChallengeSettings.User == "" { + if viper.GetString("challenge_settings.user") == "" { log.Fatalf( "Invalid configuration. You must define challenge_settings.user as the user you wish to authenticate as.", ) } - var consoleMeChallengeGeneratorEndpoint string = fmt.Sprintf( + var consoleMeChallengeGeneratorEndpoint = fmt.Sprintf( "%s/noauth/v1/challenge_generator/%s", - config.Config.ConsoleMeUrl, - config.Config.ChallengeSettings.User, + viper.GetString("consoleme_url"), + viper.GetString("challenge_settings.user"), ) var challenge ConsolemeChallenge req, err := http.NewRequest("GET", consoleMeChallengeGeneratorEndpoint, nil) + if err != nil { + return err + } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) diff --git a/cmd/root.go b/cmd/root.go index 02d7b75..1128a53 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -56,14 +56,22 @@ func initConfig() { viper.AddConfigPath(home + "/.config/weep/") } - err := viper.ReadInConfig() - if err == nil { - log.Debug("Found config") - err = viper.Unmarshal(&config.Config) - if err != nil { - log.Fatalf("unable to decode into struct, %v", err) + if err := config.ReadEmbeddedConfig(); err != nil { + log.Errorf("unable to read embedded config: %v; falling back to config file", err) + } + + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok && config.EmbeddedConfigFile != "" { + log.Debug("no config file found, trying to use embedded config") + } else { + log.Fatalf("unable to read config file: %v", err) } } + + log.Debug("Found config") + if err := viper.Unmarshal(&config.Config); err != nil { + log.Fatalf("unable to decode config into struct: %v", err) + } } func initLogging() { diff --git a/config.yml b/config.yml deleted file mode 100644 index 024586e..0000000 --- a/config.yml +++ /dev/null @@ -1,14 +0,0 @@ -consoleme_url: https://path_to_consoleme:port -authentication_method: challenge|mtls -# mtls_settings: -# cert: /path/to/your/cert -# key: /path/to/your/key -# cafile: /path/to/your/cafile -# insecure: true|false -metadata: - routes: - - path: latest/user-data - - path: latest/meta-data/local-ipv4 - data: "127.0.0.1" - - path: latest/meta-data/local-hostname - data: ip-127-0-0-1.us-west-2.compute.internal diff --git a/config/config.go b/config/config.go index b77be5e..7675339 100644 --- a/config/config.go +++ b/config/config.go @@ -14,10 +14,13 @@ type MetaDataConfig struct { } type MtlsSettings struct { - Cert string `mapstructure:"cert"` - Key string `mapstructure:"key"` - CATrust string `mapstructure:"catrust"` - Insecure bool `mapstructure:"insecure"` + Cert string `mapstructure:"cert"` + Key string `mapstructure:"key"` + CATrust string `mapstructure:"catrust"` + Insecure bool `mapstructure:"insecure"` + Darwin []string `mapstructure:"darwin"` + Linux []string `mapstructure:"linux"` + Windows []string `mapstructure:"windows"` } type ChallengeSettings struct { diff --git a/config/embedded.go b/config/embedded.go new file mode 100644 index 0000000..2af6b69 --- /dev/null +++ b/config/embedded.go @@ -0,0 +1,29 @@ +package config + +import ( + "github.com/markbates/pkger" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +var ( + EmbeddedConfigFile string // To be set by ldflags at compile time +) + +// ReadEmbeddedConfig attempts to read the embedded mTLS config and create a tls.Config +func ReadEmbeddedConfig() error { + if EmbeddedConfigFile == "" { + return EmbeddedConfigDisabledError + } + f, err := pkger.Open(EmbeddedConfigFile) + if err != nil { + return errors.Wrap(err, "could not open embedded config") + } + defer f.Close() + + err = viper.ReadConfig(f) + if err != nil { + return errors.Wrap(err, "could not read embedded config") + } + return nil +} diff --git a/config/errors.go b/config/errors.go new file mode 100644 index 0000000..259e435 --- /dev/null +++ b/config/errors.go @@ -0,0 +1,11 @@ +package config + +type Error string + +func (e Error) Error() string { return string(e) } + +const ClientCertificatesNotFoundError = Error("could not find client certificates") +const EmbeddedConfigDisabledError = Error("embedded config is disabled") +const HomeDirectoryError = Error("could not resolve user's home directory") +const MissingTLSConfigError = Error("missing required mTLS configuration") +const UnsupportedOSError = Error("running on unsupported operating system") diff --git a/consoleme/consoleme.go b/consoleme/consoleme.go index 40723dd..7b23909 100644 --- a/consoleme/consoleme.go +++ b/consoleme/consoleme.go @@ -13,8 +13,9 @@ import ( "syscall" "time" + "github.com/spf13/viper" + "github.com/netflix/weep/challenge" - "github.com/netflix/weep/config" "github.com/netflix/weep/mtls" log "github.com/sirupsen/logrus" @@ -44,8 +45,8 @@ type Client struct { // GetClient creates an authenticated ConsoleMe client func GetClient() (*Client, error) { var client *Client - consoleMeUrl := config.Config.ConsoleMeUrl - authenticationMethod := config.Config.AuthenticationMethod + consoleMeUrl := viper.GetString("consoleme_url") + authenticationMethod := viper.GetString("authentication_method") if authenticationMethod == "mtls" { mtlsClient, err := mtls.NewHTTPClient() diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..84d5f33 --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,29 @@ +consoleme_url: https://path_to_consoleme:port +authentication_method: mtls # challenge or mtls +mtls_settings: + cert: mtls.crt + key: mtls.key + cafile: mtlsCA.pem + insecure: false + darwin: # weep will look in platform-specific directories for the three files specified above + - "/run/mtls/certificates" + - "/mtls/certificates" + - "$HOME/.mtls/certificates" + - "$HOME/.mtls" + linux: + - "/run/mtls/certificates" + - "/mtls/certificates" + - "$HOME/.mtls/certificates" + - "$HOME/.mtls" + windows: + - "C:\\run\\mtls\\certificates" + - "C:\\mtls\\certificates" + - "$HOME\\.mtls\\certificates" + - "$HOME\\.mtls" +metadata: + routes: + - path: latest/user-data + - path: latest/meta-data/local-ipv4 + data: "127.0.0.1" + - path: latest/meta-data/local-hostname + data: ip-127-0-0-1.us-west-2.compute.internal diff --git a/go.mod b/go.mod index 03cb084..26f8378 100644 --- a/go.mod +++ b/go.mod @@ -21,5 +21,4 @@ require ( golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc // indirect golang.org/x/sys v0.0.0-20200828081204-131dc92a58d5 // indirect gopkg.in/ini.v1 v1.62.0 - gopkg.in/yaml.v2 v2.3.0 ) diff --git a/handlers/customHandler.go b/handlers/customHandler.go index a133edc..d05db25 100644 --- a/handlers/customHandler.go +++ b/handlers/customHandler.go @@ -13,9 +13,9 @@ func CustomHandler(w http.ResponseWriter, r *http.Request) { path := mux.Vars(r)["path"] - for i := range config.Config.MetaData.Routes { - if config.Config.MetaData.Routes[i].Path == path { - fmt.Fprintln(w, config.Config.MetaData.Routes[i].Data) + for _, configRoute := range config.Config.MetaData.Routes { + if configRoute.Path == path { + fmt.Fprintln(w, configRoute.Path) } } } diff --git a/mtls/embedded_config.go b/mtls/embedded_config.go deleted file mode 100644 index 2cdaa56..0000000 --- a/mtls/embedded_config.go +++ /dev/null @@ -1,148 +0,0 @@ -package mtls - -import ( - "crypto/tls" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/markbates/pkger" - "github.com/mitchellh/go-homedir" - "github.com/pkg/errors" - "gopkg.in/yaml.v2" -) - -var ( - EmbeddedConfigFile string // To be set by ldflags at compile time -) - -type embeddedTLSConfig struct { - Enabled bool `yaml:"enabled"` - InsecureSkipVerify bool `yaml:"insecure"` - CertFilename string `yaml:"cert_filename"` - KeyFilename string `yaml:"key_filename"` - CAFilename string `yaml:"ca_filename"` - Darwin []string `yaml:"darwin"` - Linux []string `yaml:"linux"` - Windows []string `yaml:"windows"` -} - -// GetEmbeddedTLSConfig attempts to read the embedded mTLS config and create a tls.Config -func GetEmbeddedTLSConfig() (*tls.Config, error) { - if EmbeddedConfigFile == "" { - return nil, EmbeddedConfigDisabledError - } - conf, err := readEmbeddedTLSConfig() - if err != nil { - return nil, err - } - if !conf.Enabled { - return nil, EmbeddedConfigDisabledError - } - dirs, err := getConfigDirs(conf) - if err != nil { - return nil, err - } - cert, key, ca, insecure, err := getClientCertificatePaths(dirs, conf) - if err != nil { - return nil, err - } - tlsConfig, err := GetTLSConfig(cert, key, ca, insecure) - if err != nil { - return nil, err - } - return tlsConfig, nil -} - -func readEmbeddedTLSConfig() (*embeddedTLSConfig, error) { - var conf embeddedTLSConfig - f, err := pkger.Open(EmbeddedConfigFile) - if err != nil { - return &conf, errors.Wrap(err, "could not open embedded config") - } - defer f.Close() - - // Stat embedded config file to get the size for the byte slice to read into - info, err := f.Stat() - if err != nil { - return &conf, errors.Wrap(err, "could not stat embedded config") - } - - fileData := make([]byte, info.Size()) - if _, err = f.Read(fileData); err != nil { - return &conf, errors.Wrap(err, "could not read embedded config") - } - - err = yaml.Unmarshal(fileData, &conf) - if err != nil { - return &conf, errors.Wrap(err, "could not load embedded config") - } - return &conf, nil -} - -// getConfigDirs returns a list of directories to search for mTLS certs based on platform -func getConfigDirs(conf *embeddedTLSConfig) ([]string, error) { - var mtlsDirs []string - - // Select config section based on platform - switch goos := runtime.GOOS; goos { - case "darwin": - mtlsDirs = conf.Darwin - case "linux": - mtlsDirs = conf.Linux - case "windows": - mtlsDirs = conf.Windows - default: - return nil, UnsupportedOSError - } - - // Replace $HOME token with home dir - homeDir, err := homedir.Dir() - if err != nil { - return nil, HomeDirectoryError - } - for i, path := range mtlsDirs { - mtlsDirs[i] = strings.Replace(path, "$HOME", homeDir, -1) - } - return mtlsDirs, nil -} - -func getClientCertificatePaths(configDirs []string, conf *embeddedTLSConfig) (string, string, string, bool, error) { - for _, metatronDir := range configDirs { - certPath := filepath.Join(metatronDir, conf.CertFilename) - if exists, err := fileExists(certPath); err != nil { - return "", "", "", false, err - } else if !exists { - continue - } - - keyPath := filepath.Join(metatronDir, conf.KeyFilename) - if exists, err := fileExists(keyPath); err != nil { - return "", "", "", false, err - } else if !exists { - continue - } - - caPath := filepath.Join(metatronDir, conf.CAFilename) - if exists, err := fileExists(caPath); err != nil { - return "", "", "", false, err - } else if !exists { - continue - } - - return certPath, keyPath, caPath, conf.InsecureSkipVerify, nil - } - return "", "", "", false, ClientCertificatesNotFoundError -} - -func fileExists(path string) (bool, error) { - _, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return true, nil -} diff --git a/mtls/mtls.go b/mtls/mtls.go index 8c6c311..e3f5513 100644 --- a/mtls/mtls.go +++ b/mtls/mtls.go @@ -7,12 +7,34 @@ import ( "io/ioutil" "net/http" + "path/filepath" + "runtime" + "strings" + + "github.com/mitchellh/go-homedir" "github.com/netflix/weep/config" + "github.com/netflix/weep/util" log "github.com/sirupsen/logrus" ) // GetTLSConfig makes and returns a pointer to a tls.Config -func GetTLSConfig(certFile, keyFile, caFile string, insecure bool) (*tls.Config, error) { +func GetTLSConfig(mtlsConfig *config.MtlsSettings) (*tls.Config, error) { + dirs, err := getTLSDirs(mtlsConfig) + if err != nil { + return nil, err + } + certFile, keyFile, caFile, insecure, err := getClientCertificatePaths(dirs, mtlsConfig) + if err != nil { + return nil, err + } + tlsConfig, err := makeTLSConfig(certFile, keyFile, caFile, insecure) + if err != nil { + return nil, err + } + return tlsConfig, nil +} + +func makeTLSConfig(certFile, keyFile, caFile string, insecure bool) (*tls.Config, error) { if certFile == "" || keyFile == "" || caFile == "" { log.Error("MTLS cert, key, or CA file not defined in configuration") return nil, MissingTLSConfigError @@ -64,26 +86,12 @@ func GetTLSConfig(certFile, keyFile, caFile string, insecure bool) (*tls.Config, } func NewHTTPClient() (*http.Client, error) { - // Attempt to get a TLS config from the embedded configuration - tlsConfig, err := GetEmbeddedTLSConfig() - if err != nil && err == EmbeddedConfigDisabledError { - log.Debug("Embedded MTLS config is disabled") - } else if err != nil { + mtlsConfig := &config.Config.MtlsSettings + tlsConfig, err := GetTLSConfig(mtlsConfig) + if err != nil { return nil, err } - if tlsConfig == nil { - // We don't have an embedded TLS config, so we'll make one from the app config - certFile := config.Config.MtlsSettings.Cert - keyFile := config.Config.MtlsSettings.Key - caFile := config.Config.MtlsSettings.CATrust - insecureSkipVerify := config.Config.MtlsSettings.Insecure - tlsConfig, err = GetTLSConfig(certFile, keyFile, caFile, insecureSkipVerify) - if err != nil { - return nil, err - } - } - client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, @@ -92,3 +100,62 @@ func NewHTTPClient() (*http.Client, error) { return client, nil } + +// getTLSDirs returns a list of directories to search for mTLS certs based on platform +func getTLSDirs(conf *config.MtlsSettings) ([]string, error) { + var mtlsDirs []string + + // Select config section based on platform + switch goos := runtime.GOOS; goos { + case "darwin": + mtlsDirs = conf.Darwin + case "linux": + mtlsDirs = conf.Linux + case "windows": + mtlsDirs = conf.Windows + default: + return nil, UnsupportedOSError + } + + // Replace $HOME token with home dir + homeDir, err := homedir.Dir() + if err != nil { + return nil, HomeDirectoryError + } + for i, path := range mtlsDirs { + mtlsDirs[i] = strings.Replace(path, "$HOME", homeDir, -1) + } + return mtlsDirs, nil +} + +func getClientCertificatePaths(configDirs []string, mtlsConfig *config.MtlsSettings) (string, string, string, bool, error) { + // If cert, key, and catrust are paths that exist, we'll just use those + if util.FileExists(mtlsConfig.Cert) && util.FileExists(mtlsConfig.Key) && util.FileExists(mtlsConfig.CATrust) { + return mtlsConfig.Cert, mtlsConfig.Key, mtlsConfig.CATrust, mtlsConfig.Insecure, nil + } + + // Otherwise, get a platform-specific list of directories and look for the files there + configDirs, err := getTLSDirs(mtlsConfig) + if err != nil { + return "", "", "", false, err + } + for _, metatronDir := range configDirs { + certPath := filepath.Join(metatronDir, mtlsConfig.Cert) + if !util.FileExists(certPath) { + continue + } + + keyPath := filepath.Join(metatronDir, mtlsConfig.Key) + if !util.FileExists(keyPath) { + continue + } + + caPath := filepath.Join(metatronDir, mtlsConfig.CATrust) + if !util.FileExists(caPath) { + continue + } + + return certPath, keyPath, caPath, mtlsConfig.Insecure, nil + } + return "", "", "", false, config.ClientCertificatesNotFoundError +} diff --git a/scripts/build.sh b/scripts/build.sh index 8d2cae1..36c56e8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,7 +8,7 @@ BINARY="${BINARY_NAME:-"weep"}" VERSION="${VERSION:-"unknown"}" VERSION_PRERELEASE="${VERSION_PRERELEASE:-""}" BUILD_DATE=$(date +%FT%T%z) -MTLS_CONFIG_FILE="${MTLS_CONFIG_FILE:-""}" +EMBEDDED_CONFIG_FILE="${EMBEDDED_CONFIG_FILE:-""}" # Set build tags BUILD_TAGS="${BUILD_TAGS:-"weep"}" @@ -20,15 +20,15 @@ GIT_DIRTY="$(test -n "`git status --porcelain`" && echo "+CHANGES" || true)" rm pkger.go 2&> /dev/null || true echo "=> Building..." -if [[ ! -z $MTLS_CONFIG_FILE ]]; then - echo "Bundling mTLS config" - pkger -include "${MTLS_CONFIG_FILE}" +if [ ! -z "$EMBEDDED_CONFIG_FILE" ]; then + echo "Bundling config" + pkger -include "${EMBEDDED_CONFIG_FILE}" else - echo "Not bundling mTLS config" + echo "Not bundling config" fi go build \ -ldflags "${LD_FLAGS} \ - -X github.com/netflix/weep/mtls.EmbeddedConfigFile=${MTLS_CONFIG_FILE} \ + -X github.com/netflix/weep/config.EmbeddedConfigFile=${EMBEDDED_CONFIG_FILE} \ -X github.com/netflix/weep/version.Version=${VERSION} \ -X github.com/netflix/weep/version.VersionPrerelease=${VERSION_PRERELEASE} \ -X github.com/netflix/weep/version.GitCommit=${GIT_COMMIT}${GIT_DIRTY} \ diff --git a/version/version.go b/version/version.go index 71e6909..c164754 100644 --- a/version/version.go +++ b/version/version.go @@ -4,7 +4,7 @@ import ( "bytes" "fmt" - "github.com/netflix/weep/mtls" + "github.com/netflix/weep/config" ) var ( @@ -62,8 +62,8 @@ func (c *VersionInfo) String() string { fmt.Fprintf(&versionString, " Built on: %s", BuildDate) - if mtls.EmbeddedConfigFile != "" { - fmt.Fprintf(&versionString, " with embedded mTLS config %s", mtls.EmbeddedConfigFile) + if config.EmbeddedConfigFile != "" { + fmt.Fprintf(&versionString, " with embedded mTLS config %s", config.EmbeddedConfigFile) } return versionString.String()