Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f340dc5
poc
michalpristas Aug 14, 2025
6c8a2e4
conflicts
michalpristas Aug 20, 2025
39d1fb8
Merge branch 'main' of github.com:elastic/elastic-agent into feat/swi…
michalpristas Sep 15, 2025
a882267
setuid to drop permissions
michalpristas Sep 23, 2025
7c0bce4
setuid to drop permissions
michalpristas Sep 23, 2025
8f36b1f
Merge branch 'main' of github.com:elastic/elastic-agent into feat/swi…
michalpristas Sep 23, 2025
b231c1c
resolved
michalpristas Sep 29, 2025
fa2cae1
Merge branch 'main' of github.com:elastic/elastic-agent into feat/swi…
michalpristas Sep 30, 2025
fc05260
support for linux
michalpristas Sep 30, 2025
dd606ac
Merge branch 'main' of github.com:elastic/elastic-agent into feat/swi…
michalpristas Sep 30, 2025
104487e
fmt
michalpristas Oct 1, 2025
d806184
merge with main, otel conflicts
michalpristas Oct 1, 2025
b165455
linter
michalpristas Oct 1, 2025
9bd7311
changelog
michalpristas Oct 1, 2025
9ff43ca
lint
michalpristas Oct 1, 2025
89e4fcf
lint
michalpristas Oct 1, 2025
7cd5e9f
Update internal/pkg/agent/application/actions/handlers/handle_action_…
michalpristas Oct 2, 2025
596edd9
Update internal/pkg/agent/cmd/run_darwin.go
michalpristas Oct 2, 2025
c269603
imports
michalpristas Oct 2, 2025
de325b1
naming
michalpristas Oct 3, 2025
b432690
unsaved file
michalpristas Oct 3, 2025
6d4ae3a
Update internal/pkg/fleetapi/action.go
michalpristas Oct 6, 2025
5d937b6
Update internal/pkg/fleetapi/action.go
michalpristas Oct 6, 2025
ed2fe10
Rename Describer interface
michalpristas Oct 6, 2025
cfa5315
Merge branch 'feat/switch-action' of github.com:michalpristas/elastic…
michalpristas Oct 6, 2025
dfa8800
Merge branch 'main' of github.com:elastic/elastic-agent into feat/swi…
michalpristas Oct 6, 2025
52e1634
Update changelog/fragments/1759318728-support-for-privilege_level_cha…
michalpristas Oct 6, 2025
69c9c7e
Update internal/pkg/agent/application/actions/handlers/handle_action_…
michalpristas Oct 6, 2025
521ba4f
Update internal/pkg/agent/application/actions/handlers/handle_action_…
michalpristas Oct 6, 2025
9173efc
helper for stopping components
michalpristas Oct 6, 2025
f30b811
Merge branch 'feat/switch-action' of github.com:michalpristas/elastic…
michalpristas Oct 6, 2025
109df8a
Merge branch 'main' of github.com:elastic/elastic-agent into feat/swi…
michalpristas Oct 6, 2025
43c65bb
conflicts with main
michalpristas Oct 24, 2025
3fe91c8
move check to action
michalpristas Oct 24, 2025
a8d9299
resolved otel cycle imports
michalpristas Oct 24, 2025
159c745
unused describer
michalpristas Oct 24, 2025
85c1a6d
conflicts
michalpristas Oct 24, 2025
075605d
fixed conflict with otel work
michalpristas Nov 3, 2025
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,45 @@
# REQUIRED
# 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

# REQUIRED for all kinds
# Change summary; a 80ish characters long description of the change.
summary: support for privilege_level_change

# REQUIRED for breaking-change, deprecation, known-issue
# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
# description:

# REQUIRED for breaking-change, deprecation, known-issue
# impact:

# REQUIRED for breaking-change, deprecation, known-issue
# action:

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

# AUTOMATED
# OPTIONAL to manually add other PR URLs
# PR URL: A link the PR 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

# AUTOMATED
# OPTIONAL to manually add other issue URLs
# 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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 handlers

