Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
AndersonQ committed Sep 11, 2024
1 parent 0af82e8 commit 6c597be
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 8 deletions.
18 changes: 18 additions & 0 deletions internal/pkg/agent/cmd/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func addEnrollFlags(cmd *cobra.Command) {
cmd.Flags().StringP("ca-sha256", "p", "", "Comma-separated list of certificate authority hash pins for server verification used by Elastic Agent and Fleet Server")
cmd.Flags().StringP("elastic-agent-cert", "", "", "Elastic Agent client certificate to use with Fleet Server during mTLS authentication")
cmd.Flags().StringP("elastic-agent-cert-key", "", "", "Elastic Agent client private key to use with Fleet Server during mTLS authentication")
cmd.Flags().StringP("elastic-agent-cert-key-passphrase", "", "", "Path for private key passphrase file used to decrypt Elastic Agent client certificate key")
cmd.Flags().BoolP("insecure", "i", false, "Allow insecure connection made by the Elastic Agent. It's also required to use a Fleet Server on a HTTP endpoint")
cmd.Flags().StringP("staging", "", "", "Configures Elastic Agent to download artifacts from a staging build")
cmd.Flags().StringP("proxy-url", "", "", "Configures the proxy URL: when bootstrapping Fleet Server, it's the proxy used by Fleet Server to connect to Elasticsearch; when enrolling the Elastic Agent to Fleet Server, it's the proxy used by the Elastic Agent to connect to Fleet Server")
Expand Down Expand Up @@ -111,6 +112,16 @@ func validateEnrollFlags(cmd *cobra.Command) error {
if key != "" && !filepath.IsAbs(key) {
return errors.New("--elastic-agent-cert-key must be provided as an absolute path", errors.M("path", key), errors.TypeConfig)
}
keyPassphrase, _ := cmd.Flags().GetString("elastic-agent-cert-key-passphrase")
if keyPassphrase != "" {
if !filepath.IsAbs(keyPassphrase) {
return errors.New("--elastic-agent-cert-key-passphrase must be provided as an absolute path", errors.M("path", keyPassphrase), errors.TypeConfig)
}

if cert == "" || key == "" {
return errors.New("--elastic-agent-cert and --elastic-agent-cert-key must be provided when using --elastic-agent-cert-key-passphrase", errors.M("path", keyPassphrase), errors.TypeConfig)
}
}
esCa, _ := cmd.Flags().GetString("fleet-server-es-ca")
if esCa != "" && !filepath.IsAbs(esCa) {
return errors.New("--fleet-server-es-ca must be provided as an absolute path", errors.M("path", esCa), errors.TypeConfig)
Expand Down Expand Up @@ -180,6 +191,7 @@ func buildEnrollmentFlags(cmd *cobra.Command, url string, token string) []string
ca, _ := cmd.Flags().GetString("certificate-authorities")
cert, _ := cmd.Flags().GetString("elastic-agent-cert")
key, _ := cmd.Flags().GetString("elastic-agent-cert-key")
keyPassphrase, _ := cmd.Flags().GetString("elastic-agent-cert-key-passphrase")
sha256, _ := cmd.Flags().GetString("ca-sha256")
insecure, _ := cmd.Flags().GetBool("insecure")
staging, _ := cmd.Flags().GetString("staging")
Expand Down Expand Up @@ -285,6 +297,10 @@ func buildEnrollmentFlags(cmd *cobra.Command, url string, token string) []string
args = append(args, "--elastic-agent-cert-key")
args = append(args, key)
}
if keyPassphrase != "" {
args = append(args, "--elastic-agent-cert-key-passphrase")
args = append(args, keyPassphrase)
}
if sha256 != "" {
args = append(args, "--ca-sha256")
args = append(args, sha256)
Expand Down Expand Up @@ -422,6 +438,7 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
caSHA256 := cli.StringToSlice(caSHA256str)
cert, _ := cmd.Flags().GetString("elastic-agent-cert")
key, _ := cmd.Flags().GetString("elastic-agent-cert-key")
keyPassphrase, _ := cmd.Flags().GetString("elastic-agent-cert-key-passphrase")

ctx := handleSignal(context.Background())

Expand Down Expand Up @@ -449,6 +466,7 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
CASha256: caSHA256,
Certificate: cert,
Key: key,
KeyPassphrasePath: keyPassphrase,
Insecure: insecure,
UserProvidedMetadata: make(map[string]interface{}),
Staging: staging,
Expand Down
6 changes: 4 additions & 2 deletions internal/pkg/agent/cmd/enroll_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ type enrollCmdOption struct {
CASha256 []string `yaml:"ca_sha256,omitempty"`
Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"`
KeyPassphrasePath string `yaml:"key_passphrase_path,omitempty"`
Insecure bool `yaml:"insecure,omitempty"`
EnrollAPIKey string `yaml:"enrollment_key,omitempty"`
Staging string `yaml:"staging,omitempty"`
Expand Down Expand Up @@ -149,8 +150,9 @@ func (e *enrollCmdOption) remoteConfig() (remote.Config, error) {
}
if e.Certificate != "" || e.Key != "" {
tlsCfg.Certificate = tlscommon.CertificateConfig{
Certificate: e.Certificate,
Key: e.Key,
Certificate: e.Certificate,
Key: e.Key,
PassphrasePath: e.KeyPassphrasePath,
}
}

Expand Down
205 changes: 199 additions & 6 deletions internal/pkg/agent/cmd/enroll_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ package cmd
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strconv"
"sync/atomic"
Expand All @@ -22,6 +27,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/elastic/elastic-agent-libs/testing/certutil"
"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
"github.com/elastic/elastic-agent/internal/pkg/agent/errors"
"github.com/elastic/elastic-agent/internal/pkg/cli"
Expand Down Expand Up @@ -113,6 +119,95 @@ func TestEnroll(t *testing.T) {
},
))

t.Run("successfully enroll with mTLS and save fleet config in the store", func(t *testing.T) {
agentCertPassphrase := "a really secure passphrase"
passphrasePath := filepath.Join(t.TempDir(), "passphrase")
err := os.WriteFile(
passphrasePath,
[]byte(agentCertPassphrase),
0666)
require.NoError(t, err,
"could not write agent child certificate key passphrase to temp directory")

tlsCfg, _, agentCertPathPair, fleetRootPathPair, _ :=
mTLSServer(t, agentCertPassphrase)

mockHandlerCalled := false
mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mockHandlerCalled = true
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`
{
"action": "created",
"item": {
"id": "a9328860-ec54-11e9-93c4-d72ab8a69391",
"active": true,
"policy_id": "69f3f5a0-ec52-11e9-93c4-d72ab8a69391",
"type": "PERMANENT",
"enrolled_at": "2019-10-11T18:26:37.158Z",
"user_provided_metadata": {
"custom": "customize"
},
"local_metadata": {
"platform": "linux",
"version": "8.0.0"
},
"actions": [],
"access_api_key": "my-access-api-key"
}
}`))
})

