Skip to content

Commit

Permalink
Agent/ca sha256 (elastic#16217)
Browse files Browse the repository at this point in the history
* 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 elastic#16019
  • Loading branch information
ph committed Feb 12, 2020
1 parent 45e36fc commit e572b3c
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 94 deletions.
8 changes: 1 addition & 7 deletions agent/kibana/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}

Expand Down
24 changes: 11 additions & 13 deletions libbeat/common/transport/tlscommon/ca_pinning.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -48,15 +37,15 @@ 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
// that match the provided pin.
for _, chain := range verifiedChains {
for _, certificate := range chain {
h := Fingerprint(certificate)
if hashes.Matches(h) {
if matches(hashes, h) {
return nil
}
}
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion libbeat/common/transport/tlscommon/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion libbeat/common/transport/tlscommon/tls_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 51 additions & 44 deletions x-pack/agent/pkg/agent/application/enroll_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {

Expand All @@ -72,11 +94,7 @@ func NewEnrollCmd(

return NewEnrollCmdWithStore(
log,
url,
CAs,
enrollAPIKey,
id,
userProvidedMetadata,
options,
configPath,
store,
)
Expand All @@ -85,44 +103,33 @@ 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)
if err != nil {
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
}

Expand All @@ -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,
},
}

Expand All @@ -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 {
Expand Down
50 changes: 29 additions & 21 deletions x-pack/agent/pkg/agent/application/enroll_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
}
Expand Down
24 changes: 17 additions & 7 deletions x-pack/agent/pkg/agent/cmd/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit e572b3c

Please sign in to comment.