From e2eb13a4dbc96fbfcd3bde6aeb5bebe7acdda654 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 2 Dec 2025 14:41:48 +0100 Subject: [PATCH 01/16] Allow agent to override fleet settings --- .../application/coordinator/coordinator.go | 33 +++++++++++++ .../coordinator/coordinator_test.go | 47 +++++++++++++++++++ .../coordinator/testdata/overrides.yml | 6 +++ internal/pkg/capabilities/capabilities.go | 22 ++++++--- internal/pkg/capabilities/fleet_override.go | 37 +++++++++++++++ internal/pkg/capabilities/spec.go | 16 +++++-- 6 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 internal/pkg/agent/application/coordinator/testdata/overrides.yml create mode 100644 internal/pkg/capabilities/fleet_override.go diff --git a/internal/pkg/agent/application/coordinator/coordinator.go b/internal/pkg/agent/application/coordinator/coordinator.go index d327c162b91..6e8c6fc710c 100644 --- a/internal/pkg/agent/application/coordinator/coordinator.go +++ b/internal/pkg/agent/application/coordinator/coordinator.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "os" "reflect" "strings" "sync" @@ -1609,6 +1610,12 @@ func (c *Coordinator) processConfigAgent(ctx context.Context, cfg *config.Config c.logger.Errorf("failed to add secret markers: %v", err) } + // override retrieved config from Fleet with persisted config from AgentConfig file + + if err := applyPersistedConfig(cfg, paths.ConfigFile(), c.caps.AllowFleetOverride); err != nil { + return fmt.Errorf("could not apply persisted configuration: %w", err) + } + // perform and verify ast translation m, err := cfg.ToMapStr() if err != nil { @@ -1696,6 +1703,32 @@ func (c *Coordinator) generateAST(cfg *config.Config, m map[string]interface{}) return nil } +func applyPersistedConfig(cfg *config.Config, configFile string, checkFn func() bool) error { + if !checkFn() { + // Feature is disabled, nothing to do + return nil + } + + f, err := os.OpenFile(configFile, os.O_RDONLY, 0) + if err != nil && os.IsNotExist(err) { + return nil + } else if err != nil { + return fmt.Errorf("opening config file: %w", err) + } + defer f.Close() + + persisted, err := config.NewConfigFrom(f) + if err != nil { + return fmt.Errorf("parsing persisted config: %w", err) + } + + err = cfg.Merge(persisted) + if err != nil { + return fmt.Errorf("merging persisted config: %w", err) + } + return nil +} + // observeASTVars identifies the variables that are referenced in the computed AST and passed to // the varsMgr so it knows what providers are being referenced. If a providers is not being // referenced then the provider does not need to be running. diff --git a/internal/pkg/agent/application/coordinator/coordinator_test.go b/internal/pkg/agent/application/coordinator/coordinator_test.go index 54ca803ef85..943e16d13bd 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_test.go @@ -1011,6 +1011,53 @@ func TestCoordinator_UpgradeDetails(t *testing.T) { require.Equal(t, expectedErr.Error(), coord.state.UpgradeDetails.Metadata.ErrorMsg) } +func Test_ApplyPersistedConfig(t *testing.T) { + cfgFile := filepath.Join(".", "testdata", "overrides.yml") + + testCases := []struct { + name string + featureEnable bool + expectedLogs bool + }{ + {name: "enabled", featureEnable: true, expectedLogs: false}, + {name: "disabled", featureEnable: false, expectedLogs: true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cfg, err := config.LoadFile(filepath.Join(".", "testdata", "config.yaml")) + require.NoError(t, err) + + err = applyPersistedConfig(cfg, cfgFile, func() bool { return tc.featureEnable }) + require.NoError(t, err) + + c := &configuration.Configuration{} + require.NoError(t, cfg.Agent.Unpack(&c)) + + require.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs) + require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) + require.True(t, c.Settings.MonitoringConfig.Enabled) + }) + } +} + +func Test_ApplyPersistedConfig_FeatureDisabled(t *testing.T) { + cfgFile := filepath.Join(".", "testdata", "overrides.yml") + + cfg, err := config.LoadFile(filepath.Join(".", "testdata", "config.yaml")) + require.NoError(t, err) + + err = applyPersistedConfig(cfg, cfgFile, func() bool { return false }) + require.NoError(t, err) + + c := &configuration.Configuration{} + require.NoError(t, cfg.Agent.Unpack(&c)) + + require.True(t, c.Settings.MonitoringConfig.MonitorLogs) + require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) + require.True(t, c.Settings.MonitoringConfig.Enabled) +} + func BenchmarkCoordinator_generateComponentModel(b *testing.B) { // load variables varsMaps := []map[string]any{} diff --git a/internal/pkg/agent/application/coordinator/testdata/overrides.yml b/internal/pkg/agent/application/coordinator/testdata/overrides.yml new file mode 100644 index 00000000000..d15c027615d --- /dev/null +++ b/internal/pkg/agent/application/coordinator/testdata/overrides.yml @@ -0,0 +1,6 @@ +agent: + monitoring: + enabled: true + use_output: default + logs: false + metrics: true \ No newline at end of file diff --git a/internal/pkg/capabilities/capabilities.go b/internal/pkg/capabilities/capabilities.go index e4355cc8882..25e3b043bae 100644 --- a/internal/pkg/capabilities/capabilities.go +++ b/internal/pkg/capabilities/capabilities.go @@ -19,13 +19,15 @@ type Capabilities interface { AllowUpgrade(version string, sourceURI string) bool AllowInput(name string) bool AllowOutput(name string) bool + AllowFleetOverride() bool } type capabilitiesManager struct { - log *logger.Logger - inputChecks []*stringMatcher - outputChecks []*stringMatcher - upgradeCaps []*upgradeCapability + log *logger.Logger + inputChecks []*stringMatcher + outputChecks []*stringMatcher + upgradeCaps []*upgradeCapability + fleetOverrideCaps []*fleetOverrideCapability } func (cm *capabilitiesManager) AllowInput(inputType string) bool { @@ -40,6 +42,10 @@ func (cm *capabilitiesManager) AllowUpgrade(version string, uri string) bool { return allowUpgrade(cm.log, version, uri, cm.upgradeCaps) } +func (cm *capabilitiesManager) AllowFleetOverride() bool { + return allowFleetOverride(cm.log, cm.fleetOverrideCaps) +} + func LoadFile(capsFile string, log *logger.Logger) (Capabilities, error) { // load capabilities from file fd, err := os.Open(capsFile) @@ -68,8 +74,10 @@ func Load(capsReader io.Reader, log *logger.Logger) (Capabilities, error) { caps := spec.Capabilities return &capabilitiesManager{ - inputChecks: caps.inputChecks, - outputChecks: caps.outputChecks, - upgradeCaps: caps.upgradeChecks, + log: log, + inputChecks: caps.inputChecks, + outputChecks: caps.outputChecks, + upgradeCaps: caps.upgradeChecks, + fleetOverrideCaps: caps.fleetOverrideChecks, }, nil } diff --git a/internal/pkg/capabilities/fleet_override.go b/internal/pkg/capabilities/fleet_override.go new file mode 100644 index 00000000000..827dd8ff1af --- /dev/null +++ b/internal/pkg/capabilities/fleet_override.go @@ -0,0 +1,37 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package capabilities + +import "github.com/elastic/elastic-agent/pkg/core/logger" + +type fleetOverrideCapability struct { + // Whether a successful condition check lets an upgrade proceed or blocks it + rule allowOrDeny +} + +func newFleetOverrideCapability(rule allowOrDeny) *fleetOverrideCapability { + return &fleetOverrideCapability{ + rule: rule, + } +} + +func allowFleetOverride( + log *logger.Logger, + fleetOverrideCaps []*fleetOverrideCapability, +) bool { + // first match wins + for _, cap := range fleetOverrideCaps { + switch cap.rule { + case ruleTypeAllow: + log.Debugf("Fleet override allowed by capability") + return true + case ruleTypeDeny: + log.Debugf("Fleet override denied by capability") + return false + } + } + // No matching capability found, disable by default + return false +} diff --git a/internal/pkg/capabilities/spec.go b/internal/pkg/capabilities/spec.go index 7aeca84ec48..63ce363a3e6 100644 --- a/internal/pkg/capabilities/spec.go +++ b/internal/pkg/capabilities/spec.go @@ -13,9 +13,10 @@ import ( // capabilitiesList deserializes a YAML list of capabilities into organized // arrays based on their type, for easy use by capabilitiesManager. type capabilitiesList struct { - inputChecks []*stringMatcher - outputChecks []*stringMatcher - upgradeChecks []*upgradeCapability + inputChecks []*stringMatcher + outputChecks []*stringMatcher + upgradeChecks []*upgradeCapability + fleetOverrideChecks []*fleetOverrideCapability } // a type for capability values that must equal "allow" or "deny", enforced @@ -81,6 +82,15 @@ func (r *capabilitiesList) UnmarshalYAML(unmarshal func(interface{}) error) erro return err } r.upgradeChecks = append(r.upgradeChecks, cap) + } else if _, found = mm["fleet_override"]; found { + spec := struct { + Type allowOrDeny `yaml:"rule"` + }{} + if err := yaml.Unmarshal(partialYaml, &spec); err != nil { + return err + } + cap := newFleetOverrideCapability(spec.Type) + r.fleetOverrideChecks = append(r.fleetOverrideChecks, cap) } else { return fmt.Errorf("unexpected capability type for definition number '%d'", i) } From df77bf70f198fdc267b355baced5aa639bd0824f Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 2 Dec 2025 14:47:21 +0100 Subject: [PATCH 02/16] Remove duplicate test --- .../application/coordinator/coordinator_test.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/internal/pkg/agent/application/coordinator/coordinator_test.go b/internal/pkg/agent/application/coordinator/coordinator_test.go index 943e16d13bd..26764de92c3 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_test.go @@ -1041,23 +1041,6 @@ func Test_ApplyPersistedConfig(t *testing.T) { } } -func Test_ApplyPersistedConfig_FeatureDisabled(t *testing.T) { - cfgFile := filepath.Join(".", "testdata", "overrides.yml") - - cfg, err := config.LoadFile(filepath.Join(".", "testdata", "config.yaml")) - require.NoError(t, err) - - err = applyPersistedConfig(cfg, cfgFile, func() bool { return false }) - require.NoError(t, err) - - c := &configuration.Configuration{} - require.NoError(t, cfg.Agent.Unpack(&c)) - - require.True(t, c.Settings.MonitoringConfig.MonitorLogs) - require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) - require.True(t, c.Settings.MonitoringConfig.Enabled) -} - func BenchmarkCoordinator_generateComponentModel(b *testing.B) { // load variables varsMaps := []map[string]any{} From 4ae51b307d0f49f8469b86b0e9a412d818cffba1 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 2 Dec 2025 15:43:08 +0100 Subject: [PATCH 03/16] nil check --- internal/pkg/agent/application/coordinator/coordinator.go | 6 ++++-- .../agent/application/coordinator/testdata/overrides.yml | 4 +++- internal/pkg/capabilities/fleet_override.go | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/application/coordinator/coordinator.go b/internal/pkg/agent/application/coordinator/coordinator.go index 6e8c6fc710c..eca3ea09af2 100644 --- a/internal/pkg/agent/application/coordinator/coordinator.go +++ b/internal/pkg/agent/application/coordinator/coordinator.go @@ -1612,8 +1612,10 @@ func (c *Coordinator) processConfigAgent(ctx context.Context, cfg *config.Config // override retrieved config from Fleet with persisted config from AgentConfig file - if err := applyPersistedConfig(cfg, paths.ConfigFile(), c.caps.AllowFleetOverride); err != nil { - return fmt.Errorf("could not apply persisted configuration: %w", err) + if c.caps != nil { + if err := applyPersistedConfig(cfg, paths.ConfigFile(), c.caps.AllowFleetOverride); err != nil { + return fmt.Errorf("could not apply persisted configuration: %w", err) + } } // perform and verify ast translation diff --git a/internal/pkg/agent/application/coordinator/testdata/overrides.yml b/internal/pkg/agent/application/coordinator/testdata/overrides.yml index d15c027615d..69cadda00f4 100644 --- a/internal/pkg/agent/application/coordinator/testdata/overrides.yml +++ b/internal/pkg/agent/application/coordinator/testdata/overrides.yml @@ -3,4 +3,6 @@ agent: enabled: true use_output: default logs: false - metrics: true \ No newline at end of file + metrics: true + http: + port: 6774 \ No newline at end of file diff --git a/internal/pkg/capabilities/fleet_override.go b/internal/pkg/capabilities/fleet_override.go index 827dd8ff1af..9af3ac1df59 100644 --- a/internal/pkg/capabilities/fleet_override.go +++ b/internal/pkg/capabilities/fleet_override.go @@ -23,6 +23,11 @@ func allowFleetOverride( ) bool { // first match wins for _, cap := range fleetOverrideCaps { + if cap == nil { + // being defensive here, should not happen + continue + } + switch cap.rule { case ruleTypeAllow: log.Debugf("Fleet override allowed by capability") From 1929087a10126eae2882b62a9bd67f58bc084aeb Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 3 Dec 2025 16:16:40 +0100 Subject: [PATCH 04/16] moved implementation to custom patcher --- internal/pkg/agent/application/application.go | 13 ++- .../application/coordinator/coordinator.go | 35 -------- .../coordinator/coordinator_test.go | 30 ------- .../agent/application/fleet_config_patcher.go | 28 ++++++ .../application/fleet_config_patcher_test.go | 86 +++++++++++++++++++ 5 files changed, 125 insertions(+), 67 deletions(-) create mode 100644 internal/pkg/agent/application/fleet_config_patcher.go create mode 100644 internal/pkg/agent/application/fleet_config_patcher_test.go diff --git a/internal/pkg/agent/application/application.go b/internal/pkg/agent/application/application.go index c141815eef8..b663ad06575 100644 --- a/internal/pkg/agent/application/application.go +++ b/internal/pkg/agent/application/application.go @@ -209,7 +209,11 @@ func New( log.Info("Parsed configuration and determined agent is in Fleet Server bootstrap mode") compModifiers = append(compModifiers, FleetServerComponentModifier(cfg.Fleet.Server)) - configMgr = coordinator.NewConfigPatchManager(newFleetServerBootstrapManager(log), PatchAPMConfig(log, rawConfig)) + configMgr = coordinator.NewConfigPatchManager( + newFleetServerBootstrapManager(log), + PatchAPMConfig(log, rawConfig), + PatchFleetConfig(log, rawConfig, caps, isManaged), + ) } else { log.Info("Parsed configuration and determined agent is managed by Fleet") @@ -259,7 +263,12 @@ func New( if err != nil { return nil, nil, nil, err } - configMgr = coordinator.NewConfigPatchManager(managed, injectOutputOverrides(log, rawConfig), PatchAPMConfig(log, rawConfig)) + configMgr = coordinator.NewConfigPatchManager( + managed, + injectOutputOverrides(log, rawConfig), + PatchAPMConfig(log, rawConfig), + PatchFleetConfig(log, rawConfig, caps, isManaged), + ) } } diff --git a/internal/pkg/agent/application/coordinator/coordinator.go b/internal/pkg/agent/application/coordinator/coordinator.go index eca3ea09af2..d327c162b91 100644 --- a/internal/pkg/agent/application/coordinator/coordinator.go +++ b/internal/pkg/agent/application/coordinator/coordinator.go @@ -8,7 +8,6 @@ import ( "context" "errors" "fmt" - "os" "reflect" "strings" "sync" @@ -1610,14 +1609,6 @@ func (c *Coordinator) processConfigAgent(ctx context.Context, cfg *config.Config c.logger.Errorf("failed to add secret markers: %v", err) } - // override retrieved config from Fleet with persisted config from AgentConfig file - - if c.caps != nil { - if err := applyPersistedConfig(cfg, paths.ConfigFile(), c.caps.AllowFleetOverride); err != nil { - return fmt.Errorf("could not apply persisted configuration: %w", err) - } - } - // perform and verify ast translation m, err := cfg.ToMapStr() if err != nil { @@ -1705,32 +1696,6 @@ func (c *Coordinator) generateAST(cfg *config.Config, m map[string]interface{}) return nil } -func applyPersistedConfig(cfg *config.Config, configFile string, checkFn func() bool) error { - if !checkFn() { - // Feature is disabled, nothing to do - return nil - } - - f, err := os.OpenFile(configFile, os.O_RDONLY, 0) - if err != nil && os.IsNotExist(err) { - return nil - } else if err != nil { - return fmt.Errorf("opening config file: %w", err) - } - defer f.Close() - - persisted, err := config.NewConfigFrom(f) - if err != nil { - return fmt.Errorf("parsing persisted config: %w", err) - } - - err = cfg.Merge(persisted) - if err != nil { - return fmt.Errorf("merging persisted config: %w", err) - } - return nil -} - // observeASTVars identifies the variables that are referenced in the computed AST and passed to // the varsMgr so it knows what providers are being referenced. If a providers is not being // referenced then the provider does not need to be running. diff --git a/internal/pkg/agent/application/coordinator/coordinator_test.go b/internal/pkg/agent/application/coordinator/coordinator_test.go index 26764de92c3..54ca803ef85 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_test.go @@ -1011,36 +1011,6 @@ func TestCoordinator_UpgradeDetails(t *testing.T) { require.Equal(t, expectedErr.Error(), coord.state.UpgradeDetails.Metadata.ErrorMsg) } -func Test_ApplyPersistedConfig(t *testing.T) { - cfgFile := filepath.Join(".", "testdata", "overrides.yml") - - testCases := []struct { - name string - featureEnable bool - expectedLogs bool - }{ - {name: "enabled", featureEnable: true, expectedLogs: false}, - {name: "disabled", featureEnable: false, expectedLogs: true}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - cfg, err := config.LoadFile(filepath.Join(".", "testdata", "config.yaml")) - require.NoError(t, err) - - err = applyPersistedConfig(cfg, cfgFile, func() bool { return tc.featureEnable }) - require.NoError(t, err) - - c := &configuration.Configuration{} - require.NoError(t, cfg.Agent.Unpack(&c)) - - require.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs) - require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) - require.True(t, c.Settings.MonitoringConfig.Enabled) - }) - } -} - func BenchmarkCoordinator_generateComponentModel(b *testing.B) { // load variables varsMaps := []map[string]any{} diff --git a/internal/pkg/agent/application/fleet_config_patcher.go b/internal/pkg/agent/application/fleet_config_patcher.go new file mode 100644 index 00000000000..e277e200bfa --- /dev/null +++ b/internal/pkg/agent/application/fleet_config_patcher.go @@ -0,0 +1,28 @@ +package application + +import ( + "github.com/elastic/elastic-agent/internal/pkg/agent/application/coordinator" + "github.com/elastic/elastic-agent/internal/pkg/capabilities" + "github.com/elastic/elastic-agent/internal/pkg/config" + "github.com/elastic/elastic-agent/pkg/core/logger" +) + +func PatchFleetConfig(log *logger.Logger, + rawConfig *config.Config, // original config from fleet, this one won't reload + caps capabilities.Capabilities, + isManaged bool, +) func(change coordinator.ConfigChange) coordinator.ConfigChange { + if !isManaged || // no need to override fleet config when not managed + caps == nil || !caps.AllowFleetOverride() { + return noop + } + + return func(change coordinator.ConfigChange) coordinator.ConfigChange { + newConfig := change.Config() + if err := newConfig.Merge(rawConfig); err != nil { + log.Errorf("error merging fleet config into config change: %v", err) + } + + return change + } +} diff --git a/internal/pkg/agent/application/fleet_config_patcher_test.go b/internal/pkg/agent/application/fleet_config_patcher_test.go new file mode 100644 index 00000000000..6713a024dd3 --- /dev/null +++ b/internal/pkg/agent/application/fleet_config_patcher_test.go @@ -0,0 +1,86 @@ +package application + +import ( + "os" + "path/filepath" + "testing" + + "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" + "github.com/elastic/elastic-agent/internal/pkg/config" + "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_FleetPatcher(t *testing.T) { + configFile := filepath.Join(".", "coordinator", "testdata", "overrides.yml") + + testCases := []struct { + name string + isManaged bool + featureEnable bool + expectedLogs bool + }{ + {name: "managed - enabled", isManaged: true, featureEnable: true, expectedLogs: false}, + {name: "managed - disabled", isManaged: true, featureEnable: false, expectedLogs: true}, + {name: "not managed - enabled", isManaged: false, featureEnable: true, expectedLogs: true}, + {name: "not managed - disabled", isManaged: false, featureEnable: false, expectedLogs: true}, + } + + overridesFile, err := os.OpenFile(configFile, os.O_RDONLY, 0) + require.NoError(t, err) + defer overridesFile.Close() + + rawConfig, err := config.NewConfigFrom(overridesFile) + require.NoError(t, err) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + caps := &mockCapabilities{} + caps.On("AllowFleetOverride").Return(tc.featureEnable) + + log, _ := loggertest.New(t.Name()) + + cfg, err := config.LoadFile(filepath.Join(".", "coordinator", "testdata", "config.yaml")) + require.NoError(t, err) + + configChange := &mockConfigChange{ + c: cfg, + } + + patcher := PatchFleetConfig(log, rawConfig, caps, tc.isManaged) + patcher(configChange) + + c := &configuration.Configuration{} + require.NoError(t, cfg.Agent.Unpack(&c)) + assert.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs) + require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) + require.True(t, c.Settings.MonitoringConfig.Enabled) + }) + } +} + +type mockCapabilities struct { + mock.Mock +} + +func (m *mockCapabilities) AllowUpgrade(version string, sourceURI string) bool { + args := m.Called(version, sourceURI) + return args.Bool(0) +} + +func (m *mockCapabilities) AllowInput(name string) bool { + args := m.Called() + return args.Bool(0) +} + +func (m *mockCapabilities) AllowOutput(name string) bool { + args := m.Called() + return args.Bool(0) +} + +func (m *mockCapabilities) AllowFleetOverride() bool { + args := m.Called() + return args.Bool(0) +} From 165155810cab1d4c1e1885b0d81b3a2d19ac4119 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 3 Dec 2025 16:17:11 +0100 Subject: [PATCH 05/16] added missing headers --- .../pkg/agent/application/fleet_config_patcher.go | 4 ++++ .../agent/application/fleet_config_patcher_test.go | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/pkg/agent/application/fleet_config_patcher.go b/internal/pkg/agent/application/fleet_config_patcher.go index e277e200bfa..83891bc015f 100644 --- a/internal/pkg/agent/application/fleet_config_patcher.go +++ b/internal/pkg/agent/application/fleet_config_patcher.go @@ -1,3 +1,7 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + package application import ( diff --git a/internal/pkg/agent/application/fleet_config_patcher_test.go b/internal/pkg/agent/application/fleet_config_patcher_test.go index 6713a024dd3..502830c32be 100644 --- a/internal/pkg/agent/application/fleet_config_patcher_test.go +++ b/internal/pkg/agent/application/fleet_config_patcher_test.go @@ -1,3 +1,7 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + package application import ( @@ -5,12 +9,13 @@ import ( "path/filepath" "testing" - "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" - "github.com/elastic/elastic-agent/internal/pkg/config" - "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" + "github.com/elastic/elastic-agent/internal/pkg/config" + "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" ) func Test_FleetPatcher(t *testing.T) { From 3a5a76f9fc14c3a90983c77694fa60b3d83ff681 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 4 Dec 2025 09:56:51 +0100 Subject: [PATCH 06/16] add output to test --- .../coordinator/testdata/overrides.yml | 9 +++++- .../application/fleet_config_patcher_test.go | 28 +++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/internal/pkg/agent/application/coordinator/testdata/overrides.yml b/internal/pkg/agent/application/coordinator/testdata/overrides.yml index 69cadda00f4..02399ea43dc 100644 --- a/internal/pkg/agent/application/coordinator/testdata/overrides.yml +++ b/internal/pkg/agent/application/coordinator/testdata/overrides.yml @@ -5,4 +5,11 @@ agent: logs: false metrics: true http: - port: 6774 \ No newline at end of file + port: 6774 +outputs: + default: + type: kafka + hosts: localhost + api_key: "" + username: "" + password: "" \ No newline at end of file diff --git a/internal/pkg/agent/application/fleet_config_patcher_test.go b/internal/pkg/agent/application/fleet_config_patcher_test.go index 502830c32be..3e21d373abb 100644 --- a/internal/pkg/agent/application/fleet_config_patcher_test.go +++ b/internal/pkg/agent/application/fleet_config_patcher_test.go @@ -22,15 +22,16 @@ func Test_FleetPatcher(t *testing.T) { configFile := filepath.Join(".", "coordinator", "testdata", "overrides.yml") testCases := []struct { - name string - isManaged bool - featureEnable bool - expectedLogs bool + name string + isManaged bool + featureEnable bool + expectedLogs bool + expectedOutputType string }{ - {name: "managed - enabled", isManaged: true, featureEnable: true, expectedLogs: false}, - {name: "managed - disabled", isManaged: true, featureEnable: false, expectedLogs: true}, - {name: "not managed - enabled", isManaged: false, featureEnable: true, expectedLogs: true}, - {name: "not managed - disabled", isManaged: false, featureEnable: false, expectedLogs: true}, + {name: "managed - enabled", isManaged: true, featureEnable: true, expectedLogs: false, expectedOutputType: "kafka"}, + {name: "managed - disabled", isManaged: true, featureEnable: false, expectedLogs: true, expectedOutputType: "elasticsearch"}, + {name: "not managed - enabled", isManaged: false, featureEnable: true, expectedLogs: true, expectedOutputType: "elasticsearch"}, + {name: "not managed - disabled", isManaged: false, featureEnable: false, expectedLogs: true, expectedOutputType: "elasticsearch"}, } overridesFile, err := os.OpenFile(configFile, os.O_RDONLY, 0) @@ -62,6 +63,17 @@ func Test_FleetPatcher(t *testing.T) { assert.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs) require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) require.True(t, c.Settings.MonitoringConfig.Enabled) + + // make sure output is not kafka + oc, err := cfg.Agent.Child("outputs", -1) + require.NoError(t, err) + + do, err := oc.Child("default", -1) + require.NoError(t, err) + + outputType, err := do.String("type", -1) + require.NoError(t, err) + assert.Equal(t, tc.expectedOutputType, outputType, "output type should be %s, got %s", tc.expectedOutputType, outputType) }) } } From 58a25579c10ba47ac5ab7e1d835787684d4825c8 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 4 Dec 2025 11:59:41 +0100 Subject: [PATCH 07/16] reverted patcher --- internal/pkg/agent/application/application.go | 13 +-- .../application/coordinator/coordinator.go | 35 ++++++ .../coordinator/coordinator_test.go | 30 +++++ .../agent/application/fleet_config_patcher.go | 32 ------ .../application/fleet_config_patcher_test.go | 103 ------------------ 5 files changed, 67 insertions(+), 146 deletions(-) delete mode 100644 internal/pkg/agent/application/fleet_config_patcher.go delete mode 100644 internal/pkg/agent/application/fleet_config_patcher_test.go diff --git a/internal/pkg/agent/application/application.go b/internal/pkg/agent/application/application.go index b663ad06575..c141815eef8 100644 --- a/internal/pkg/agent/application/application.go +++ b/internal/pkg/agent/application/application.go @@ -209,11 +209,7 @@ func New( log.Info("Parsed configuration and determined agent is in Fleet Server bootstrap mode") compModifiers = append(compModifiers, FleetServerComponentModifier(cfg.Fleet.Server)) - configMgr = coordinator.NewConfigPatchManager( - newFleetServerBootstrapManager(log), - PatchAPMConfig(log, rawConfig), - PatchFleetConfig(log, rawConfig, caps, isManaged), - ) + configMgr = coordinator.NewConfigPatchManager(newFleetServerBootstrapManager(log), PatchAPMConfig(log, rawConfig)) } else { log.Info("Parsed configuration and determined agent is managed by Fleet") @@ -263,12 +259,7 @@ func New( if err != nil { return nil, nil, nil, err } - configMgr = coordinator.NewConfigPatchManager( - managed, - injectOutputOverrides(log, rawConfig), - PatchAPMConfig(log, rawConfig), - PatchFleetConfig(log, rawConfig, caps, isManaged), - ) + configMgr = coordinator.NewConfigPatchManager(managed, injectOutputOverrides(log, rawConfig), PatchAPMConfig(log, rawConfig)) } } diff --git a/internal/pkg/agent/application/coordinator/coordinator.go b/internal/pkg/agent/application/coordinator/coordinator.go index d327c162b91..eca3ea09af2 100644 --- a/internal/pkg/agent/application/coordinator/coordinator.go +++ b/internal/pkg/agent/application/coordinator/coordinator.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "os" "reflect" "strings" "sync" @@ -1609,6 +1610,14 @@ func (c *Coordinator) processConfigAgent(ctx context.Context, cfg *config.Config c.logger.Errorf("failed to add secret markers: %v", err) } + // override retrieved config from Fleet with persisted config from AgentConfig file + + if c.caps != nil { + if err := applyPersistedConfig(cfg, paths.ConfigFile(), c.caps.AllowFleetOverride); err != nil { + return fmt.Errorf("could not apply persisted configuration: %w", err) + } + } + // perform and verify ast translation m, err := cfg.ToMapStr() if err != nil { @@ -1696,6 +1705,32 @@ func (c *Coordinator) generateAST(cfg *config.Config, m map[string]interface{}) return nil } +func applyPersistedConfig(cfg *config.Config, configFile string, checkFn func() bool) error { + if !checkFn() { + // Feature is disabled, nothing to do + return nil + } + + f, err := os.OpenFile(configFile, os.O_RDONLY, 0) + if err != nil && os.IsNotExist(err) { + return nil + } else if err != nil { + return fmt.Errorf("opening config file: %w", err) + } + defer f.Close() + + persisted, err := config.NewConfigFrom(f) + if err != nil { + return fmt.Errorf("parsing persisted config: %w", err) + } + + err = cfg.Merge(persisted) + if err != nil { + return fmt.Errorf("merging persisted config: %w", err) + } + return nil +} + // observeASTVars identifies the variables that are referenced in the computed AST and passed to // the varsMgr so it knows what providers are being referenced. If a providers is not being // referenced then the provider does not need to be running. diff --git a/internal/pkg/agent/application/coordinator/coordinator_test.go b/internal/pkg/agent/application/coordinator/coordinator_test.go index 54ca803ef85..26764de92c3 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_test.go @@ -1011,6 +1011,36 @@ func TestCoordinator_UpgradeDetails(t *testing.T) { require.Equal(t, expectedErr.Error(), coord.state.UpgradeDetails.Metadata.ErrorMsg) } +func Test_ApplyPersistedConfig(t *testing.T) { + cfgFile := filepath.Join(".", "testdata", "overrides.yml") + + testCases := []struct { + name string + featureEnable bool + expectedLogs bool + }{ + {name: "enabled", featureEnable: true, expectedLogs: false}, + {name: "disabled", featureEnable: false, expectedLogs: true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cfg, err := config.LoadFile(filepath.Join(".", "testdata", "config.yaml")) + require.NoError(t, err) + + err = applyPersistedConfig(cfg, cfgFile, func() bool { return tc.featureEnable }) + require.NoError(t, err) + + c := &configuration.Configuration{} + require.NoError(t, cfg.Agent.Unpack(&c)) + + require.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs) + require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) + require.True(t, c.Settings.MonitoringConfig.Enabled) + }) + } +} + func BenchmarkCoordinator_generateComponentModel(b *testing.B) { // load variables varsMaps := []map[string]any{} diff --git a/internal/pkg/agent/application/fleet_config_patcher.go b/internal/pkg/agent/application/fleet_config_patcher.go deleted file mode 100644 index 83891bc015f..00000000000 --- a/internal/pkg/agent/application/fleet_config_patcher.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -package application - -import ( - "github.com/elastic/elastic-agent/internal/pkg/agent/application/coordinator" - "github.com/elastic/elastic-agent/internal/pkg/capabilities" - "github.com/elastic/elastic-agent/internal/pkg/config" - "github.com/elastic/elastic-agent/pkg/core/logger" -) - -func PatchFleetConfig(log *logger.Logger, - rawConfig *config.Config, // original config from fleet, this one won't reload - caps capabilities.Capabilities, - isManaged bool, -) func(change coordinator.ConfigChange) coordinator.ConfigChange { - if !isManaged || // no need to override fleet config when not managed - caps == nil || !caps.AllowFleetOverride() { - return noop - } - - return func(change coordinator.ConfigChange) coordinator.ConfigChange { - newConfig := change.Config() - if err := newConfig.Merge(rawConfig); err != nil { - log.Errorf("error merging fleet config into config change: %v", err) - } - - return change - } -} diff --git a/internal/pkg/agent/application/fleet_config_patcher_test.go b/internal/pkg/agent/application/fleet_config_patcher_test.go deleted file mode 100644 index 3e21d373abb..00000000000 --- a/internal/pkg/agent/application/fleet_config_patcher_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License 2.0; -// you may not use this file except in compliance with the Elastic License 2.0. - -package application - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/elastic/elastic-agent/internal/pkg/agent/configuration" - "github.com/elastic/elastic-agent/internal/pkg/config" - "github.com/elastic/elastic-agent/pkg/core/logger/loggertest" -) - -func Test_FleetPatcher(t *testing.T) { - configFile := filepath.Join(".", "coordinator", "testdata", "overrides.yml") - - testCases := []struct { - name string - isManaged bool - featureEnable bool - expectedLogs bool - expectedOutputType string - }{ - {name: "managed - enabled", isManaged: true, featureEnable: true, expectedLogs: false, expectedOutputType: "kafka"}, - {name: "managed - disabled", isManaged: true, featureEnable: false, expectedLogs: true, expectedOutputType: "elasticsearch"}, - {name: "not managed - enabled", isManaged: false, featureEnable: true, expectedLogs: true, expectedOutputType: "elasticsearch"}, - {name: "not managed - disabled", isManaged: false, featureEnable: false, expectedLogs: true, expectedOutputType: "elasticsearch"}, - } - - overridesFile, err := os.OpenFile(configFile, os.O_RDONLY, 0) - require.NoError(t, err) - defer overridesFile.Close() - - rawConfig, err := config.NewConfigFrom(overridesFile) - require.NoError(t, err) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - caps := &mockCapabilities{} - caps.On("AllowFleetOverride").Return(tc.featureEnable) - - log, _ := loggertest.New(t.Name()) - - cfg, err := config.LoadFile(filepath.Join(".", "coordinator", "testdata", "config.yaml")) - require.NoError(t, err) - - configChange := &mockConfigChange{ - c: cfg, - } - - patcher := PatchFleetConfig(log, rawConfig, caps, tc.isManaged) - patcher(configChange) - - c := &configuration.Configuration{} - require.NoError(t, cfg.Agent.Unpack(&c)) - assert.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs) - require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) - require.True(t, c.Settings.MonitoringConfig.Enabled) - - // make sure output is not kafka - oc, err := cfg.Agent.Child("outputs", -1) - require.NoError(t, err) - - do, err := oc.Child("default", -1) - require.NoError(t, err) - - outputType, err := do.String("type", -1) - require.NoError(t, err) - assert.Equal(t, tc.expectedOutputType, outputType, "output type should be %s, got %s", tc.expectedOutputType, outputType) - }) - } -} - -type mockCapabilities struct { - mock.Mock -} - -func (m *mockCapabilities) AllowUpgrade(version string, sourceURI string) bool { - args := m.Called(version, sourceURI) - return args.Bool(0) -} - -func (m *mockCapabilities) AllowInput(name string) bool { - args := m.Called() - return args.Bool(0) -} - -func (m *mockCapabilities) AllowOutput(name string) bool { - args := m.Called() - return args.Bool(0) -} - -func (m *mockCapabilities) AllowFleetOverride() bool { - args := m.Called() - return args.Bool(0) -} From d80817cb7d64b00b357585efea0b7ef1a4afb388 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 4 Dec 2025 12:19:45 +0100 Subject: [PATCH 08/16] updated tests --- .../coordinator/coordinator_test.go | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/internal/pkg/agent/application/coordinator/coordinator_test.go b/internal/pkg/agent/application/coordinator/coordinator_test.go index 26764de92c3..2ceb9066d8d 100644 --- a/internal/pkg/agent/application/coordinator/coordinator_test.go +++ b/internal/pkg/agent/application/coordinator/coordinator_test.go @@ -1015,12 +1015,13 @@ func Test_ApplyPersistedConfig(t *testing.T) { cfgFile := filepath.Join(".", "testdata", "overrides.yml") testCases := []struct { - name string - featureEnable bool - expectedLogs bool + name string + featureEnable bool + expectedLogs bool + expectedOutputType string }{ - {name: "enabled", featureEnable: true, expectedLogs: false}, - {name: "disabled", featureEnable: false, expectedLogs: true}, + {name: "enabled", featureEnable: true, expectedLogs: false, expectedOutputType: "kafka"}, + {name: "disabled", featureEnable: false, expectedLogs: true, expectedOutputType: "elasticsearch"}, } for _, tc := range testCases { @@ -1037,6 +1038,16 @@ func Test_ApplyPersistedConfig(t *testing.T) { require.Equal(t, tc.expectedLogs, c.Settings.MonitoringConfig.MonitorLogs) require.True(t, c.Settings.MonitoringConfig.MonitorMetrics) require.True(t, c.Settings.MonitoringConfig.Enabled) + + // make sure output is not kafka + oc, err := cfg.Agent.Child("outputs", -1) + require.NoError(t, err) + + do, err := oc.Child("default", -1) + require.NoError(t, err) + outputType, err := do.String("type", -1) + require.NoError(t, err) + assert.Equal(t, tc.expectedOutputType, outputType, "output type should be %s, got %s", tc.expectedOutputType, outputType) }) } } From a8f379c539f32b065fc1d935b7df5f137c945dc4 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 5 Dec 2025 11:13:03 +0100 Subject: [PATCH 09/16] check permissions for root for capabilities --- internal/pkg/agent/cmd/run.go | 6 ++++++ pkg/utils/perm_unix.go | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index 8ef9662211e..a2bd1191887 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -286,6 +286,12 @@ func runElasticAgent( ownership.GID = gid } + // check capabilities permissions before fixing them + if err := utils.HasStrictExecPerms(paths.AgentCapabilitiesPath(), ownership.UID); err != nil { + // capabilities are corrupted, we should not proceed + return fmt.Errorf("invalid capabilities file permissions: %w", err) + } + topPath := paths.Top() err = perms.FixPermissions(topPath, perms.WithOwnership(ownership)) if err != nil { diff --git a/pkg/utils/perm_unix.go b/pkg/utils/perm_unix.go index a8eac8d8874..bf8fb8c68ea 100644 --- a/pkg/utils/perm_unix.go +++ b/pkg/utils/perm_unix.go @@ -9,6 +9,8 @@ package utils import ( "errors" "os" + + "github.com/elastic/elastic-agent-libs/file" ) // FileOwner is the ownership a file should have. @@ -28,7 +30,7 @@ func CurrentFileOwner() (FileOwner, error) { // HasStrictExecPerms ensures that the path is executable by the owner, cannot be written by anyone other than the // owner of the file and that the owner of the file is the same as the UID or root. func HasStrictExecPerms(path string, uid int) error { - info, err := os.Stat(path) + info, err := file.Stat(path) if err != nil { return err } @@ -41,5 +43,15 @@ func HasStrictExecPerms(path string, uid int) error { if info.Mode()&0100 == 0 { return errors.New("not executable by owner") } + + fileUID, err := info.UID() + if err != nil { + return err + } + + if fileUID != 0 && fileUID != uid { + return errors.New("file owner does not match expected UID or root") + } + return nil } From 7ff467c132bdea1ed9ae566cdcd5bceb06747a75 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 5 Dec 2025 11:32:19 +0100 Subject: [PATCH 10/16] wrapping because different uid type on windows --- internal/pkg/agent/cmd/run.go | 3 +-- internal/pkg/agent/cmd/run_darwin.go | 4 ++++ internal/pkg/agent/cmd/run_linux.go | 4 ++++ internal/pkg/agent/cmd/run_windows.go | 5 +++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index a2bd1191887..0a0e7faba80 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -287,8 +287,7 @@ func runElasticAgent( } // check capabilities permissions before fixing them - if err := utils.HasStrictExecPerms(paths.AgentCapabilitiesPath(), ownership.UID); err != nil { - // capabilities are corrupted, we should not proceed + if err := checkCapabilitiesPerms(paths.AgentCapabilitiesPath(), ownership.UID); err != nil { return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/internal/pkg/agent/cmd/run_darwin.go b/internal/pkg/agent/cmd/run_darwin.go index db588e2d242..be9b17bc1d8 100644 --- a/internal/pkg/agent/cmd/run_darwin.go +++ b/internal/pkg/agent/cmd/run_darwin.go @@ -39,3 +39,7 @@ func dropRootPrivileges(l *logger.Logger, ownership utils.FileOwner) error { return nil } + +func checkCapabilitiesPerms(agentCapabilitiesPath string, uid int) error { + return utils.HasStrictExecPerms(agentCapabilitiesPath, uid) +} diff --git a/internal/pkg/agent/cmd/run_linux.go b/internal/pkg/agent/cmd/run_linux.go index b18aba7efbc..ef537eabcd0 100644 --- a/internal/pkg/agent/cmd/run_linux.go +++ b/internal/pkg/agent/cmd/run_linux.go @@ -41,3 +41,7 @@ func dropRootPrivileges(l *logger.Logger, ownership utils.FileOwner) error { return nil } + +func checkCapabilitiesPerms(agentCapabilitiesPath string, uid int) error { + return utils.HasStrictExecPerms(agentCapabilitiesPath, uid) +} diff --git a/internal/pkg/agent/cmd/run_windows.go b/internal/pkg/agent/cmd/run_windows.go index 2c776bfcc90..80b8c1ec342 100644 --- a/internal/pkg/agent/cmd/run_windows.go +++ b/internal/pkg/agent/cmd/run_windows.go @@ -26,3 +26,8 @@ func logExternal(msg string) { } func dropRootPrivileges(_ *logger.Logger, _ utils.FileOwner) error { return nil } + +func checkCapabilitiesPerms(_ string, _ string) error { + // not implemented on Windows + return nil +} From aa22ec2a4a6ba60e6062fea6085aa1bdf0557190 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 5 Dec 2025 11:47:33 +0100 Subject: [PATCH 11/16] ignore not found --- internal/pkg/agent/cmd/run_darwin.go | 8 +++++++- internal/pkg/agent/cmd/run_linux.go | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/internal/pkg/agent/cmd/run_darwin.go b/internal/pkg/agent/cmd/run_darwin.go index be9b17bc1d8..01b15b8b700 100644 --- a/internal/pkg/agent/cmd/run_darwin.go +++ b/internal/pkg/agent/cmd/run_darwin.go @@ -8,6 +8,7 @@ package cmd import ( "fmt" + "os" "syscall" "github.com/elastic/elastic-agent/pkg/core/logger" @@ -41,5 +42,10 @@ func dropRootPrivileges(l *logger.Logger, ownership utils.FileOwner) error { } func checkCapabilitiesPerms(agentCapabilitiesPath string, uid int) error { - return utils.HasStrictExecPerms(agentCapabilitiesPath, uid) + if err := utils.HasStrictExecPerms(agentCapabilitiesPath, uid); err != nil && !os.IsNotExist(err) { + // capabilities are corrupted, we should not proceed + return fmt.Errorf("invalid capabilities file permissions: %w", err) + } + + return nil } diff --git a/internal/pkg/agent/cmd/run_linux.go b/internal/pkg/agent/cmd/run_linux.go index ef537eabcd0..d289b53e28e 100644 --- a/internal/pkg/agent/cmd/run_linux.go +++ b/internal/pkg/agent/cmd/run_linux.go @@ -8,6 +8,7 @@ package cmd import ( "fmt" + "os" "syscall" "github.com/elastic/elastic-agent/pkg/core/logger" @@ -43,5 +44,10 @@ func dropRootPrivileges(l *logger.Logger, ownership utils.FileOwner) error { } func checkCapabilitiesPerms(agentCapabilitiesPath string, uid int) error { - return utils.HasStrictExecPerms(agentCapabilitiesPath, uid) + if err := utils.HasStrictExecPerms(agentCapabilitiesPath, uid); err != nil && !os.IsNotExist(err) { + // capabilities are corrupted, we should not proceed + return fmt.Errorf("invalid capabilities file permissions: %w", err) + } + + return nil } From feb91321200412636255f3abfa7d2bbfc390a2d3 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 5 Dec 2025 13:05:54 +0100 Subject: [PATCH 12/16] differentiate between desired and accidental root --- internal/pkg/agent/cmd/run.go | 2 +- internal/pkg/agent/cmd/run_darwin.go | 11 +++++++++-- internal/pkg/agent/cmd/run_linux.go | 10 ++++++++-- internal/pkg/agent/cmd/run_windows.go | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index 0a0e7faba80..0f25c040539 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -287,7 +287,7 @@ func runElasticAgent( } // check capabilities permissions before fixing them - if err := checkCapabilitiesPerms(paths.AgentCapabilitiesPath(), ownership.UID); err != nil { + if err := checkCapabilitiesPerms(paths.AgentCapabilitiesPath(), userName, ownership.UID); err != nil { return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/internal/pkg/agent/cmd/run_darwin.go b/internal/pkg/agent/cmd/run_darwin.go index 01b15b8b700..79b729dd449 100644 --- a/internal/pkg/agent/cmd/run_darwin.go +++ b/internal/pkg/agent/cmd/run_darwin.go @@ -41,8 +41,15 @@ func dropRootPrivileges(l *logger.Logger, ownership utils.FileOwner) error { return nil } -func checkCapabilitiesPerms(agentCapabilitiesPath string, uid int) error { - if err := utils.HasStrictExecPerms(agentCapabilitiesPath, uid); err != nil && !os.IsNotExist(err) { +func checkCapabilitiesPerms(agentCapabilitiesPath string, userName string, uid int) error { + + var capabilitiesUID int + if userName != "" { + capabilitiesUID = uid + } else { + capabilitiesUID = os.Getuid() + } + if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID); err != nil && !os.IsNotExist(err) { // capabilities are corrupted, we should not proceed return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/internal/pkg/agent/cmd/run_linux.go b/internal/pkg/agent/cmd/run_linux.go index d289b53e28e..01f45481c55 100644 --- a/internal/pkg/agent/cmd/run_linux.go +++ b/internal/pkg/agent/cmd/run_linux.go @@ -43,8 +43,14 @@ func dropRootPrivileges(l *logger.Logger, ownership utils.FileOwner) error { } -func checkCapabilitiesPerms(agentCapabilitiesPath string, uid int) error { - if err := utils.HasStrictExecPerms(agentCapabilitiesPath, uid); err != nil && !os.IsNotExist(err) { +func checkCapabilitiesPerms(agentCapabilitiesPath string, userName string, uid int) error { + var capabilitiesUID int + if userName != "" { + capabilitiesUID = uid + } else { + capabilitiesUID = os.Getuid() + } + if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID); err != nil && !os.IsNotExist(err) { // capabilities are corrupted, we should not proceed return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/internal/pkg/agent/cmd/run_windows.go b/internal/pkg/agent/cmd/run_windows.go index 80b8c1ec342..80531f1f26e 100644 --- a/internal/pkg/agent/cmd/run_windows.go +++ b/internal/pkg/agent/cmd/run_windows.go @@ -27,7 +27,7 @@ func logExternal(msg string) { func dropRootPrivileges(_ *logger.Logger, _ utils.FileOwner) error { return nil } -func checkCapabilitiesPerms(_ string, _ string) error { +func checkCapabilitiesPerms(_ string, _ string, _ string) error { // not implemented on Windows return nil } From b13ce202cc223c621064c9f0468cb3d39fde67c9 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 5 Dec 2025 13:06:13 +0100 Subject: [PATCH 13/16] differentiate between desired and accidental root --- internal/pkg/agent/cmd/run_darwin.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/pkg/agent/cmd/run_darwin.go b/internal/pkg/agent/cmd/run_darwin.go index 79b729dd449..5c257c17421 100644 --- a/internal/pkg/agent/cmd/run_darwin.go +++ b/internal/pkg/agent/cmd/run_darwin.go @@ -42,7 +42,6 @@ func dropRootPrivileges(l *logger.Logger, ownership utils.FileOwner) error { } func checkCapabilitiesPerms(agentCapabilitiesPath string, userName string, uid int) error { - var capabilitiesUID int if userName != "" { capabilitiesUID = uid From a34fd867792a1f981738b5198ba5ee2571b353a1 Mon Sep 17 00:00:00 2001 From: Michal Date: Fri, 5 Dec 2025 14:33:49 +0100 Subject: [PATCH 14/16] skip check uid owner for backward compatibility --- internal/pkg/agent/cmd/run_darwin.go | 2 +- internal/pkg/agent/cmd/run_linux.go | 2 +- pkg/component/runtime/command.go | 2 +- pkg/utils/perm_unix.go | 4 ++-- pkg/utils/perm_windows.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/pkg/agent/cmd/run_darwin.go b/internal/pkg/agent/cmd/run_darwin.go index 5c257c17421..7e2e7d2cdec 100644 --- a/internal/pkg/agent/cmd/run_darwin.go +++ b/internal/pkg/agent/cmd/run_darwin.go @@ -48,7 +48,7 @@ func checkCapabilitiesPerms(agentCapabilitiesPath string, userName string, uid i } else { capabilitiesUID = os.Getuid() } - if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID); err != nil && !os.IsNotExist(err) { + if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID, true); err != nil && !os.IsNotExist(err) { // capabilities are corrupted, we should not proceed return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/internal/pkg/agent/cmd/run_linux.go b/internal/pkg/agent/cmd/run_linux.go index 01f45481c55..4c81e94b831 100644 --- a/internal/pkg/agent/cmd/run_linux.go +++ b/internal/pkg/agent/cmd/run_linux.go @@ -50,7 +50,7 @@ func checkCapabilitiesPerms(agentCapabilitiesPath string, userName string, uid i } else { capabilitiesUID = os.Getuid() } - if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID); err != nil && !os.IsNotExist(err) { + if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID, true); err != nil && !os.IsNotExist(err) { // capabilities are corrupted, we should not proceed return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/pkg/component/runtime/command.go b/pkg/component/runtime/command.go index 765b154f1b0..c808a1a3ff7 100644 --- a/pkg/component/runtime/command.go +++ b/pkg/component/runtime/command.go @@ -369,7 +369,7 @@ func (c *commandRuntime) start(comm Communicator) error { if err != nil { return fmt.Errorf("failed to determine absolute path: %w", err) } - err = utils.HasStrictExecPerms(path, uid) + err = utils.HasStrictExecPerms(path, uid, false) if err != nil { return fmt.Errorf("execution of component prevented: %w", err) } diff --git a/pkg/utils/perm_unix.go b/pkg/utils/perm_unix.go index bf8fb8c68ea..fe8bc5cc7c7 100644 --- a/pkg/utils/perm_unix.go +++ b/pkg/utils/perm_unix.go @@ -29,7 +29,7 @@ func CurrentFileOwner() (FileOwner, error) { // HasStrictExecPerms ensures that the path is executable by the owner, cannot be written by anyone other than the // owner of the file and that the owner of the file is the same as the UID or root. -func HasStrictExecPerms(path string, uid int) error { +func HasStrictExecPerms(path string, uid int, checkUID bool) error { info, err := file.Stat(path) if err != nil { return err @@ -49,7 +49,7 @@ func HasStrictExecPerms(path string, uid int) error { return err } - if fileUID != 0 && fileUID != uid { + if checkUID && fileUID != 0 && fileUID != uid { return errors.New("file owner does not match expected UID or root") } diff --git a/pkg/utils/perm_windows.go b/pkg/utils/perm_windows.go index b87c9e0cc16..5dfa3497e0a 100644 --- a/pkg/utils/perm_windows.go +++ b/pkg/utils/perm_windows.go @@ -63,7 +63,7 @@ func CurrentFileOwner() (FileOwner, error) { // HasStrictExecPerms ensures that the path is executable by the owner and that the owner of the file // is the same as the UID or root. -func HasStrictExecPerms(path string, uid int) error { +func HasStrictExecPerms(path string, uid int, _ bool) error { // TODO: Need to add check on Windows to ensure that the ACL are correct for the binary before execution. return nil } From 4e8cf1bc80b46893f1a5d4dcc8161b509ceb1cdf Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 10 Dec 2025 09:31:15 +0100 Subject: [PATCH 15/16] separate functions for ownership check to improve readability --- internal/pkg/agent/cmd/run_darwin.go | 2 +- internal/pkg/agent/cmd/run_linux.go | 2 +- pkg/component/runtime/command.go | 3 +-- pkg/utils/perm_unix.go | 38 +++++++++++++++++++++------- pkg/utils/perm_windows.go | 10 ++++++-- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/internal/pkg/agent/cmd/run_darwin.go b/internal/pkg/agent/cmd/run_darwin.go index 7e2e7d2cdec..add3d5bb0eb 100644 --- a/internal/pkg/agent/cmd/run_darwin.go +++ b/internal/pkg/agent/cmd/run_darwin.go @@ -48,7 +48,7 @@ func checkCapabilitiesPerms(agentCapabilitiesPath string, userName string, uid i } else { capabilitiesUID = os.Getuid() } - if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID, true); err != nil && !os.IsNotExist(err) { + if err := utils.HasStrictExecPermsAndOwnership(agentCapabilitiesPath, capabilitiesUID); err != nil && !os.IsNotExist(err) { // capabilities are corrupted, we should not proceed return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/internal/pkg/agent/cmd/run_linux.go b/internal/pkg/agent/cmd/run_linux.go index 4c81e94b831..d89516917c3 100644 --- a/internal/pkg/agent/cmd/run_linux.go +++ b/internal/pkg/agent/cmd/run_linux.go @@ -50,7 +50,7 @@ func checkCapabilitiesPerms(agentCapabilitiesPath string, userName string, uid i } else { capabilitiesUID = os.Getuid() } - if err := utils.HasStrictExecPerms(agentCapabilitiesPath, capabilitiesUID, true); err != nil && !os.IsNotExist(err) { + if err := utils.HasStrictExecPermsAndOwnership(agentCapabilitiesPath, capabilitiesUID); err != nil && !os.IsNotExist(err) { // capabilities are corrupted, we should not proceed return fmt.Errorf("invalid capabilities file permissions: %w", err) } diff --git a/pkg/component/runtime/command.go b/pkg/component/runtime/command.go index c808a1a3ff7..66f3842d431 100644 --- a/pkg/component/runtime/command.go +++ b/pkg/component/runtime/command.go @@ -363,13 +363,12 @@ func (c *commandRuntime) start(comm Communicator) error { } env = append(env, fmt.Sprintf("%s=%s", envAgentComponentID, c.current.ID)) env = append(env, fmt.Sprintf("%s=%s", envAgentComponentType, c.getSpecType())) - uid := os.Geteuid() workDir := c.current.WorkDirPath(paths.Run()) path, err := filepath.Abs(c.getSpecBinaryPath()) if err != nil { return fmt.Errorf("failed to determine absolute path: %w", err) } - err = utils.HasStrictExecPerms(path, uid, false) + err = utils.HasStrictExecPerms(path) if err != nil { return fmt.Errorf("execution of component prevented: %w", err) } diff --git a/pkg/utils/perm_unix.go b/pkg/utils/perm_unix.go index fe8bc5cc7c7..00921a4e888 100644 --- a/pkg/utils/perm_unix.go +++ b/pkg/utils/perm_unix.go @@ -29,19 +29,25 @@ func CurrentFileOwner() (FileOwner, error) { // HasStrictExecPerms ensures that the path is executable by the owner, cannot be written by anyone other than the // owner of the file and that the owner of the file is the same as the UID or root. -func HasStrictExecPerms(path string, uid int, checkUID bool) error { +func HasStrictExecPerms(path string) error { info, err := file.Stat(path) if err != nil { return err } - if info.IsDir() { - return errors.New("is a directory") - } - if info.Mode()&0022 != 0 { - return errors.New("cannot be writeable by group or other") + + return hasStrictExecPerms(info) +} + +// HasStrictExecPermsAndOwnership ensures that the path is executable by the owner and that the owner of the file +// is the same as the UID or root. +func HasStrictExecPermsAndOwnership(path string, uid int) error { + info, err := file.Stat(path) + if err != nil { + return err } - if info.Mode()&0100 == 0 { - return errors.New("not executable by owner") + + if err := hasStrictExecPerms(info); err != nil { + return err } fileUID, err := info.UID() @@ -49,9 +55,23 @@ func HasStrictExecPerms(path string, uid int, checkUID bool) error { return err } - if checkUID && fileUID != 0 && fileUID != uid { + if fileUID != 0 && fileUID != uid { return errors.New("file owner does not match expected UID or root") } return nil } + +func hasStrictExecPerms(info file.FileInfo) error { + if info.IsDir() { + return errors.New("is a directory") + } + if info.Mode()&0022 != 0 { + return errors.New("cannot be writeable by group or other") + } + if info.Mode()&0100 == 0 { + return errors.New("not executable by owner") + } + + return nil +} diff --git a/pkg/utils/perm_windows.go b/pkg/utils/perm_windows.go index 5dfa3497e0a..7e63cc3a7b0 100644 --- a/pkg/utils/perm_windows.go +++ b/pkg/utils/perm_windows.go @@ -61,9 +61,15 @@ func CurrentFileOwner() (FileOwner, error) { }, nil } -// HasStrictExecPerms ensures that the path is executable by the owner and that the owner of the file +// HasStrictExecPerms ensures that the path is executable by the owner. +func HasStrictExecPerms(path string) error { + // TODO: Need to add check on Windows to ensure that the ACL are correct for the binary before execution. + return nil +} + +// HasStrictExecPermsAndOwnership ensures that the path is executable by the owner and that the owner of the file // is the same as the UID or root. -func HasStrictExecPerms(path string, uid int, _ bool) error { +func HasStrictExecPermsAndOwnership(path string, uid int) error { // TODO: Need to add check on Windows to ensure that the ACL are correct for the binary before execution. return nil } From a2709d96ef85dbcf354db17fc76fbadce87a6d3d Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 10 Dec 2025 10:28:00 +0100 Subject: [PATCH 16/16] disably overrides when containerized --- .../application/coordinator/coordinator.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/internal/pkg/agent/application/coordinator/coordinator.go b/internal/pkg/agent/application/coordinator/coordinator.go index eca3ea09af2..01de65a4b41 100644 --- a/internal/pkg/agent/application/coordinator/coordinator.go +++ b/internal/pkg/agent/application/coordinator/coordinator.go @@ -615,7 +615,7 @@ func (c *Coordinator) Migrate( backoffFactory func(done <-chan struct{}) backoff.Backoff, notifyFn func(context.Context, *fleetapi.ActionMigrate) error, ) error { - if c.specs.Platform().OS == component.Container { + if c.isContainerizedEnvironment() { return ErrContainerNotSupported } if !c.isManaged { @@ -1613,7 +1613,7 @@ func (c *Coordinator) processConfigAgent(ctx context.Context, cfg *config.Config // override retrieved config from Fleet with persisted config from AgentConfig file if c.caps != nil { - if err := applyPersistedConfig(cfg, paths.ConfigFile(), c.caps.AllowFleetOverride); err != nil { + if err := applyPersistedConfig(cfg, paths.ConfigFile(), c.isContainerizedEnvironment, c.caps.AllowFleetOverride); err != nil { return fmt.Errorf("could not apply persisted configuration: %w", err) } } @@ -1705,10 +1705,12 @@ func (c *Coordinator) generateAST(cfg *config.Config, m map[string]interface{}) return nil } -func applyPersistedConfig(cfg *config.Config, configFile string, checkFn func() bool) error { - if !checkFn() { - // Feature is disabled, nothing to do - return nil +func applyPersistedConfig(cfg *config.Config, configFile string, checkFns ...func() bool) error { + for _, checkFn := range checkFns { + if !checkFn() { + // Feature is disabled, nothing to do + return nil + } } f, err := os.OpenFile(configFile, os.O_RDONLY, 0) @@ -2397,3 +2399,7 @@ func computeEnrollOptions(ctx context.Context, cfgPath string, cfgFleetPath stri options = enroll.FromFleetConfig(cfg.Fleet) return options, nil } + +func (c *Coordinator) isContainerizedEnvironment() bool { + return c.specs.Platform().OS == component.Container +}