Skip to content

Pass --header enrollment option to fleet-server #8071

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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,35 @@
# 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: enhancement

# Change summary; a 80ish characters long description of the change.
summary: Add --header to enrollment communication with Fleet Server
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
summary: Add --header to enrollment communication with Fleet Server
summary: Use --header from enrollment when communicating with Fleet Server

The current version doesn't quite read correctly as a sentence to me.


# 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: |
The --header option for the enrollment command now adds the headers to the communication with Fleet Server. This
allows a proxy that requires specific headers present for traffic to flow to be placed in front of a Fleet Server
to be used and still allowing the Elastic Agent to enroll.

# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
component: elastic-agent

# 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/elastic/elastic-agent/pull/8071

# 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/elastic/elastic-agent/issues/6823
3 changes: 2 additions & 1 deletion internal/pkg/agent/cmd/enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func addEnrollFlags(cmd *cobra.Command) {
cmd.Flags().StringP("fleet-server-cert-key", "", "", "Private key for the certificate used by Fleet Server for exposed HTTPS endpoint")
cmd.Flags().StringP("fleet-server-cert-key-passphrase", "", "", "Path for private key passphrase file used to decrypt Fleet Server certificate key")
cmd.Flags().StringP("fleet-server-client-auth", "", "none", "Fleet Server mTLS client authentication for connecting Elastic Agents. Must be one of [none, optional, required]")
cmd.Flags().StringSliceP("header", "", []string{}, "Headers used by Fleet Server when communicating with Elasticsearch")
cmd.Flags().StringSliceP("header", "", []string{}, "Headers used by Agent to communicate with Fleet Server, and when a bootstrapped Fleet Server communicates 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 certificates for server verification used by Elastic Agent and Fleet Server")
cmd.Flags().StringP("ca-sha256", "p", "", "Comma-separated list of certificate authority hash pins for server verification used by Elastic Agent and Fleet Server")
Expand Down Expand Up @@ -506,6 +506,7 @@ func enroll(streams *cli.IOStreams, cmd *cobra.Command) error {
UserProvidedMetadata: make(map[string]interface{}),
Staging: staging,
FixPermissions: fixPermissions,
Headers: mapFromEnvList(fHeaders),
ProxyURL: proxyURL,
ProxyDisabled: proxyDisabled,
ProxyHeaders: mapFromEnvList(proxyHeaders),
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/agent/cmd/enroll_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type enrollCmdOption struct {
ReplaceToken string `yaml:"replace_token,omitempty"`
EnrollAPIKey string `yaml:"enrollment_key,omitempty"`
Staging string `yaml:"staging,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
ProxyURL string `yaml:"proxy_url,omitempty"`
ProxyDisabled bool `yaml:"proxy_disabled,omitempty"`
ProxyHeaders map[string]string `yaml:"proxy_headers,omitempty"`
Expand All @@ -144,6 +145,7 @@ func (e *enrollCmdOption) remoteConfig() (remote.Config, error) {
if cfg.Protocol == remote.ProtocolHTTP && !e.Insecure {
return remote.Config{}, fmt.Errorf("connection to fleet-server is insecure, strongly recommended to use a secure connection (override with --insecure)")
}
cfg.Headers = e.Headers

var tlsCfg tlscommon.Config

Expand Down
78 changes: 74 additions & 4 deletions internal/pkg/agent/cmd/enroll_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ func (m *mockStore) Save(in io.Reader) error {

func TestEnroll(t *testing.T) {
testutils.InitStorage(t)
skipCreateSecret := false
if runtime.GOOS == "darwin" {
skipCreateSecret = true
}
skipCreateSecret := runtime.GOOS == "darwin"

log, _ := logger.New("tst", false)

Expand Down Expand Up @@ -424,6 +421,79 @@ func TestEnroll(t *testing.T) {
assert.Equal(t, host, config.Client.Host)
},
))

t.Run("headers are sent to server", withServer(
func(t *testing.T) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/api/fleet/agents/enroll", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Test-Header") != "Test-Value" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This proves we use a header for /enroll, is this enough to also prove the same header would be included in a checkin?

I see in enroll.go we explicitly add the header to enrollCmdOption so it isn't obvious just by reading it. Regardless, would there be a way to test that?

w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`
{
"statusCode": 500,
"error": "Missing required X-Test-Header header"
}`))
return
}
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"
}
}`))
})
return mux
}, func(t *testing.T, host string) {
url := "http://" + host
store := &mockStore{}
cmd, err := newEnrollCmd(
log,
&enrollCmdOption{
URL: url,
CAs: []string{},
EnrollAPIKey: "my-enrollment-api-key",
Insecure: true,
UserProvidedMetadata: map[string]interface{}{"custom": "customize"},
SkipCreateSecret: skipCreateSecret,
SkipDaemonRestart: true,
Headers: map[string]string{
"X-Test-Header": "Test-Value",
},
},
"",
store,
nil,
)
require.NoError(t, err)

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 should return no error")

assert.True(t, store.Called, "the store should have been called")
config, err := readConfig(store.Content)
require.NoError(t, err)
assert.Equal(t, "my-access-api-key", config.AccessAPIKey)
assert.Equal(t, host, config.Client.Host)
},
))
}

func TestValidateArgs(t *testing.T) {
Expand Down
16 changes: 16 additions & 0 deletions internal/pkg/remote/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ func NewWithConfig(log *logger.Logger, cfg Config, wrapper wrapperFunc) (*Client
return nil, err
}

if cfg.Headers != nil {
transport = &headersRoundTripper{rt: transport, headers: cfg.Headers}
}

if wrapper != nil {
transport, err = wrapper(transport)
if err != nil {
Expand Down Expand Up @@ -345,3 +349,15 @@ func (r requestClient) newRequest(method string, path string, params url.Values,

return http.NewRequestWithContext(context.TODO(), method, newPath, body)
}

type headersRoundTripper struct {
rt http.RoundTripper
headers map[string]string
}

func (r *headersRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
for key, value := range r.headers {
req.Header.Set(key, value)
}
return r.rt.RoundTrip(req)
}
11 changes: 6 additions & 5 deletions internal/pkg/remote/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ import (

// Config is the configuration for the client.
type Config struct {
Protocol Protocol `config:"protocol" yaml:"protocol,omitempty"`
SpaceID string `config:"space.id" yaml:"space.id,omitempty"`
Path string `config:"path" yaml:"path,omitempty"`
Host string `config:"host" yaml:"host,omitempty"`
Hosts []string `config:"hosts" yaml:"hosts,omitempty"`
Protocol Protocol `config:"protocol" yaml:"protocol,omitempty"`
SpaceID string `config:"space.id" yaml:"space.id,omitempty"`
Path string `config:"path" yaml:"path,omitempty"`
Host string `config:"host" yaml:"host,omitempty"`
Hosts []string `config:"hosts" yaml:"hosts,omitempty"`
Headers map[string]string `config:"headers" yaml:"headers,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the headers are secrets, is this going to leak them in diagnostics? Are they either already redacted or not included in diagnostics at all?


Transport httpcommon.HTTPTransportSettings `config:",inline" yaml:",inline"`
}
Expand Down