From 3c87de479ad7113940253590a0a5554d25d6975d Mon Sep 17 00:00:00 2001 From: Pier-Hugues Pellerin Date: Mon, 10 Feb 2020 10:16:57 -0500 Subject: [PATCH] Allow to use a ca_sha256 when enroll an Agent When you enroll an agent you can specify the `certificate_authorities`, but when you fallback on the OS trust store you may want to be able to check which CA was used to validate the remote server chain this PR allow to define a CASHA256 to validate the remote server. Based on work from #16019 --- agent/kibana/client.go | 8 +- .../common/transport/tlscommon/ca_pinning.go | 24 +++-- libbeat/common/transport/tlscommon/config.go | 2 +- .../common/transport/tlscommon/tls_config.go | 2 +- .../agent/pkg/agent/application/enroll_cmd.go | 95 ++++++++++--------- .../pkg/agent/application/enroll_cmd_test.go | 50 ++++++---- x-pack/agent/pkg/agent/cmd/enroll.go | 24 +++-- 7 files changed, 111 insertions(+), 94 deletions(-) diff --git a/agent/kibana/client.go b/agent/kibana/client.go index 7b384751549..1535198888a 100644 --- a/agent/kibana/client.go +++ b/agent/kibana/client.go @@ -77,7 +77,7 @@ func New( } // NewConfigFromURL returns a Kibana Config based on a received host. -func NewConfigFromURL(kURL string, CAs []string) (*Config, error) { +func NewConfigFromURL(kURL string) (*Config, error) { u, err := url.Parse(kURL) if err != nil { return nil, errors.Wrap(err, "could not parse Kibana url") @@ -97,12 +97,6 @@ func NewConfigFromURL(kURL string, CAs []string) (*Config, error) { c.Username = username c.Password = password - if len(CAs) > 0 { - c.TLS = &tlscommon.Config{ - CAs: CAs, - } - } - return &c, nil } diff --git a/libbeat/common/transport/tlscommon/ca_pinning.go b/libbeat/common/transport/tlscommon/ca_pinning.go index d83bf533d13..e489ca6d6f4 100644 --- a/libbeat/common/transport/tlscommon/ca_pinning.go +++ b/libbeat/common/transport/tlscommon/ca_pinning.go @@ -28,17 +28,6 @@ import ( // ErrCAPinMissmatch is returned when no pin is matched in the verified chain. var ErrCAPinMissmatch = errors.New("provided CA certificate pins doesn't match any of the certificate authorities used to validate the certificate") -type pins []string - -func (p pins) Matches(candidate string) bool { - for _, pin := range p { - if pin == candidate { - return true - } - } - return false -} - // verifyPeerCertFunc is a callback defined on the tls.Config struct that will called when a // TLS connection is used. type verifyPeerCertFunc func([][]byte, [][]*x509.Certificate) error @@ -48,7 +37,7 @@ type verifyPeerCertFunc func([][]byte, [][]*x509.Certificate) error // NOTE: Defining a PIN to check certificates is not a replacement for the normal TLS validations it's // an additional validation. In fact if you set `InsecureSkipVerify` to true and a PIN, the // verifiedChains variable will be empty and the added validation will fail. -func MakeCAPinCallback(hashes pins) func([][]byte, [][]*x509.Certificate) error { +func MakeCAPinCallback(hashes []string) func([][]byte, [][]*x509.Certificate) error { return func(_ [][]byte, verifiedChains [][]*x509.Certificate) error { // The chain of trust has been already established before the call to the VerifyPeerCertificate // function, after we go through the chain to make sure we have at least a certificate certificate @@ -56,7 +45,7 @@ func MakeCAPinCallback(hashes pins) func([][]byte, [][]*x509.Certificate) error for _, chain := range verifiedChains { for _, certificate := range chain { h := Fingerprint(certificate) - if hashes.Matches(h) { + if matches(hashes, h) { return nil } } @@ -71,3 +60,12 @@ func Fingerprint(certificate *x509.Certificate) string { hash := sha256.Sum256(certificate.RawSubjectPublicKeyInfo) return base64.StdEncoding.EncodeToString(hash[:]) } + +func matches(pins []string, candidate string) bool { + for _, pin := range pins { + if pin == candidate { + return true + } + } + return false +} diff --git a/libbeat/common/transport/tlscommon/config.go b/libbeat/common/transport/tlscommon/config.go index 3fdaeced560..8d7650eb5bf 100644 --- a/libbeat/common/transport/tlscommon/config.go +++ b/libbeat/common/transport/tlscommon/config.go @@ -33,7 +33,7 @@ type Config struct { Certificate CertificateConfig `config:",inline" yaml:",inline"` CurveTypes []tlsCurveType `config:"curve_types" yaml:"curve_types,omitempty"` Renegotiation tlsRenegotiationSupport `config:"renegotiation" yaml:"renegotiation"` - CASha256 pins `config:"ca_sha256" yaml:"ca_sha256,omitempty"` + CASha256 []string `config:"ca_sha256" yaml:"ca_sha256,omitempty"` } // LoadTLSConfig will load a certificate from config with all TLS based keys diff --git a/libbeat/common/transport/tlscommon/tls_config.go b/libbeat/common/transport/tlscommon/tls_config.go index 41c574bc078..5d8ce936029 100644 --- a/libbeat/common/transport/tlscommon/tls_config.go +++ b/libbeat/common/transport/tlscommon/tls_config.go @@ -67,7 +67,7 @@ type TLSConfig struct { // CASha256 is the CA certificate pin, this is used to validate the CA that will be used to trust // the server certificate. - CASha256 pins + CASha256 []string } // ToConfig generates a tls.Config object. Note, you must use BuildModuleConfig to generate a config with diff --git a/x-pack/agent/pkg/agent/application/enroll_cmd.go b/x-pack/agent/pkg/agent/application/enroll_cmd.go index 1f9d57710a6..ee2bf521847 100644 --- a/x-pack/agent/pkg/agent/application/enroll_cmd.go +++ b/x-pack/agent/pkg/agent/application/enroll_cmd.go @@ -13,6 +13,7 @@ import ( "gopkg.in/yaml.v2" "github.com/elastic/beats/agent/kibana" + "github.com/elastic/beats/libbeat/common/transport/tlscommon" "github.com/elastic/beats/x-pack/agent/pkg/agent/application/info" "github.com/elastic/beats/x-pack/agent/pkg/agent/errors" "github.com/elastic/beats/x-pack/agent/pkg/agent/storage" @@ -43,24 +44,45 @@ type clienter interface { // EnrollCmd is an enroll subcommand that interacts between the Kibana API and the Agent. type EnrollCmd struct { - log *logger.Logger - enrollAPIKey string - client clienter - id string - userProvidedMetadata map[string]interface{} - configStore store - kibanaConfig *kibana.Config + log *logger.Logger + options *EnrollCmdOption + client clienter + configStore store + kibanaConfig *kibana.Config +} + +// EnrollCmdOption define all the supported enrollment option. +type EnrollCmdOption struct { + ID string + URL string + CAs []string + CASha256 []string + UserProvidedMetadata map[string]interface{} + EnrollAPIKey string +} + +func (e *EnrollCmdOption) KibanaConfig() (*kibana.Config, error) { + cfg, err := kibana.NewConfigFromURL(e.URL) + if err != nil { + return nil, err + } + + // Add any SSL options from the CLI. + if len(e.CAs) > 0 || len(e.CASha256) > 0 { + cfg.TLS = &tlscommon.Config{ + CAs: e.CAs, + CASha256: e.CASha256, + } + } + + return cfg, nil } // NewEnrollCmd creates a new enroll command that will registers the current beats to the remote // system. func NewEnrollCmd( log *logger.Logger, - url string, - CAs []string, - enrollAPIKey string, - id string, - userProvidedMetadata map[string]interface{}, + options *EnrollCmdOption, configPath string, ) (*EnrollCmd, error) { @@ -72,11 +94,7 @@ func NewEnrollCmd( return NewEnrollCmdWithStore( log, - url, - CAs, - enrollAPIKey, - id, - userProvidedMetadata, + options, configPath, store, ) @@ -85,20 +103,17 @@ func NewEnrollCmd( //NewEnrollCmdWithStore creates an new enrollment and accept a custom store. func NewEnrollCmdWithStore( log *logger.Logger, - url string, - CAs []string, - enrollAPIKey string, - id string, - userProvidedMetadata map[string]interface{}, + options *EnrollCmdOption, configPath string, store store, ) (*EnrollCmd, error) { - cfg, err := kibana.NewConfigFromURL(url, CAs) + + cfg, err := options.KibanaConfig() if err != nil { return nil, errors.New(err, - "invalid Kibana URL", - errors.TypeNetwork, - errors.M(errors.MetaKeyURI, url)) + "invalid Kibana configuration", + errors.TypeConfig, + errors.M(errors.MetaKeyURI, options.URL)) } client, err := fleetapi.NewWithConfig(log, cfg) @@ -106,23 +121,15 @@ func NewEnrollCmdWithStore( return nil, errors.New(err, "fail to create the API client", errors.TypeNetwork, - errors.M(errors.MetaKeyURI, url)) - } - - if userProvidedMetadata == nil { - userProvidedMetadata = make(map[string]interface{}) + errors.M(errors.MetaKeyURI, options.URL)) } - // Extract the token - // Create the kibana client return &EnrollCmd{ - log: log, - client: client, - enrollAPIKey: enrollAPIKey, - id: id, - userProvidedMetadata: userProvidedMetadata, - kibanaConfig: cfg, - configStore: store, + log: log, + client: client, + options: options, + kibanaConfig: cfg, + configStore: store, }, nil } @@ -136,12 +143,12 @@ func (c *EnrollCmd) Execute() error { } r := &fleetapi.EnrollRequest{ - EnrollAPIKey: c.enrollAPIKey, - SharedID: c.id, + EnrollAPIKey: c.options.EnrollAPIKey, + SharedID: c.options.ID, Type: fleetapi.PermanentEnroll, Metadata: fleetapi.Metadata{ Local: metadata, - UserProvided: c.userProvidedMetadata, + UserProvided: c.options.UserProvidedMetadata, }, } @@ -163,7 +170,7 @@ func (c *EnrollCmd) Execute() error { } if err := c.configStore.Save(reader); err != nil { - return errors.New(err, "could not save enroll credentials", errors.TypeFilesystem) + return errors.New(err, "could not save enrollment information", errors.TypeFilesystem) } if _, err := info.NewAgentInfo(); err != nil { diff --git a/x-pack/agent/pkg/agent/application/enroll_cmd_test.go b/x-pack/agent/pkg/agent/application/enroll_cmd_test.go index 09ad7cdbca6..a9b631261d1 100644 --- a/x-pack/agent/pkg/agent/application/enroll_cmd_test.go +++ b/x-pack/agent/pkg/agent/application/enroll_cmd_test.go @@ -81,11 +81,13 @@ func TestEnroll(t *testing.T) { store := &mockStore{Err: errors.New("fail to save")} cmd, err := NewEnrollCmdWithStore( log, - url, - []string{caFile}, - "my-enrollment-token", - "my-id", - map[string]interface{}{"custom": "customize"}, + &EnrollCmdOption{ + ID: "my-id", + URL: url, + CAs: []string{caFile}, + EnrollAPIKey: "my-enrollment-token", + UserProvidedMetadata: map[string]interface{}{"custom": "customize"}, + }, "", store, ) @@ -133,11 +135,13 @@ func TestEnroll(t *testing.T) { store := &mockStore{} cmd, err := NewEnrollCmdWithStore( log, - url, - []string{caFile}, - "my-enrollment-api-key", - "my-id", - map[string]interface{}{"custom": "customize"}, + &EnrollCmdOption{ + ID: "my-id", + URL: url, + CAs: []string{caFile}, + EnrollAPIKey: "my-enrollment-api-key", + UserProvidedMetadata: map[string]interface{}{"custom": "customize"}, + }, "", store, ) @@ -189,11 +193,13 @@ func TestEnroll(t *testing.T) { store := &mockStore{} cmd, err := NewEnrollCmdWithStore( log, - url, - make([]string, 0), - "my-enrollment-api-key", - "my-id", - map[string]interface{}{"custom": "customize"}, + &EnrollCmdOption{ + ID: "my-id", + URL: url, + CAs: []string{}, + EnrollAPIKey: "my-enrollment-api-key", + UserProvidedMetadata: map[string]interface{}{"custom": "customize"}, + }, "", store, ) @@ -231,11 +237,13 @@ func TestEnroll(t *testing.T) { store := &mockStore{} cmd, err := NewEnrollCmdWithStore( log, - url, - make([]string, 0), - "my-enrollment-token", - "my-id", - map[string]interface{}{"custom": "customize"}, + &EnrollCmdOption{ + ID: "my-id", + URL: url, + CAs: []string{}, + EnrollAPIKey: "my-enrollment-token", + UserProvidedMetadata: map[string]interface{}{"custom": "customize"}, + }, "", store, ) @@ -319,7 +327,7 @@ func readConfig(raw []byte) (*FleetAgentConfig, error) { return nil, err } - cfg := &FleetAgentConfig{} + cfg := defaultFleetAgentConfig() if err := config.Unpack(cfg); err != nil { return nil, err } diff --git a/x-pack/agent/pkg/agent/cmd/enroll.go b/x-pack/agent/pkg/agent/cmd/enroll.go index 697f74cfc11..abd3d2c9e16 100644 --- a/x-pack/agent/pkg/agent/cmd/enroll.go +++ b/x-pack/agent/pkg/agent/cmd/enroll.go @@ -36,7 +36,8 @@ func newEnrollCommandWithArgs(flags *globalFlags, _ []string, streams *cli.IOStr }, } - cmd.Flags().StringP("certificate-authorities", "a", "", "Comma separated list of root certificate for server verifications") + cmd.Flags().StringP("certificate_authorities", "a", "", "Comma separated list of root certificate for server verifications") + cmd.Flags().StringP("ca_sha256", "p", "", "Comma separated list of certificate authorities hash pins used for certificate verifications") cmd.Flags().BoolP("force", "f", false, "Force overwrite the current and do not prompt for confirmation") return cmd @@ -71,20 +72,29 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command, flags *globalFlags, args url := args[0] enrollmentToken := args[1] - caStr, _ := cmd.Flags().GetString("certificate-authorities") + caStr, _ := cmd.Flags().GetString("certificate_authorities") CAs := cli.StringToSlice(caStr) + caSHA256str, _ := cmd.Flags().GetString("ca_sha256") + caSHA256 := cli.StringToSlice(caSHA256str) + delay(defaultDelay) + options := application.EnrollCmdOption{ + ID: "", // TODO(ph), This should not be an empty string, will clarify in a new PR. + EnrollAPIKey: enrollmentToken, + URL: url, + CAs: CAs, + CASha256: caSHA256, + UserProvidedMetadata: make(map[string]interface{}), + } + c, err := application.NewEnrollCmd( logger, - url, - CAs, - enrollmentToken, - "", - nil, + &options, flags.PathConfigFile, ) + if err != nil { return err }