Skip to content

Commit

Permalink
Add unprivileged and privileged subcommand to Elastic Agent (#4621)
Browse files Browse the repository at this point in the history
* Work on privileged/unprivileged command.

* Add integration tests for switching between unprivileged and privileged mode.

* Fix upstream rename.

* Add changelog.

* Switch to new install privileged/unprivileged checks.

* Adjust FixPermissions to take ownership back to Administrators.

* Code review feedback.

* Fix service component check. Support switching on macOS.

* Fix lint.

* Update to constant.

* Add tests for unprivileged switch failure with endpoint installed.

* Fix runtime check to keep runtime spec.

* Fix test contains.

* Only run test on linux.

* Linux only, more.
  • Loading branch information
blakerouse authored Jun 12, 2024
1 parent c45842a commit 4f0d26b
Show file tree
Hide file tree
Showing 26 changed files with 1,179 additions and 225 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user’s deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: feature

# Change summary; a 80ish characters long description of the change.
summary: Add unprivileged and privileged switch commands

# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
description: |
Adds ability to switch between privileged and unprivileged mode using the privileged and unprivileged
subcommands respectively.
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
component:

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

# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
issue: https://github.com/elastic/ingest-dev/issues/2790
2 changes: 2 additions & 0 deletions internal/pkg/agent/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command {
cmd.AddCommand(newUpgradeCommandWithArgs(args, streams))
cmd.AddCommand(newEnrollCommandWithArgs(args, streams))
cmd.AddCommand(newInspectCommandWithArgs(args, streams))
cmd.AddCommand(newPrivilegedCommandWithArgs(args, streams))
cmd.AddCommand(newUnprivilegedCommandWithArgs(args, streams))
cmd.AddCommand(newWatchCommandWithArgs(args, streams))
cmd.AddCommand(newContainerCommand(args, streams))
cmd.AddCommand(newStatusCommand(args, streams))
Expand Down
51 changes: 2 additions & 49 deletions internal/pkg/agent/cmd/enroll_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/elastic/elastic-agent/internal/pkg/release"
"github.com/elastic/elastic-agent/internal/pkg/remote"
"github.com/elastic/elastic-agent/pkg/control/v2/client"
"github.com/elastic/elastic-agent/pkg/control/v2/client/wait"
"github.com/elastic/elastic-agent/pkg/core/logger"
"github.com/elastic/elastic-agent/pkg/core/process"
"github.com/elastic/elastic-agent/pkg/utils"
Expand Down Expand Up @@ -335,7 +336,7 @@ func (c *enrollCmd) fleetServerBootstrap(ctx context.Context, persistentConfig m
if err != nil {
if !c.options.FleetServer.SpawnAgent {
// wait longer to try and communicate with the Elastic Agent
err = waitForAgent(ctx, c.options.DaemonTimeout)
err = wait.ForAgent(ctx, c.options.DaemonTimeout)
if err != nil {
return "", errors.New("failed to communicate with elastic-agent daemon; is elastic-agent running?")
}
Expand Down Expand Up @@ -722,54 +723,6 @@ type waitResult struct {
err error
}

func waitForAgent(ctx context.Context, timeout time.Duration) error {
if timeout == 0 {
timeout = 1 * time.Minute
}
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
maxBackoff := timeout
if maxBackoff <= 0 {
// indefinite timeout
maxBackoff = 10 * time.Minute
}

resChan := make(chan waitResult)
innerCtx, innerCancel := context.WithCancel(context.Background())
defer innerCancel()
go func() {
backOff := expBackoffWithContext(innerCtx, 1*time.Second, maxBackoff)
for {
backOff.Wait()
_, err := getDaemonState(innerCtx)
if errors.Is(err, context.Canceled) {
resChan <- waitResult{err: err}
return
}
if err == nil {
resChan <- waitResult{}
break
}
}
}()

var res waitResult
select {
case <-ctx.Done():
innerCancel()
res = <-resChan
case res = <-resChan:
}

if res.err != nil {
return res.err
}
return nil
}

func waitForFleetServer(ctx context.Context, agentSubproc <-chan *os.ProcessState, log *logger.Logger, timeout time.Duration) (string, error) {
if timeout == 0 {
timeout = 2 * time.Minute
Expand Down
76 changes: 43 additions & 33 deletions internal/pkg/agent/cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,43 +252,12 @@ func inspectComponents(ctx context.Context, cfgPath string, opts inspectComponen
return err
}

// Load the requirements before trying to load the configuration. These should always load
// even if the configuration is wrong.
platform, err := component.LoadPlatformDetail()
if err != nil {
return fmt.Errorf("failed to gather system information: %w", err)
}
specs, err := component.LoadRuntimeSpecs(paths.Components(), platform)
if err != nil {
return fmt.Errorf("failed to detect inputs and outputs: %w", err)
}

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

m, lvl, err := getConfigWithVariables(ctx, l, cfgPath, opts.variablesWait, !isAdmin)
comps, err := getComponentsFromPolicy(ctx, l, cfgPath, opts.variablesWait)
if err != nil {
// error already includes the context
return err
}

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

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

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

// Hide configuration unless toggled on.
if !opts.showConfig {
for i, comp := range comps {
Expand Down Expand Up @@ -349,6 +318,47 @@ 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, lvl, err := getConfigWithVariables(ctx, l, cfgPath, variablesWait, !isAdmin)
if err != nil {
return nil, err
}

monitorFn, err := getMonitoringFn(ctx, m)
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)
if err != nil {
return nil, fmt.Errorf("failed to render components: %w", err)
}

return comps, nil
}

func getMonitoringFn(ctx context.Context, cfg map[string]interface{}) (component.GenerateMonitoringCfgFn, error) {
config, err := config.NewConfigFrom(cfg)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/agent/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
defer func() {
if err != nil {
progBar.Describe("Stopping Service")
innerErr := install.StopService(topPath)
innerErr := install.StopService(topPath, install.DefaultStopTimeout, install.DefaultStopInterval)
if innerErr != nil {
progBar.Describe("Failed to Stop Service")
} else {
Expand Down
95 changes: 95 additions & 0 deletions internal/pkg/agent/cmd/privileged.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package cmd

import (
"context"
"errors"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
"github.com/elastic/elastic-agent/internal/pkg/agent/install"
"github.com/elastic/elastic-agent/internal/pkg/cli"
"github.com/elastic/elastic-agent/pkg/control/v2/client/wait"
"github.com/elastic/elastic-agent/pkg/utils"
)

func newPrivilegedCommandWithArgs(s []string, streams *cli.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "privileged",
Short: "Switch installed Elastic Agent to run as privileged",
Long: `This command converts the installed Elastic Agent from running unprivileged to running as privileged.
By default this command will ask or a confirmation before making this change. You can bypass the confirmation request
using the -f flag. This is not a zero downtime operation and will always stop the running Elastic Agent (if running).
It is possible that loss of metrics, logs, or data could occur during this window of time. The Elastic Agent
daemon will always be started (even if it was off to start). In the case that the Elastic Agent is already running
privileged it will still perform all the same work, including stopping and starting the Elastic Agent.
`,
Args: cobra.ExactArgs(0),
Run: func(c *cobra.Command, args []string) {
if err := privilegedCmd(streams, c); err != nil {
fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage())
os.Exit(1)
}
},
}

cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation")
cmd.Flags().DurationP("daemon-timeout", "", 0, "Timeout waiting for Elastic Agent daemon restart after the change is applied (-1 = no wait)")

return cmd
}

func privilegedCmd(streams *cli.IOStreams, cmd *cobra.Command) (err error) {
isAdmin, err := utils.HasRoot()
if err != nil {
return fmt.Errorf("unable to perform privileged command while checking for root/Administrator rights: %w", err)
}
if !isAdmin {
return fmt.Errorf("unable to perform privileged command, not executed with %s permissions", utils.PermissionUser)
}

topPath := paths.Top()
daemonTimeout, _ := cmd.Flags().GetDuration("daemon-timeout")
force, _ := cmd.Flags().GetBool("force")
if !force {
confirm, err := cli.Confirm("This will restart the running Elastic Agent and convert it to run in privileged mode. Do you want to continue?", true)
if err != nil {
return fmt.Errorf("problem reading prompt response")
}
if !confirm {
return fmt.Errorf("unprivileged switch was cancelled by the user")
}
}

pt := install.CreateAndStartNewSpinner(streams.Out, "Converting Elastic Agent to privileged...")
err = install.SwitchExecutingMode(topPath, pt, "", "")
if err != nil {
// error already adds context
return err
}

// wait for the service
if daemonTimeout >= 0 {
pt.Describe("Waiting for running service")
ctx := handleSignal(context.Background()) // allowed to be cancelled
err = wait.ForAgent(ctx, daemonTimeout)
if err != nil {
if errors.Is(err, context.Canceled) {
pt.Describe("Cancelled waiting for running service")
return nil
}
pt.Describe("Failed waiting for running service")
return err
}
pt.Describe("Service is up and running")
}

return nil
}
Loading

0 comments on commit 4f0d26b

Please sign in to comment.