Skip to content

Commit e984ed2

Browse files
Send fleet-server elasticsearch config under new bootstrap attribute (#4643)
Alter the fleet-server bootstrap component modifier to insert all (elasticsearch) output configuration options specified by enrollment args under a new elasticsearch.boostrap key instead of overwriting any existing keys. This will allow elastic-agent to send the list of hosts (and other config options) retrieved from a policy to fleet server as well as the config needed to form the initial connection to elasticsearch used to collect policy information. Fleet-server has been altered to use the bootstrap config that is passed if the policy attributes are unspecified or fail.
1 parent 5754a6a commit e984ed2

9 files changed

+461
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Kind can be one of:
2+
# - breaking-change: a change to previously-documented behavior
3+
# - deprecation: functionality that is being removed in a later release
4+
# - bug-fix: fixes a problem in a previous version
5+
# - enhancement: extends functionality but does not break or fix existing behavior
6+
# - feature: new functionality
7+
# - known-issue: problems that we are aware of in a given version
8+
# - security: impacts on the security of a product or a user’s deployment.
9+
# - upgrade: important information for someone upgrading from a prior version
10+
# - other: does not fit into any of the other categories
11+
kind: enhancement
12+
13+
# Change summary; a 80ish characters long description of the change.
14+
summary: Fleet Server component now uses policy output configuration to communicate with Elasticsearch
15+
16+
# Long description; in case the summary is not enough to describe the change
17+
# this field accommodate a description without length limits.
18+
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
19+
description: |
20+
Alter how elatic-agent passes the fleet-server output component so that the policy's output is used.
21+
In cases where fleet-server encounters an error when trying to use the policy's output it will use
22+
the configuration specified during enrollment as a fallback. In cases where it uses the fallback
23+
the policy's output is periodically retested and used if it's successful.
24+
25+
# Affected component; a word indicating the component this changeset affects.
26+
component:
27+
28+
# PR URL; optional; the PR number that added the changeset.
29+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
30+
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
31+
# Please provide it if you are adding a fragment for a different PR.
32+
pr: https://github.com/elastic/elastic-agent/pull/4643
33+
34+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
35+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
36+
issue: https://github.com/elastic/elastic-agent/issue/2784

docs/fleet-server-bootstrap.asciidoc

+58
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,61 @@ its API key to use for communication. The new `fleet.yml` still includes the `fl
8888
but this time the `fleet.server.bootstrap: false` is set.
8989
. `enroll` command then either restarts the running Elatic Agent daemon if one was running
9090
from Step 2, or it stops the spawned `run` subprocess and returns.
91+
92+
=== Elasticsearch output
93+
94+
The options passed that are used to specify fleet-server initially connects to elasticsearch are:
95+
96+
- `--fleet-server-es`
97+
- `--fleet-server-es-ca`
98+
- `--fleet-server-es-ca-trusted-fingerprint`
99+
- `--fleet-server-es-insecure`
100+
- `--fleet-server-es-cert`
101+
- `--fleet-server-es-cert-key`
102+
- `--fleet-server-es-service-token`
103+
- `--fleet-server-es-service-token-path`
104+
- `--proxy-url`
105+
- `--proxy-disabled`
106+
- `--proxy-header`
107+
108+
These options are always passed under a `bootstrap` attribute in the output when elastic-agent is passing config to fleet-server.
109+
When the fleet-server recieves an output block, it will inject any keys that are missing from the top level output but are specified in the `bootstrap` block
110+
After injecting the keys from bootstrap, fleet-server will test connecting the Elasticsearch with the output.
111+
If the test fails, the values under the `bootstrap` attribute are used as the output and fleet-server will periodically retest the output in case the error was caused by a temporary network issue.
112+
Note that if `--fleet-server-es-insecure` is specified, and the output in the policy contains one or more CA, or a CA fingerprint, the `--fleet-server-es-insecure` flag is ignored.
113+
114+
An example of this sequence is sequence is:
115+
116+
1) elastic-agent starts fleet-server and sends an output block that looks similar to:
117+
```yaml
118+
output:
119+
bootstrap:
120+
service_token: VALUE
121+
hosts: ["HOST"]
122+
```
123+
124+
2) fleet-server injects attributes into the top level from bootstrap if they are missing, resulting in
125+
```yaml
126+
output:
127+
service_token: VALUE
128+
hosts: ["HOST"]
129+
```
130+
131+
3) fleet-server connects to Elasticsearch with the output block
132+
4) elastic-agent enrolls and recieves its policy
133+
5) elastic-agent sends configuration generated from the policy to fleet-server, this may result in the output as follows:
134+
```yaml
135+
output:
136+
hosts: ["HOST", "HOST2"]
137+
bootstrap:
138+
service_token: VALUE
139+
hosts: ["HOST"]
140+
```
141+
142+
6) fleet-server will inject missing values resulting in:
143+
```yaml
144+
output:
145+
service_token: VALUE
146+
hosts: ["HOST", "HOST2"]
147+
```
148+
7) fleet-server tests and uses the resulting output block.

internal/pkg/agent/application/fleet_server_bootstrap.go

+17-1
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ func FleetServerComponentModifier(serverCfg *configuration.FleetServerConfig) co
6565
} else {
6666
for j, unit := range comp.Units {
6767
if unit.Type == client.UnitTypeOutput && unit.Config.Type == elasticsearch {
68-
unitCfgMap, err := toMapStr(unit.Config.Source.AsMap(), &serverCfg.Output.Elasticsearch)
68+
unitCfgMap, err := toMapStr(unit.Config.Source.AsMap())
6969
if err != nil {
7070
return nil, err
7171
}
72+
if err := addBootstrapCfg(unitCfgMap, &serverCfg.Output.Elasticsearch); err != nil {
73+
return nil, err
74+
}
7275
fixOutputMap(unitCfgMap)
7376
unitCfg, err := component.ExpectedConfig(unitCfgMap)
7477
if err != nil {
@@ -100,6 +103,19 @@ func FleetServerComponentModifier(serverCfg *configuration.FleetServerConfig) co
100103
}
101104
}
102105

106+
// addBootrapCfg will transform the passed configuration.Elasticsearch to a map and add it to dst under the bootstrap key.
107+
func addBootstrapCfg(dst map[string]interface{}, es *configuration.Elasticsearch) error {
108+
if es == nil {
109+
return fmt.Errorf("fleet-server bootstrap output config is undefined")
110+
}
111+
mp, err := toMapStr(es)
112+
if err != nil {
113+
return err
114+
}
115+
dst["bootstrap"] = mp
116+
return nil
117+
}
118+
103119
// InjectFleetConfigComponentModifier The modifier that injects the fleet configuration for the components
104120
// that need to be able to connect to fleet server.
105121
func InjectFleetConfigComponentModifier(fleetCfg *configuration.FleetAgentConfig, agentInfo info.Agent) coordinator.ComponentsModifier {

internal/pkg/agent/application/fleet_server_bootstrap_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,87 @@ func TestFleetServerComponentModifier_NoServerConfig(t *testing.T) {
7474
}
7575
}
7676

77+
func TestFleetServerComponentModifier(t *testing.T) {
78+
tests := []struct {
79+
name string
80+
source map[string]interface{}
81+
expect map[string]interface{}
82+
}{{
83+
name: "empty output component",
84+
source: map[string]interface{}{},
85+
expect: map[string]interface{}{
86+
"bootstrap": map[string]interface{}{
87+
"protocol": "https",
88+
"hosts": []interface{}{"elasticsearch:9200"},
89+
"service_token": "example-token",
90+
},
91+
},
92+
}, {
93+
name: "output component provided",
94+
source: map[string]interface{}{
95+
"protocol": "http",
96+
"hosts": []interface{}{"elasticsearch:9200", "host:9200"},
97+
},
98+
expect: map[string]interface{}{
99+
"protocol": "http",
100+
"hosts": []interface{}{"elasticsearch:9200", "host:9200"},
101+
"bootstrap": map[string]interface{}{
102+
"protocol": "https",
103+
"hosts": []interface{}{"elasticsearch:9200"},
104+
"service_token": "example-token",
105+
},
106+
},
107+
}}
108+
cfg := &configuration.FleetServerConfig{
109+
Output: configuration.FleetServerOutputConfig{
110+
Elasticsearch: configuration.Elasticsearch{
111+
Protocol: "https",
112+
Hosts: []string{"elasticsearch:9200"},
113+
ServiceToken: "example-token",
114+
},
115+
},
116+
}
117+
modifier := FleetServerComponentModifier(cfg)
118+
119+
for _, tc := range tests {
120+
t.Run(tc.name, func(t *testing.T) {
121+
src, err := structpb.NewStruct(tc.source)
122+
require.NoError(t, err)
123+
comps, err := modifier([]component.Component{{
124+
InputSpec: &component.InputRuntimeSpec{
125+
InputType: "fleet-server",
126+
},
127+
Units: []component.Unit{{
128+
Type: client.UnitTypeOutput,
129+
Config: &proto.UnitExpectedConfig{
130+
Type: "elasticsearch",
131+
Source: src,
132+
},
133+
}},
134+
}}, nil)
135+
require.NoError(t, err)
136+
137+
require.Len(t, comps, 1)
138+
require.Len(t, comps[0].Units, 1)
139+
res := comps[0].Units[0].Config.Source.AsMap()
140+
for k, v := range tc.expect {
141+
val, ok := res[k]
142+
require.Truef(t, ok, "expected %q to be in output unit config", k)
143+
if mp, ok := v.(map[string]interface{}); ok {
144+
rMap, ok := val.(map[string]interface{})
145+
require.Truef(t, ok, "expected %q to be map[string]interface{} was %T", k, val)
146+
for kk, vv := range mp {
147+
assert.Contains(t, rMap, kk)
148+
assert.Equal(t, rMap[kk], vv)
149+
}
150+
} else {
151+
assert.Equal(t, v, val)
152+
}
153+
}
154+
})
155+
}
156+
}
157+
77158
func TestInjectFleetConfigComponentModifier(t *testing.T) {
78159
fleetConfig := &configuration.FleetAgentConfig{
79160
Enabled: true,

internal/pkg/agent/configuration/fleet_server.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ type FleetServerOutputConfig struct {
3232
Elasticsearch Elasticsearch `config:"elasticsearch" yaml:"elasticsearch"`
3333
}
3434

35-
// Elasticsearch is the configuration for elasticsearch.
35+
// Elasticsearch is the configuration for fleet-server's connection to elasticsearch.
36+
// Note that these keys may be injected into policy output by fleet-server.
37+
// The following TLS options may be set in bootstrap:
38+
// - VerificationMode
39+
// - CAs
40+
// - CATrustedFingerprint
41+
// - CertificateConfig.Certificate AND CertificateConfig.Key
42+
// If an attribute is added to this struct, or another TLS attribute is passed ensure that it is handled as part of the bootstrap config handler in fleet-server/internal/pkg/server/agent.go
3643
type Elasticsearch struct {
3744
Protocol string `config:"protocol" yaml:"protocol"`
3845
Hosts []string `config:"hosts" yaml:"hosts"`

pkg/testing/fixture_install.go

+26
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ func (e EnrollOpts) toCmdArgs() []string {
5656
return args
5757
}
5858

59+
type FleetBootstrapOpts struct {
60+
ESHost string // --fleet-server-es
61+
ServiceToken string // --fleet-server-service-token
62+
Policy string // --fleet-server-policy
63+
Port int // --fleet-server-port
64+
}
65+
66+
func (f FleetBootstrapOpts) toCmdArgs() []string {
67+
var args []string
68+
if f.ESHost != "" {
69+
args = append(args, "--fleet-server-es", f.ESHost)
70+
}
71+
if f.ServiceToken != "" {
72+
args = append(args, "--fleet-server-service-token", f.ServiceToken)
73+
}
74+
if f.Policy != "" {
75+
args = append(args, "--fleet-server-policy", f.Policy)
76+
}
77+
if f.Port > 0 {
78+
args = append(args, "--fleet-server-port", fmt.Sprintf("%d", f.Port))
79+
}
80+
return args
81+
}
82+
5983
// InstallOpts specifies the options for the install command
6084
type InstallOpts struct {
6185
BasePath string // --base-path
@@ -68,6 +92,7 @@ type InstallOpts struct {
6892
Privileged bool // inverse of --unprivileged (as false is the default)
6993

7094
EnrollOpts
95+
FleetBootstrapOpts
7196
}
7297

7398
func (i InstallOpts) toCmdArgs(operatingSystem string) ([]string, error) {
@@ -95,6 +120,7 @@ func (i InstallOpts) toCmdArgs(operatingSystem string) ([]string, error) {
95120
}
96121

97122
args = append(args, i.EnrollOpts.toCmdArgs()...)
123+
args = append(args, i.FleetBootstrapOpts.toCmdArgs()...)
98124

99125
return args, nil
100126
}

pkg/testing/tools/estools/elasticsearch.go

+31
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
"github.com/google/uuid"
17+
1618
"github.com/elastic/elastic-agent-libs/mapstr"
1719
"github.com/elastic/elastic-transport-go/v8/elastictransport"
1820
"github.com/elastic/go-elasticsearch/v8/esapi"
@@ -201,6 +203,35 @@ func CreateAPIKey(ctx context.Context, client elastictransport.Interface, req AP
201203
return parsed, nil
202204
}
203205

206+
func CreateServiceToken(ctx context.Context, client elastictransport.Interface, service string) (string, error) {
207+
req := esapi.SecurityCreateServiceTokenRequest{
208+
Namespace: "elastic",
209+
Service: service,
210+
Name: uuid.New().String(), // FIXME(michel-laterman): We need to specify a random name until an upstream issue is fixed: https://github.com/elastic/go-elasticsearch/issues/861
211+
}
212+
resp, err := req.Do(ctx, client)
213+
if err != nil {
214+
return "", fmt.Errorf("error creating service token: %w", err)
215+
}
216+
defer resp.Body.Close()
217+
resultBuf, err := handleResponseRaw(resp)
218+
if err != nil {
219+
return "", fmt.Errorf("error handling HTTP response: %w", err)
220+
}
221+
222+
var parsed struct {
223+
Token struct {
224+
Value string `json:"value"`
225+
} `json:"token"`
226+
}
227+
err = json.Unmarshal(resultBuf, &parsed)
228+
if err != nil {
229+
return "", fmt.Errorf("error unmarshaling json response: %w", err)
230+
}
231+
return parsed.Token.Value, nil
232+
233+
}
234+
204235
// FindMatchingLogLines returns any logs with message fields that match the given line
205236
func FindMatchingLogLines(ctx context.Context, client elastictransport.Interface, namespace, line string) (Documents, error) {
206237
return FindMatchingLogLinesWithContext(ctx, client, namespace, line)

testing/integration/fleet-server.json

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"id": "3434b864-d135-4d03-a944-29ee7ad61ddd",
3+
"version": "WzMwNywxXQ==",
4+
"name": "fleet_server-1",
5+
"namespace": "",
6+
"description": "",
7+
"package": {
8+
"name": "fleet_server",
9+
"title": "Fleet Server",
10+
"version": "1.5.0"
11+
},
12+
"enabled": true,
13+
"inputs": [
14+
{
15+
"type": "fleet-server",
16+
"policy_template": "fleet_server",
17+
"enabled": true,
18+
"streams": [],
19+
"vars": {
20+
"max_agents": {
21+
"type": "integer"
22+
},
23+
"max_connections": {
24+
"type": "integer"
25+
},
26+
"custom": {
27+
"value": "",
28+
"type": "yaml"
29+
}
30+
}
31+
}
32+
],
33+
"revision": 1,
34+
"created_at": "2024-05-22T16:13:09.177Z",
35+
"created_by": "system",
36+
"updated_at": "2024-05-22T16:13:09.177Z",
37+
"updated_by": "system"
38+
}

0 commit comments

Comments
 (0)