Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mTLS flags to install and enroll commands #4007

Merged
merged 16 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user’s deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: feature

# Change summary; a 80ish characters long description of the change.
summary: Add mTLS flags to install/enroll

# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
description: |
Add mTLS flags to the install/enroll commands to allow fleet-server to
use client certs when connecting to Elasticsearch, and to allow
elastic-agent to use client certs when connecting to fleet-server.
Fleet-server will use the CAs passed in `--certificate-authorities` to
validate any client certs. Agent client certs do not influence auth in
fleet-server, an enrollment token, or API key is still required.

# Affected component; a word indicating the component this changeset affects.
component:

# PR URL; optional; the PR number that added the changeset.
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
# Please provide it if you are adding a fragment for a different PR.
#pr: https://github.com/owner/repo/1234

# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
#issue: https://github.com/owner/repo/1234
67 changes: 66 additions & 1 deletion internal/pkg/agent/cmd/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func addEnrollFlags(cmd *cobra.Command) {
cmd.Flags().StringP("fleet-server-es-ca", "", "", "Path to certificate authority to use with communicate with elasticsearch")
cmd.Flags().StringP("fleet-server-es-ca-trusted-fingerprint", "", "", "Elasticsearch certificate authority's SHA256 fingerprint")
cmd.Flags().BoolP("fleet-server-es-insecure", "", false, "Disables validation of certificates")
cmd.Flags().StringP("fleet-server-es-cert", "", "", "Client certificate to use when connecting to Elasticsearch.")
cmd.Flags().StringP("fleet-server-es-cert-key", "", "", "Client private key to use when connecing to Elasticsearch.")
cmd.Flags().StringP("fleet-server-service-token", "", "", "Service token to use for communication with elasticsearch")
cmd.Flags().StringP("fleet-server-service-token-path", "", "", "Filepath for service token secret file to use for communication with elasticsearch")
cmd.Flags().StringP("fleet-server-policy", "", "", "Start and run a Fleet Server on this specific policy")
Expand All @@ -63,10 +65,13 @@ func addEnrollFlags(cmd *cobra.Command) {
cmd.Flags().StringP("fleet-server-cert", "", "", "Certificate to use for exposed Fleet Server HTTPS endpoint")
cmd.Flags().StringP("fleet-server-cert-key", "", "", "Private key to use for exposed Fleet Server HTTPS endpoint")
cmd.Flags().StringP("fleet-server-cert-key-passphrase", "", "", "Path for private key passphrase file used to decrypt certificate key")
cmd.Flags().StringP("fleet-server-client-auth", "", "", "Fleet-server mTLS client authentication for connecting elastic-agents. Must be one of [none, optional, required]")
cmd.Flags().StringSliceP("header", "", []string{}, "Headers used in communication with elasticsearch")
cmd.Flags().BoolP("fleet-server-insecure-http", "", false, "Expose Fleet Server over HTTP (not recommended; insecure)")
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().StringP("elastic-agent-cert", "", "", "Elastic-agent client certificate to use with fleet-server during authentication")
cmd.Flags().StringP("elastic-agent-cert-key", "", "", "Elastic-agent client certificate to use with fleet-server during authentication")
cmd.Flags().BoolP("insecure", "i", false, "Allow insecure connection to fleet-server")
cmd.Flags().StringP("staging", "", "", "Configures agent to download artifacts from a staging build")
cmd.Flags().StringP("proxy-url", "", "", "Configures the proxy url")
Expand All @@ -78,18 +83,35 @@ func addEnrollFlags(cmd *cobra.Command) {
cmd.Flags().Bool("skip-daemon-reload", false, "Skip daemon reload after enrolling")
cmd.Flags().StringSliceP("tag", "", []string{}, "User set tags")

cmd.Flags().MarkHidden("skip-daemon-reload") //nolint:errcheck // an error is only returned if the flag does not exist.
cmd.Flags().MarkHidden("skip-daemon-reload") //nolint:errcheck // an error is only returned if the flag does not exist.
cmd.Flags().MarkHidden("fleet-server-client-auth") //nolint:errcheck // FIXME this is not fully implemented
}

func validateEnrollFlags(cmd *cobra.Command) error {
ca, _ := cmd.Flags().GetString("certificate-authorities")
if ca != "" && !filepath.IsAbs(ca) {
return errors.New("--certificate-authorities must be provided as an absolute path", errors.M("path", ca), errors.TypeConfig)
}
cert, _ := cmd.Flags().GetString("elastic-agent-cert")
if cert != "" && !filepath.IsAbs(cert) {
return errors.New("--elastic-agent-cert must be provided as an absolute path", errors.M("path", cert), errors.TypeConfig)
}
key, _ := cmd.Flags().GetString("elastic-agent-cert-key")
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)
}
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)
}
esCert, _ := cmd.Flags().GetString("fleet-server-es-cert")
if esCert != "" && !filepath.IsAbs(esCert) {
return errors.New("--fleet-server-es-cert must be provided as an absolute path", errors.M("path", esCert), errors.TypeConfig)
}
esCertKey, _ := cmd.Flags().GetString("fleet-server-es-cert-key")
if esCertKey != "" && !filepath.IsAbs(esCertKey) {
return errors.New("--fleet-server-es-cert-key must be provided as an absolute path", errors.M("path", esCertKey), errors.TypeConfig)
}
fCert, _ := cmd.Flags().GetString("fleet-server-cert")
if fCert != "" && !filepath.IsAbs(fCert) {
return errors.New("--fleet-server-cert must be provided as an absolute path", errors.M("path", fCert), errors.TypeConfig)
Expand All @@ -110,6 +132,14 @@ func validateEnrollFlags(cmd *cobra.Command) error {
if fPassphrase != "" && !filepath.IsAbs(fPassphrase) {
return errors.New("--fleet-server-cert-key-passphrase must be provided as an absolute path", errors.M("path", fPassphrase), errors.TypeConfig)
}
fClientAuth, _ := cmd.Flags().GetString("fleet-server-client-auth")
switch fClientAuth {
case "":
case "none", "optional", "required":
// NOTE we can split this case if we want to do additional checks when optional or required is passed.
michel-laterman marked this conversation as resolved.
Show resolved Hide resolved
default:
return errors.New("--fleet-server-client-auth must be one of [none, optional, required]")
}
return nil
}

Expand All @@ -124,6 +154,8 @@ func buildEnrollmentFlags(cmd *cobra.Command, url string, token string) []string
fElasticSearchCA, _ := cmd.Flags().GetString("fleet-server-es-ca")
fElasticSearchCASHA256, _ := cmd.Flags().GetString("fleet-server-es-ca-trusted-fingerprint")
fElasticSearchInsecure, _ := cmd.Flags().GetBool("fleet-server-es-insecure")
fElasticSearchClientCert, _ := cmd.Flags().GetString("fleet-server-es-cert")
fElasticSearchClientCertKey, _ := cmd.Flags().GetString("fleet-server-es-cert-key")
fServiceToken, _ := cmd.Flags().GetString("fleet-server-service-token")
fServiceTokenPath, _ := cmd.Flags().GetString("fleet-server-service-token-path")
fPolicy, _ := cmd.Flags().GetString("fleet-server-policy")
Expand All @@ -132,9 +164,12 @@ func buildEnrollmentFlags(cmd *cobra.Command, url string, token string) []string
fCert, _ := cmd.Flags().GetString("fleet-server-cert")
fCertKey, _ := cmd.Flags().GetString("fleet-server-cert-key")
fPassphrase, _ := cmd.Flags().GetString("fleet-server-cert-key-passphrase")
fClientAuth, _ := cmd.Flags().GetString("fleet-server-client-auth")
fHeaders, _ := cmd.Flags().GetStringSlice("header")
fInsecure, _ := cmd.Flags().GetBool("fleet-server-insecure-http")
ca, _ := cmd.Flags().GetString("certificate-authorities")
cert, _ := cmd.Flags().GetString("elastic-agent-cert")
key, _ := cmd.Flags().GetString("elastic-agent-cert-key")
sha256, _ := cmd.Flags().GetString("ca-sha256")
insecure, _ := cmd.Flags().GetBool("insecure")
staging, _ := cmd.Flags().GetString("staging")
Expand Down Expand Up @@ -167,6 +202,14 @@ func buildEnrollmentFlags(cmd *cobra.Command, url string, token string) []string
args = append(args, "--fleet-server-es-ca-trusted-fingerprint")
args = append(args, fElasticSearchCASHA256)
}
if fElasticSearchClientCert != "" {
args = append(args, "--fleet-server-es-cert")
args = append(args, fElasticSearchClientCert)
}
if fElasticSearchClientCertKey != "" {
args = append(args, "--fleet-server-es-cert-key")
args = append(args, fElasticSearchClientCertKey)
}
if fServiceToken != "" {
args = append(args, "--fleet-server-service-token")
args = append(args, fServiceToken)
Expand Down Expand Up @@ -199,6 +242,10 @@ func buildEnrollmentFlags(cmd *cobra.Command, url string, token string) []string
args = append(args, "--fleet-server-cert-key-passphrase")
args = append(args, fPassphrase)
}
if fClientAuth != "" {
args = append(args, "--fleet-server-client-auth")
args = append(args, fClientAuth)
}
if daemonTimeout != 0 {
args = append(args, "--daemon-timeout")
args = append(args, daemonTimeout.String())
Expand All @@ -220,6 +267,14 @@ func buildEnrollmentFlags(cmd *cobra.Command, url string, token string) []string
args = append(args, "--certificate-authorities")
args = append(args, ca)
}
if cert != "" {
args = append(args, "--elastic-agent-cert")
args = append(args, cert)
}
if key != "" {
args = append(args, "--elastic-agent-cert-key")
args = append(args, key)
}
if sha256 != "" {
args = append(args, "--ca-sha256")
args = append(args, sha256)
Expand Down Expand Up @@ -328,6 +383,8 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
fElasticSearchCA, _ := cmd.Flags().GetString("fleet-server-es-ca")
fElasticSearchCASHA256, _ := cmd.Flags().GetString("fleet-server-es-ca-trusted-fingerprint")
fElasticSearchInsecure, _ := cmd.Flags().GetBool("fleet-server-es-insecure")
fElasticSearchClientCert, _ := cmd.Flags().GetString("fleet-server-es-cert")
fElasticSearchClientCertKey, _ := cmd.Flags().GetString("fleet-server-es-cert-key")
fHeaders, _ := cmd.Flags().GetStringSlice("header")
fServiceToken, _ := cmd.Flags().GetString("fleet-server-service-token")
fServiceTokenPath, _ := cmd.Flags().GetString("fleet-server-service-token-path")
Expand All @@ -338,6 +395,7 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
fCert, _ := cmd.Flags().GetString("fleet-server-cert")
fCertKey, _ := cmd.Flags().GetString("fleet-server-cert-key")
fPassphrase, _ := cmd.Flags().GetString("fleet-server-cert-key-passphrase")
fClientAuth, _ := cmd.Flags().GetString("fleet-server-client-auth")
fInsecure, _ := cmd.Flags().GetBool("fleet-server-insecure-http")
proxyURL, _ := cmd.Flags().GetString("proxy-url")
proxyDisabled, _ := cmd.Flags().GetBool("proxy-disabled")
Expand All @@ -352,6 +410,8 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
CAs := cli.StringToSlice(caStr)
caSHA256str, _ := cmd.Flags().GetString("ca-sha256")
caSHA256 := cli.StringToSlice(caSHA256str)
cert, _ := cmd.Flags().GetString("elastic-agent-cert")
key, _ := cmd.Flags().GetString("elastic-agent-cert-key")

ctx := handleSignal(context.Background())

Expand All @@ -369,6 +429,8 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
URL: url,
CAs: CAs,
CASha256: caSHA256,
Certificate: cert,
Key: key,
Insecure: insecure,
UserProvidedMetadata: make(map[string]interface{}),
Staging: staging,
Expand All @@ -385,6 +447,8 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
ElasticsearchCA: fElasticSearchCA,
ElasticsearchCASHA256: fElasticSearchCASHA256,
ElasticsearchInsecure: fElasticSearchInsecure,
ElasticsearchCert: fElasticSearchClientCert,
ElasticsearchCertKey: fElasticSearchClientCertKey,
ServiceToken: fServiceToken,
ServiceTokenPath: fServiceTokenPath,
PolicyID: fPolicy,
Expand All @@ -393,6 +457,7 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
Cert: fCert,
CertKey: fCertKey,
CertKeyPassphrasePath: fPassphrase,
ClientAuth: fClientAuth,
Insecure: fInsecure,
SpawnAgent: !fromInstall,
Headers: mapFromEnvList(fHeaders),
Expand Down
44 changes: 43 additions & 1 deletion internal/pkg/agent/cmd/enroll_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ type enrollCmdFleetServerOption struct {
ElasticsearchCA string
ElasticsearchCASHA256 string
ElasticsearchInsecure bool
ElasticsearchCert string
ElasticsearchCertKey string
ServiceToken string
ServiceTokenPath string
PolicyID string
Expand All @@ -89,6 +91,7 @@ type enrollCmdFleetServerOption struct {
Cert string
CertKey string
CertKeyPassphrasePath string
ClientAuth string
Insecure bool
SpawnAgent bool
Headers map[string]string
Expand All @@ -101,6 +104,8 @@ type enrollCmdOption struct {
InternalURL string `yaml:"-"`
CAs []string `yaml:"ca,omitempty"`
CASha256 []string `yaml:"ca_sha256,omitempty"`
Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"`
Insecure bool `yaml:"insecure,omitempty"`
EnrollAPIKey string `yaml:"enrollment_key,omitempty"`
Staging string `yaml:"staging,omitempty"`
Expand Down Expand Up @@ -137,6 +142,12 @@ func (e *enrollCmdOption) remoteConfig() (remote.Config, error) {
if e.Insecure {
tlsCfg.VerificationMode = tlscommon.VerifyNone
}
if e.Certificate != "" || e.Key != "" {
tlsCfg.Certificate = tlscommon.CertificateConfig{
Certificate: e.Certificate,
Key: e.Key,
}
}

cfg.Transport.TLS = &tlsCfg

Expand Down Expand Up @@ -344,6 +355,8 @@ func (c *enrollCmd) fleetServerBootstrap(ctx context.Context, persistentConfig m
c.options.FleetServer.PolicyID,
c.options.FleetServer.Host, c.options.FleetServer.Port, c.options.FleetServer.InternalPort,
c.options.FleetServer.Cert, c.options.FleetServer.CertKey, c.options.FleetServer.CertKeyPassphrasePath, c.options.FleetServer.ElasticsearchCA, c.options.FleetServer.ElasticsearchCASHA256,
c.options.CAs, c.options.FleetServer.ClientAuth,
c.options.FleetServer.ElasticsearchCert, c.options.FleetServer.ElasticsearchCertKey,
c.options.FleetServer.Headers,
c.options.ProxyURL,
c.options.ProxyDisabled,
Expand Down Expand Up @@ -570,6 +583,8 @@ func (c *enrollCmd) enroll(ctx context.Context, persistentConfig map[string]inte
c.options.FleetServer.PolicyID,
c.options.FleetServer.Host, c.options.FleetServer.Port, c.options.FleetServer.InternalPort,
c.options.FleetServer.Cert, c.options.FleetServer.CertKey, c.options.FleetServer.CertKeyPassphrasePath, c.options.FleetServer.ElasticsearchCA, c.options.FleetServer.ElasticsearchCASHA256,
c.options.CAs, c.options.FleetServer.ClientAuth,
c.options.FleetServer.ElasticsearchCert, c.options.FleetServer.ElasticsearchCertKey,
c.options.FleetServer.Headers,
c.options.ProxyURL, c.options.ProxyDisabled, c.options.ProxyHeaders,
c.options.FleetServer.ElasticsearchInsecure,
Expand Down Expand Up @@ -921,6 +936,8 @@ func createFleetServerBootstrapConfig(
connStr, serviceToken, serviceTokenPath, policyID, host string,
port uint16, internalPort uint16,
cert, key, passphrasePath, esCA, esCASHA256 string,
cas []string, clientAuth string,
esClientCert, esClientCertKey string,
headers map[string]string,
proxyURL string,
proxyDisabled bool,
Expand Down Expand Up @@ -951,6 +968,21 @@ func createFleetServerBootstrapConfig(
es.TLS.CATrustedFingerprint = esCASHA256
}
}
if esClientCert != "" || esClientCertKey != "" {
AndersonQ marked this conversation as resolved.
Show resolved Hide resolved
if es.TLS == nil {
es.TLS = &tlscommon.Config{
Certificate: tlscommon.CertificateConfig{
Certificate: esClientCert,
Key: esClientCertKey,
},
}
} else {
es.TLS.Certificate = tlscommon.CertificateConfig{
Certificate: esClientCert,
Key: esClientCertKey,
}
}
}
michel-laterman marked this conversation as resolved.
Show resolved Hide resolved
if host == "" {
host = defaultFleetServerHost
}
Expand Down Expand Up @@ -988,7 +1020,7 @@ func createFleetServerBootstrapConfig(
cfg.Server.Policy = &configuration.FleetServerPolicyConfig{ID: policyID}
}
if cert != "" || key != "" {
cfg.Server.TLS = &tlscommon.Config{
cfg.Server.TLS = &tlscommon.ServerConfig{
Certificate: tlscommon.CertificateConfig{
Certificate: cert,
Key: key,
Expand All @@ -1000,6 +1032,16 @@ func createFleetServerBootstrapConfig(
}
}

if cfg.Server.TLS != nil {
cfg.Server.TLS.CAs = cas
}

if cfg.Server.TLS != nil && clientAuth != "" {
if err := cfg.Server.TLS.ClientAuth.Unpack(clientAuth); err != nil {
return nil, errors.New(err, "failed to unpack --fleet-server-client-auth", errors.TypeConfig)
}
}

if localFleetServer {
cfg.Client.Transport.Proxy.Disable = true
cfg.Server.InternalPort = internalPort
Expand Down
29 changes: 27 additions & 2 deletions internal/pkg/agent/cmd/enroll_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"context"
"crypto/tls"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -406,6 +405,32 @@ func TestValidateArgs(t *testing.T) {
require.Contains(t, args, "--fleet-server-service-token-path")
require.Contains(t, args, "/path/to/token")
})

t.Run("fleet-es client certificates are passed", func(t *testing.T) {
cmd := newEnrollCommandWithArgs([]string{}, streams)
err := cmd.Flags().Set("fleet-server-es-cert", "/path/to/cert")
require.NoError(t, err)
err = cmd.Flags().Set("fleet-server-es-cert-key", "/path/to/key")
require.NoError(t, err)
args := buildEnrollmentFlags(cmd, url, enrolmentToken)
require.Contains(t, args, "--fleet-server-es-cert")
require.Contains(t, args, "/path/to/cert")
require.Contains(t, args, "--fleet-server-es-cert-key")
require.Contains(t, args, "/path/to/key")
})

t.Run("elastic-agent client certificates are passed", func(t *testing.T) {
cmd := newEnrollCommandWithArgs([]string{}, streams)
err := cmd.Flags().Set("elastic-agent-cert", "/path/to/cert")
require.NoError(t, err)
err = cmd.Flags().Set("elastic-agent-cert-key", "/path/to/key")
require.NoError(t, err)
args := buildEnrollmentFlags(cmd, url, enrolmentToken)
require.Contains(t, args, "--elastic-agent-cert")
require.Contains(t, args, "/path/to/cert")
require.Contains(t, args, "--elastic-agent-cert-key")
require.Contains(t, args, "/path/to/key")
})
}

func TestValidateEnrollFlags(t *testing.T) {
Expand Down Expand Up @@ -478,7 +503,7 @@ func withTLSServer(
}

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