import (
"context"
"fmt"

"github.com/elastic/elastic-agent/internal/pkg/agent/application/coordinator"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/reexec"
"github.com/elastic/elastic-agent/internal/pkg/agent/install"
"github.com/elastic/elastic-agent/internal/pkg/agent/install/service"
"github.com/elastic/elastic-agent/internal/pkg/config"
"github.com/elastic/elastic-agent/internal/pkg/fleetapi"
"github.com/elastic/elastic-agent/internal/pkg/fleetapi/acker"
"github.com/elastic/elastic-agent/pkg/core/logger"
)

type reexecCoordinator interface {
ReExec(callback reexec.ShutdownCallbackFn, argOverrides ...string)
}

type PrivilegeLevelChange struct {
log *logger.Logger
coord reexecCoordinator
ch chan coordinator.ConfigChange
}

func NewPrivilegeLevelChange(
log *logger.Logger,
coord reexecCoordinator,
ch chan coordinator.ConfigChange,
) *PrivilegeLevelChange {
return &PrivilegeLevelChange{
log: log,
coord: coord,
ch: ch,
}
}

// Handle handles UNENROLL action.
func (h *PrivilegeLevelChange) Handle(ctx context.Context, a fleetapi.Action, acker acker.Acker) error {
return h.handle(ctx, a, acker)
}

func (h *PrivilegeLevelChange) handle(ctx context.Context, a fleetapi.Action, acker acker.Acker) (rerr error) {
action, ok := a.(*fleetapi.ActionPrivilegeLevelChange)
if !ok {
return fmt.Errorf("invalid type, expected ActionPrivilegeLevelChange and received %T", a)
}

defer func() {
if rerr != nil {
h.ackFailure(ctx, rerr, action, acker)
}
}()

if !action.Data.Unprivileged {
// only unprivileged supported at this point
return fmt.Errorf("unsupported action, ActionPrivilegeLevelChange supports only downgrading permissions")
}

// ensure no component issues
err := service.EnsureNoServiceComponentIssues()
if err != nil {
h.log.Debugf("handlerPrivilegeLevelChange: found issues with components: %v", err)
return err
}

var username, groupname, password string
if action.Data.UserInfo != nil {
username = action.Data.UserInfo.Username
groupname = action.Data.UserInfo.Groupname
password = action.Data.UserInfo.Password
}
username, password = install.UnprivilegedUser(username, password)
groupname = install.UnprivilegedGroup(groupname)

// apply empty config to stop processing
unenrollPolicy := newPolicyChange(ctx, config.New(), a, acker, true, false)
h.ch <- unenrollPolicy

unenrollCtx, cancel := context.WithTimeout(ctx, unenrollTimeout)
defer cancel()

unenrollPolicy.WaitAck(unenrollCtx)

// fix permissions
topPath := paths.Top()
_, err = install.SwitchServiceUser(topPath, &debugDescriber{h.log}, username, groupname, password)
if err != nil {
// error already adds context
return err
}

// ack
if err := acker.Ack(ctx, a); err != nil {
h.log.Errorw("failed to ACK an action",
"error.message", err)
}
if err := acker.Commit(ctx); err != nil {
h.log.Errorw("failed to commit ACK of an action",
"error.message", err)
}

// restart
h.coord.ReExec(nil)
return nil
}

func (h *PrivilegeLevelChange) ackFailure(ctx context.Context, err error, action *fleetapi.ActionPrivilegeLevelChange, acker acker.Acker) {
action.Err = err

if err := acker.Ack(ctx, action); err != nil {
h.log.Errorw("failed to ack privilege level change action",
"error.message", err,
"action", action)
}

if err := acker.Commit(ctx); err != nil {
h.log.Errorw("failed to commit privilege level change action",
"error.message", err,
"action", action)
}
}

type debugDescriber struct {
l *logger.Logger
}

func (d *debugDescriber) Describe(a string) {
d.l.Debug(a)
}
5 changes: 5 additions & 0 deletions internal/pkg/agent/application/managed_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,11 @@ func (m *managedConfigManager) initDispatcher(canceller context.CancelFunc) *han
handlers.NewMigrate(m.log, m.agentInfo, m.coord),
)

m.dispatcher.MustRegister(
&fleetapi.ActionPrivilegeLevelChange{},
handlers.NewPrivilegeLevelChange(m.log, m.coord, m.ch),
)