s := httptest.NewUnstartedServer(mockHandler)
s.TLS = tlsCfg
s.StartTLS()
defer s.Close()

store := &mockStore{}
enrollOptions := enrollCmdOption{
CAs: []string{string(fleetRootPathPair.Cert)},
Certificate: string(agentCertPathPair.Cert),
Key: string(agentCertPathPair.Key),
KeyPassphrasePath: passphrasePath,

URL: s.URL,
EnrollAPIKey: "my-enrollment-api-key",
UserProvidedMetadata: map[string]interface{}{"custom": "customize"},
SkipCreateSecret: skipCreateSecret,
SkipDaemonRestart: true,
}
cmd, err := newEnrollCmd(
log,
&enrollOptions,
"",
store,
)
require.NoError(t, err, "could not create enroll command")

streams, _, _, _ := cli.NewTestingIOStreams()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()

err = cmd.Execute(ctx, streams)
require.NoError(t, err, "enroll command returned and unexpected error")

fleetCfg, err := readConfig(store.Content)
require.NoError(t, err, "could not read fleet config from store")

assert.True(t, mockHandlerCalled, "mock handler should have been called")
fleetTLS := fleetCfg.Client.Transport.TLS

require.NotNil(t, fleetTLS, `fleet client TLS config should have been set`)
assert.Equal(t, s.URL, fmt.Sprintf("%s://%s",
fleetCfg.Client.Protocol, fleetCfg.Client.Host))
assert.Equal(t, enrollOptions.CAs, fleetTLS.CAs)
assert.Equal(t,
enrollOptions.Certificate, fleetTLS.Certificate.Certificate)
assert.Equal(t, enrollOptions.Key, fleetTLS.Certificate.Key)
assert.Equal(t,
enrollOptions.KeyPassphrasePath, fleetTLS.Certificate.PassphrasePath)
})

