Skip to content

Commit

Permalink
[8.14](backport #4965) "install -f" uses exec to uninstall an existin…
Browse files Browse the repository at this point in the history
…g agent (#5021)

* "install -f" uses exec to uninstall an existing agent (#4965)

* Add explicit check for token and tamper protection in Uninstall func

* fix typo

* Load features from config, fix protection flag load

* Change approach to execute elastic-agent uninstall command

Change the approach that is taken when "elastic-agent install -f" is ran
to use an exec to run "elastic-agent uninstall -f" in cases where the
agent is installed. this allows the process that runs the uninstall to
use all the correct path values for the installed agent instead of the
values associated with the binary that the install command is ran from.

* Add e2e test

* lookup agent binary on path, fix test

* fix test

* Add flag that preserves old approach

* fix typo

* change args format in test

* Use same fixture

* Hide new option

---------

Co-authored-by: Julien Lind <[email protected]>
(cherry picked from commit 1d7b865)

# Conflicts:
#	internal/pkg/agent/cmd/install.go
#	testing/integration/endpoint_security_test.go

* cleanup

---------

Co-authored-by: Michel Laterman <[email protected]>
Co-authored-by: michel-laterman <[email protected]>
  • Loading branch information
3 people authored Jul 5, 2024
1 parent 3d9e68f commit 4beef2c
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 93 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user’s deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: bug

# Change summary; a 80ish characters long description of the change.
summary: Use installed agent to uninstall itself when install -f is used.

# 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: |
When using "elastic-agent install -f", the agent will exec "elastic-agent uninstall -f"
using the agent found in the system's path. This ensures all path references are correctly
loaded and tamper protection errors will cause the install attempt to fail.
# Affected component; a word indicating the component this changeset affects.
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/owner/repo/1234

# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
issue: https://github.com/elastic/elastic-agent/issues/4506
53 changes: 51 additions & 2 deletions internal/pkg/agent/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import (
)

const (
flagInstallBasePath = "base-path"
flagInstallUnprivileged = "unprivileged"
flagInstallBasePath = "base-path"
flagInstallUnprivileged = "unprivileged"
flagInstallRunUninstallFromBinary = "run-uninstall-from-binary"
)

func newInstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command {
Expand All @@ -49,6 +50,9 @@ would like the Agent to operate.
cmd.Flags().String(flagInstallBasePath, paths.DefaultBasePath, "The path where the Elastic Agent will be installed. It must be an absolute path.")
cmd.Flags().Bool(flagInstallUnprivileged, false, "Installed Elastic Agent will create an 'elastic-agent' user and run as that user. (experimental)")
_ = cmd.Flags().MarkHidden(flagInstallUnprivileged) // Hidden until fully supported

cmd.Flags().Bool(flagInstallRunUninstallFromBinary, false, "Run the uninstall command from this binary instead of using the binary found in the system's path.")
_ = cmd.Flags().MarkHidden(flagInstallRunUninstallFromBinary) // For internal use only.
addEnrollFlags(cmd)

return cmd
Expand Down Expand Up @@ -88,6 +92,11 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
return fmt.Errorf("already installed at: %s", topPath)
}

runUninstallBinary, _ := cmd.Flags().GetBool(flagInstallRunUninstallFromBinary)
if status == install.Installed && force && runUninstallBinary {
fmt.Fprintln(streams.Out, "Uninstall will not be ran from the agent installed in system path, components may persist.")
}

nonInteractive, _ := cmd.Flags().GetBool("non-interactive")
if nonInteractive {
fmt.Fprintln(streams.Out, "Installing in non-interactive mode.")
Expand Down Expand Up @@ -199,6 +208,24 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {

var ownership utils.FileOwner
cfgFile := paths.ConfigFile()
if status == install.Installed {
// Uninstall the agent
progBar.Describe("Uninstalling current Elastic Agent")
if !runUninstallBinary {
err := execUninstall(streams)
if err != nil {
progBar.Describe("Uninstall failed")
return err
}
} else {
err := install.Uninstall(cfgFile, topPath, "", log, progBar)
if err != nil {
progBar.Describe("Uninstall from binary failed")
return err
}
}
progBar.Describe("Successfully uninstalled Elastic Agent")
}
if status != install.PackageInstall {
ownership, err = install.Install(cfgFile, topPath, unprivileged, log, progBar, streams)
if err != nil {
Expand Down Expand Up @@ -278,3 +305,25 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
fmt.Fprint(streams.Out, "\nElastic Agent has been successfully installed.\n")
return nil
}

// execUninstall execs "elastic-agent uninstall --force" from the elastic agent installed on the system (found in PATH)
func execUninstall(streams *cli.IOStreams) error {
args := []string{
"uninstall",
"--force",
}
execPath, err := exec.LookPath(paths.BinaryName)
if err != nil {
return fmt.Errorf("unable to find %s on path: %w", paths.BinaryName, err)
}
uninstall := exec.Command(execPath, args...)
uninstall.Stdout = streams.Out
uninstall.Stderr = streams.Err
if err := uninstall.Start(); err != nil {
return fmt.Errorf("unable to start elastic-agent uninstall: %w", err)
}
if err := uninstall.Wait(); err != nil {
return fmt.Errorf("failed to uninstall elastic-agent: %w", err)
}
return nil
}
20 changes: 0 additions & 20 deletions internal/pkg/agent/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,6 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p
return utils.FileOwner{}, errors.New(err, "failed to discover the source directory for installation", errors.TypeFilesystem)
}

// We only uninstall Agent if it is currently installed.
status, _ := Status(topPath)
if status == Installed {
// Uninstall current installation
//
// There is no uninstall token for "install" command.
// Uninstall will fail on protected agent.
// The protected Agent will need to be uninstalled first before it can be installed.
pt.Describe("Uninstalling current Elastic Agent")
err = Uninstall(cfgFile, topPath, "", log, pt)
if err != nil {
pt.Describe("Failed to uninstall current Elastic Agent")
return utils.FileOwner{}, errors.New(
err,
fmt.Sprintf("failed to uninstall Agent at (%s)", filepath.Dir(topPath)),
errors.M("directory", filepath.Dir(topPath)))
}
pt.Describe("Successfully uninstalled current Elastic Agent")
}

var ownership utils.FileOwner
username := ""
groupName := ""
Expand Down
167 changes: 96 additions & 71 deletions testing/integration/endpoint_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,10 @@ func TestInstallWithEndpointSecurityAndRemoveEndpointIntegration(t *testing.T) {
}
}

// buildPolicyWithTamperProtection helper function to build the policy request with or without tamper protection
func buildPolicyWithTamperProtection(policy kibana.AgentPolicy, protected bool) kibana.AgentPolicy {
if protected {
policy.AgentFeatures = append(policy.AgentFeatures, map[string]interface{}{
"name": "tamper_protection",
"enabled": true,
})
}
policy.IsProtected = protected
return policy
}

func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.Info, protected bool) {
deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := testcontext.WithDeadline(t, context.Background(), deadline)
defer cancel()
// installSecurityAgent is a helper function to install an elastic-agent in priviliged mode with the force+non-interactve flags.
// the policy the agent is enrolled with can have protection enabled if passed
func installSecurityAgent(ctx context.Context, t *testing.T, info *define.Info, protected bool) (*atesting.Fixture, kibana.PolicyResponse) {
t.Helper()

// Get path to agent executable.
fixture, err := define.NewFixture(t, define.Version())
Expand Down Expand Up @@ -194,6 +182,27 @@ func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.I
policy, err := tools.InstallAgentWithPolicy(ctx, t,
installOpts, fixture, info.KibanaClient, createPolicyReq)
require.NoError(t, err, "failed to install agent with policy")
return fixture, policy
}

// buildPolicyWithTamperProtection helper function to build the policy request with or without tamper protection
func buildPolicyWithTamperProtection(policy kibana.AgentPolicy, protected bool) kibana.AgentPolicy {
if protected {
policy.AgentFeatures = append(policy.AgentFeatures, map[string]interface{}{
"name": "tamper_protection",
"enabled": true,
})
}
policy.IsProtected = protected
return policy
}

func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.Info, protected bool) {
deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := testcontext.WithDeadline(t, context.Background(), deadline)
defer cancel()

fixture, policy := installSecurityAgent(ctx, t, info, protected)

t.Cleanup(func() {
t.Log("Un-enrolling Elastic Agent...")
Expand Down Expand Up @@ -225,39 +234,13 @@ func testInstallAndCLIUninstallWithEndpointSecurity(t *testing.T, info *define.I
}

func testInstallAndUnenrollWithEndpointSecurity(t *testing.T, info *define.Info, protected bool) {
// Get path to agent executable.
fixture, err := define.NewFixture(t, define.Version())
require.NoError(t, err)

t.Log("Enrolling the agent in Fleet")
policyUUID := uuid.New().String()
createPolicyReq := buildPolicyWithTamperProtection(
kibana.AgentPolicy{
Name: "test-policy-" + policyUUID,
Namespace: "default",
Description: "Test policy " + policyUUID,
MonitoringEnabled: []kibana.MonitoringEnabledOption{
kibana.MonitoringEnabledLogs,
kibana.MonitoringEnabledMetrics,
},
},
protected,
)

installOpts := atesting.InstallOpts{
NonInteractive: true,
Force: true,
Privileged: true,
}

ctx, cn := testcontext.WithDeadline(t, context.Background(), time.Now().Add(10*time.Minute))
defer cn()

policy, err := tools.InstallAgentWithPolicy(ctx, t, installOpts, fixture, info.KibanaClient, createPolicyReq)
require.NoError(t, err)
fixture, policy := installSecurityAgent(ctx, t, info, protected)

t.Log("Installing Elastic Defend")
_, err = installElasticDefendPackage(t, info, policy.ID)
_, err := installElasticDefendPackage(t, info, policy.ID)
require.NoError(t, err)

t.Log("Polling for endpoint-security to become Healthy")
Expand Down Expand Up @@ -338,36 +321,10 @@ func testInstallAndUnenrollWithEndpointSecurity(t *testing.T, info *define.Info,
}

func testInstallWithEndpointSecurityAndRemoveEndpointIntegration(t *testing.T, info *define.Info, protected bool) {
// Get path to agent executable.
fixture, err := define.NewFixture(t, define.Version())
require.NoError(t, err)

t.Log("Enrolling the agent in Fleet")
policyUUID := uuid.New().String()
createPolicyReq := buildPolicyWithTamperProtection(
kibana.AgentPolicy{
Name: "test-policy-" + policyUUID,
Namespace: "default",
Description: "Test policy " + policyUUID,
MonitoringEnabled: []kibana.MonitoringEnabledOption{
kibana.MonitoringEnabledLogs,
kibana.MonitoringEnabledMetrics,
},
},
protected,
)

installOpts := atesting.InstallOpts{
NonInteractive: true,
Force: true,
Privileged: true,
}

ctx, cn := testcontext.WithDeadline(t, context.Background(), time.Now().Add(10*time.Minute))
defer cn()

policy, err := tools.InstallAgentWithPolicy(ctx, t, installOpts, fixture, info.KibanaClient, createPolicyReq)
require.NoError(t, err)
fixture, policy := installSecurityAgent(ctx, t, info, protected)

t.Log("Installing Elastic Defend")
pkgPolicyResp, err := installElasticDefendPackage(t, info, policy.ID)
Expand Down Expand Up @@ -914,3 +871,71 @@ func agentIsHealthyNoEndpoint(t *testing.T, ctx context.Context, agentClient cli

return true
}

// TestForceInstallOverProtectedPolicy tests that running `elastic-agent install -f`
// when an installed agent is running a policy with tamper protection enabled fails.
func TestForceInstallOverProtectedPolicy(t *testing.T) {
info := define.Require(t, define.Requirements{
Group: Fleet,
Stack: &define.Stack{},
Local: false, // requires Agent installation
Sudo: true, // requires Agent installation
OS: []define.OS{
{Type: define.Linux},
},
})

deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := testcontext.WithDeadline(t, context.Background(), deadline)
defer cancel()

fixture, policy := installSecurityAgent(ctx, t, info, true)

t.Cleanup(func() {
t.Log("Un-enrolling Elastic Agent...")
// Use a separate context as the one in the test body will have been cancelled at this point.
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), time.Minute)
defer cleanupCancel()
assert.NoError(t, fleettools.UnEnrollAgent(cleanupCtx, info.KibanaClient, policy.ID))
})

t.Log("Installing Elastic Defend")
pkgPolicyResp, err := installElasticDefendPackage(t, info, policy.ID)
require.NoErrorf(t, err, "Policy Response was: %v", pkgPolicyResp)

t.Log("Polling for endpoint-security to become Healthy")
ctx, cancel = context.WithTimeout(ctx, endpointHealthPollingTimeout)
defer cancel()

agentClient := fixture.Client()
err = agentClient.Connect(ctx)
require.NoError(t, err, "could not connect to local agent")

require.Eventually(t,
func() bool { return agentAndEndpointAreHealthy(t, ctx, agentClient) },
endpointHealthPollingTimeout,
time.Second,
"Endpoint component or units are not healthy.",
)
t.Log("Verified endpoint component and units are healthy")

t.Log("Run elastic-agent install -f...")
// We use the same policy with tamper protection enabled for this test and expect it to fail.
token, err := info.KibanaClient.CreateEnrollmentAPIKey(ctx, kibana.CreateEnrollmentAPIKeyRequest{
PolicyID: policy.ID,
})
require.NoError(t, err)
url, err := fleettools.DefaultURL(ctx, info.KibanaClient)
require.NoError(t, err)

args := []string{
"install",
"--force",
"--url",
url,
"--enrollment-token",
token.APIKey,
}
out, err := fixture.Exec(ctx, args)
require.Errorf(t, err, "No error detected, command output: %s", out)
}

0 comments on commit 4beef2c

Please sign in to comment.