m.dispatcher.MustRegister(
&fleetapi.ActionUnknown{},
handlers.NewUnknown(m.log),
Expand Down
135 changes: 4 additions & 131 deletions internal/pkg/agent/cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,20 @@ import (
"time"

"github.com/spf13/cobra"
"go.opentelemetry.io/collector/confmap"
"gopkg.in/yaml.v2"

componentmonitoring "github.com/elastic/elastic-agent/internal/pkg/agent/application/monitoring/component"

"github.com/elastic/elastic-agent-libs/logp"
"github.com/elastic/elastic-agent-libs/service"

"github.com/elastic/elastic-agent/internal/pkg/agent/application/info"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
"github.com/elastic/elastic-agent/internal/pkg/agent/configuration"
"github.com/elastic/elastic-agent/internal/pkg/agent/errors"
"github.com/elastic/elastic-agent/internal/pkg/agent/transpiler"
"github.com/elastic/elastic-agent/internal/pkg/agent/vars"
installSvc "github.com/elastic/elastic-agent/internal/pkg/agent/install/service"
"github.com/elastic/elastic-agent/internal/pkg/capabilities"
"github.com/elastic/elastic-agent/internal/pkg/cli"
"github.com/elastic/elastic-agent/internal/pkg/config"
"github.com/elastic/elastic-agent/internal/pkg/config/operations"
"github.com/elastic/elastic-agent/internal/pkg/diagnostics"
otelconfig "github.com/elastic/elastic-agent/internal/pkg/otel/config"
"github.com/elastic/elastic-agent/internal/pkg/otel/manager"
"github.com/elastic/elastic-agent/pkg/component"
"github.com/elastic/elastic-agent/pkg/core/logger"
"github.com/elastic/elastic-agent/pkg/utils"
Expand Down Expand Up @@ -161,7 +154,7 @@ func inspectConfig(ctx context.Context, cfgPath string, opts inspectConfigOpts,
return nil
}

cfg, otel, lvl, err := getConfigWithVariables(ctx, l, cfgPath, opts.variablesWait, !isAdmin)
cfg, otel, lvl, err := installSvc.GetConfigWithVariables(ctx, l, cfgPath, opts.variablesWait, !isAdmin)
if err != nil {
return fmt.Errorf("error fetching config with variables: %w", err)
}
Expand All @@ -183,7 +176,7 @@ func inspectConfig(ctx context.Context, cfgPath string, opts inspectConfigOpts,
return fmt.Errorf("failed to detect inputs and outputs: %w", err)
}

monitorFn, err := getMonitoringFn(ctx, l, cfg, otel)
monitorFn, err := installSvc.GetMonitoringFn(ctx, l, cfg, otel)
if err != nil {
return fmt.Errorf("failed to get monitoring: %w", err)
}
Expand Down Expand Up @@ -291,7 +284,7 @@ func inspectComponents(ctx context.Context, cfgPath string, opts inspectComponen
return err
}

comps, err := getComponentsFromPolicy(ctx, l, cfgPath, opts.variablesWait)
comps, err := installSvc.GetComponentsFromPolicy(ctx, l, cfgPath, opts.variablesWait)
if err != nil {
// error already includes the context
return err
Expand Down Expand Up @@ -356,126 +349,6 @@ func inspectComponents(ctx context.Context, cfgPath string, opts inspectComponen
return printComponents(allowed, blocked, streams)
}

func getComponentsFromPolicy(ctx context.Context, l *logger.Logger, cfgPath string, variablesWait time.Duration, platformModifiers ...component.PlatformModifier) ([]component.Component, error) {
// Load the requirements before trying to load the configuration. These should always load
// even if the configuration is wrong.
platform, err := component.LoadPlatformDetail(platformModifiers...)
if err != nil {
return nil, fmt.Errorf("failed to gather system information: %w", err)
}
specs, err := component.LoadRuntimeSpecs(paths.Components(), platform)
if err != nil {
return nil, fmt.Errorf("failed to detect inputs and outputs: %w", err)
}

isAdmin, err := utils.HasRoot()
if err != nil {
return nil, fmt.Errorf("error checking for root/Administrator privileges: %w", err)
}

m, otel, lvl, err := getConfigWithVariables(ctx, l, cfgPath, variablesWait, !isAdmin)
if err != nil {
return nil, err
}

monitorFn, err := getMonitoringFn(ctx, l, m, otel)
if err != nil {
return nil, fmt.Errorf("failed to get monitoring: %w", err)
}

agentInfo, err := info.NewAgentInfoWithLog(ctx, "error", false)
if err != nil {
return nil, fmt.Errorf("could not load agent info: %w", err)
}

// Compute the components from the computed configuration.
comps, err := specs.ToComponents(m, monitorFn, lvl, agentInfo, map[string]uint64{})
if err != nil {
return nil, fmt.Errorf("failed to render components: %w", err)
}

return comps, nil
}

func getMonitoringFn(ctx context.Context, logger *logger.Logger, cfg map[string]interface{}, otelCfg *confmap.Conf) (component.GenerateMonitoringCfgFn, error) {
config, err := config.NewConfigFrom(cfg)
if err != nil {
return nil, err
}

agentCfg := configuration.DefaultConfiguration()
if err := config.UnpackTo(agentCfg); err != nil {
return nil, err
}

agentInfo, err := info.NewAgentInfoWithLog(ctx, "error", false)
if err != nil {
return nil, fmt.Errorf("could not load agent info: %w", err)
}
otelExecMode := otelconfig.GetExecutionModeFromConfig(logger, config)
isOtelExecModeSubprocess := otelExecMode == manager.SubprocessExecutionMode
monitor := componentmonitoring.New(agentCfg.Settings.V1MonitoringEnabled, agentCfg.Settings.DownloadConfig.OS(), agentCfg.Settings.MonitoringConfig, otelCfg, agentInfo, isOtelExecModeSubprocess)
return monitor.MonitoringConfig, nil
}

func getConfigWithVariables(ctx context.Context, l *logger.Logger, cfgPath string, timeout time.Duration, unprivileged bool) (map[string]interface{}, *confmap.Conf, logp.Level, error) {

cfg, err := operations.LoadFullAgentConfig(ctx, l, cfgPath, true, unprivileged)
if err != nil {
return nil, nil, logp.InfoLevel, err
}
lvl, err := getLogLevel(cfg, cfgPath)
if err != nil {
return nil, nil, logp.InfoLevel, err
}
m, err := cfg.ToMapStr()
if err != nil {
return nil, nil, lvl, err
}
ast, err := transpiler.NewAST(m)
if err != nil {
return nil, nil, lvl, fmt.Errorf("could not create the AST from the configuration: %w", err)
}

// Wait for the variables based on the timeout.
vars, err := vars.WaitForVariables(ctx, l, cfg, timeout)
if err != nil {
return nil, nil, lvl, fmt.Errorf("failed to gather variables: %w", err)
}

// Render the inputs using the discovered inputs.
inputs, ok := transpiler.Lookup(ast, "inputs")
if ok {
renderedInputs, err := transpiler.RenderInputs(inputs, vars)
if err != nil {
return nil, nil, lvl, fmt.Errorf("rendering inputs failed: %w", err)
}
err = transpiler.Insert(ast, renderedInputs, "inputs")
if err != nil {
return nil, nil, lvl, fmt.Errorf("inserting rendered inputs failed: %w", err)
}
}
m, err = ast.Map()
if err != nil {
return nil, nil, lvl, fmt.Errorf("failed to convert ast to map[string]interface{}: %w", err)
}
return m, cfg.OTel, lvl, nil
}

func getLogLevel(rawCfg *config.Config, cfgPath string) (logp.Level, error) {
cfg, err := configuration.NewFromConfig(rawCfg)
if err != nil {
return logger.DefaultLogLevel, errors.New(err,
fmt.Sprintf("could not parse configuration file %s", cfgPath),
errors.TypeFilesystem,
errors.M(errors.MetaKeyPath, cfgPath))
}
if cfg.Settings.LoggingConfig != nil {
return cfg.Settings.LoggingConfig.Level, nil
}
return logger.DefaultLogLevel, nil
}

func printComponents(
components []component.Component,
blocked []component.Component,
Expand Down
Loading