t.Run("successfully enroll with TLS and save access api key in the store", withTLSServer(
func(t *testing.T) *http.ServeMux {
mux := http.NewServeMux()
Expand Down Expand Up @@ -167,7 +262,7 @@ func TestEnroll(t *testing.T) {
defer cancel()

if err := cmd.Execute(ctx, streams); err != nil {
t.Fatalf("enrrol coms returned and unexpected error: %v", err)
t.Fatalf("enroll command returned and unexpected error: %v", err)
}

config, err := readConfig(store.Content)
Expand Down Expand Up @@ -229,7 +324,7 @@ func TestEnroll(t *testing.T) {
defer cancel()

if err := cmd.Execute(ctx, streams); err != nil {
t.Fatalf("enrrol coms returned and unexpected error: %v", err)
t.Fatalf("enroll command returned and unexpected error: %v", err)
}

assert.True(t, store.Called)
Expand Down Expand Up @@ -522,7 +617,8 @@ func TestValidateEnrollFlags(t *testing.T) {
t.Run("no flags", func(t *testing.T) {
cmd := newEnrollCommandWithArgs([]string{}, streams)
err := validateEnrollFlags(cmd)
require.NoError(t, err)

assert.NoError(t, err)
})

t.Run("service_token and a service_token_path are mutually exclusive", func(t *testing.T) {
Expand All @@ -531,12 +627,34 @@ func TestValidateEnrollFlags(t *testing.T) {
require.NoError(t, err)
err = cmd.Flags().Set("fleet-server-service-token", "token-value")
require.NoError(t, err)

err = validateEnrollFlags(cmd)
require.Error(t, err)
assert.Error(t, err)

var agentErr errors.Error
require.ErrorAs(t, err, &agentErr)
require.Equal(t, errors.TypeConfig, agentErr.Type())
assert.ErrorAs(t, err, &agentErr)
assert.Equal(t, errors.TypeConfig, agentErr.Type())
})

t.Run("elastic-agent-cert-key does not require key-passphrase", func(t *testing.T) {
cmd := newEnrollCommandWithArgs([]string{}, streams)
err := cmd.Flags().Set("elastic-agent-cert-key", "/path/to/elastic-agent-cert-key")

err = validateEnrollFlags(cmd)

assert.NoError(t, err, "validateEnrollFlags should have succeeded")
})

t.Run("elastic-agent-cert-key-passphrase requires certificate and key", func(t *testing.T) {
cmd := newEnrollCommandWithArgs([]string{}, streams)
err := cmd.Flags().Set("elastic-agent-cert-key-passphrase", "/path/to/elastic-agent-cert-key-passphrase")

err = validateEnrollFlags(cmd)

assert.Error(t, err, "validateEnrollFlags should not accept only --elastic-agent-cert-key-passphrase")
var agentErr errors.Error
assert.ErrorAs(t, err, &agentErr)
assert.Equal(t, errors.TypeConfig, agentErr.Type())
})
}

Expand Down Expand Up @@ -645,6 +763,81 @@ func withTLSServer(
}
}

// mTLSServer generates the necessary certificates and tls.Config for a mTLS
// server. If agentPassphrase is given, it'll encrypt the agent's client
// certificate key.
// It returns the *tls.Config to be used with httptest.NewUnstartedServer,
// the agentRootPair, agentChildPair, fleetRootPathPair, fleetCertPathPair.
// Theirs Cert and Key values are the path to the respective certificate and
// certificate key in PEM format.
func mTLSServer(t *testing.T, agentPassphrase string) (
*tls.Config, certutil.Pair, certutil.Pair, certutil.Pair, certutil.Pair) {

dir := t.TempDir()

// generate certificates
agentRootPair, agentCertPair, err := certutil.NewRootAndChildCerts()
require.NoError(t, err, "could not create agent's root CA and child certificate")

// encrypt keys if needed
if agentPassphrase != "" {
agentChildDERKey, _ := pem.Decode(agentCertPair.Key)
require.NoError(t, err, "could not create tls.Certificates from child certificate")

encPem, err := x509.EncryptPEMBlock( //nolint:staticcheck // we need to drop support for this, but while we don't, it needs to be tested.
rand.Reader,
"EC PRIVATE KEY",
agentChildDERKey.Bytes,
[]byte(agentPassphrase),
x509.PEMCipherAES128)
require.NoError(t, err, "failed encrypting agent child certificate key block")

agentCertPair.Key = pem.EncodeToMemory(encPem)
}

agentRootPathPair := savePair(t, dir, "agent_ca", agentRootPair)
agentCertPathPair := savePair(t, dir, "agent_cert", agentCertPair)

fleetRootPair, fleetChildPair, err := certutil.NewRootAndChildCerts()
require.NoError(t, err, "could not create fleet-server's root CA and child certificate")
fleetRootPathPair := savePair(t, dir, "fleet_ca", fleetRootPair)
fleetCertPathPair := savePair(t, dir, "fleet_cert", fleetChildPair)

// configure server's TLS
fleetRootCertPool := x509.NewCertPool()
fleetRootCertPool.AppendCertsFromPEM(fleetRootPair.Cert)
cert, err := tls.X509KeyPair(fleetRootPair.Cert, fleetRootPair.Key)
require.NoError(t, err, "could not create tls.Certificates from child certificate")

agentRootCertPool := x509.NewCertPool()
agentRootCertPool.AppendCertsFromPEM(agentRootPair.Cert)

cfg := &tls.Config{ //nolint:gosec // it's just a test
RootCAs: fleetRootCertPool,
Certificates: []tls.Certificate{cert},
ClientCAs: agentRootCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}

return cfg, agentRootPathPair, agentCertPathPair, fleetRootPathPair, fleetCertPathPair
}

// savePair saves the key pair on {dest}/{name}.pem and {dest}/{name}_key.pem
func savePair(t *testing.T, dest string, name string, pair certutil.Pair) certutil.Pair {
certPath := filepath.Join(dest, name+".pem")
err := os.WriteFile(certPath, pair.Cert, 0o600)
require.NoErrorf(t, err, "could not save %s certificate", name)

keyPath := filepath.Join(dest, name+"_key.pem")
err = os.WriteFile(keyPath, pair.Key, 0o600)
require.NoErrorf(t, err, "could not save %s certificate key", name)

return certutil.Pair{
Cert: []byte(certPath),
Key: []byte(keyPath),
}
}

func bytesToTMPFile(b []byte) (string, error) {
f, err := os.CreateTemp("", "prefix")
if err != nil {
Expand Down

0 comments on commit 6c597be

Please sign in to comment.