From 602a2b4587a8050b38881ad9e5811be79b4562c1 Mon Sep 17 00:00:00 2001 From: Blake Rouse Date: Fri, 5 Jan 2024 16:35:17 -0500 Subject: [PATCH] [Unprivileged] Adjust the control socket path add `--path.socket` option (#3909) --- ...-path-into-the-installation-directory.yaml | 32 ++++++++ .../agent/application/info/agent_metadata.go | 7 +- internal/pkg/agent/application/info/state.go | 38 --------- .../pkg/agent/application/info/state_unix.go | 22 ----- .../pkg/agent/application/paths/common.go | 81 ++++++++++++++++--- internal/pkg/agent/application/paths/paths.go | 12 --- .../agent/application/paths/paths_darwin.go | 14 +--- .../agent/application/paths/paths_linux.go | 10 ++- .../state_windows.go => paths/paths_unix.go} | 14 ++-- .../agent/application/paths/paths_windows.go | 33 ++++++-- .../pkg/agent/application/upgrade/upgrade.go | 2 +- internal/pkg/agent/cmd/common.go | 1 + internal/pkg/agent/cmd/container.go | 17 ++-- internal/pkg/agent/cmd/enroll_cmd.go | 2 +- internal/pkg/agent/cmd/install.go | 5 -- internal/pkg/agent/cmd/run.go | 52 ++++++++++-- internal/pkg/agent/cmd/uninstall.go | 3 +- internal/pkg/agent/install/install.go | 24 +++--- internal/pkg/agent/install/install_unix.go | 21 +---- internal/pkg/agent/install/install_windows.go | 5 +- pkg/control/addr.go | 36 ++++----- pkg/control/addr_common.go | 41 ---------- pkg/control/addr_windows.go | 29 ------- pkg/control/v1/client/dial_windows.go | 2 +- pkg/control/v2/client/dial_windows.go | 2 +- pkg/control/v2/server/listener.go | 2 +- pkg/control/v2/server/listener_windows.go | 8 +- pkg/control/v2/server/server.go | 1 + pkg/testing/fixture_install.go | 12 ++- .../integration/install_unprivileged_test.go | 7 +- 30 files changed, 269 insertions(+), 266 deletions(-) create mode 100644 changelog/fragments/1702501610-Adjust-control-socket-path-into-the-installation-directory.yaml delete mode 100644 internal/pkg/agent/application/info/state.go delete mode 100644 internal/pkg/agent/application/info/state_unix.go rename internal/pkg/agent/application/{info/state_windows.go => paths/paths_unix.go} (50%) delete mode 100644 pkg/control/addr_common.go delete mode 100644 pkg/control/addr_windows.go diff --git a/changelog/fragments/1702501610-Adjust-control-socket-path-into-the-installation-directory.yaml b/changelog/fragments/1702501610-Adjust-control-socket-path-into-the-installation-directory.yaml new file mode 100644 index 00000000000..7a05df56a69 --- /dev/null +++ b/changelog/fragments/1702501610-Adjust-control-socket-path-into-the-installation-directory.yaml @@ -0,0 +1,32 @@ +# 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: Move control socket path to the installation directory + +# 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: + +# 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/3909 + +# 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/3840 diff --git a/internal/pkg/agent/application/info/agent_metadata.go b/internal/pkg/agent/application/info/agent_metadata.go index 81ad78a834d..d4b26d5f3ca 100644 --- a/internal/pkg/agent/application/info/agent_metadata.go +++ b/internal/pkg/agent/application/info/agent_metadata.go @@ -7,13 +7,14 @@ package info import ( "context" "fmt" + "runtime" "strings" - "github.com/elastic/elastic-agent/pkg/core/logger" - + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/release" + "github.com/elastic/elastic-agent/pkg/core/logger" "github.com/elastic/elastic-agent/pkg/features" "github.com/elastic/go-sysinfo" @@ -168,7 +169,7 @@ func (i *AgentInfo) ECSMetadata(l *logger.Logger) (*ECSMeta, error) { BuildOriginal: release.Info().String(), // only upgradeable if running from Agent installer and running under the // control of the system supervisor (or built specifically with upgrading enabled) - Upgradeable: release.Upgradeable() || (RunningInstalled() && RunningUnderSupervisor()), + Upgradeable: release.Upgradeable() || (paths.RunningInstalled() && RunningUnderSupervisor()), LogLevel: i.LogLevel(), }, }, diff --git a/internal/pkg/agent/application/info/state.go b/internal/pkg/agent/application/info/state.go deleted file mode 100644 index 1f72e04d4fc..00000000000 --- a/internal/pkg/agent/application/info/state.go +++ /dev/null @@ -1,38 +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; -// you may not use this file except in compliance with the Elastic License. - -package info - -import ( - "os" - "path/filepath" - - "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" - "github.com/elastic/elastic-agent/pkg/utils" -) - -// MarkerFileName is the name of the file that's created by -// `elastic-agent install` in the Agent's topPath folder to -// indicate that the Agent executing from the binary under -// the same topPath folder is an installed Agent. -const MarkerFileName = ".installed" - -// RunningInstalled returns true when executing Agent is the installed Agent. -func RunningInstalled() bool { - // Check if install marker created by `elastic-agent install` exists - markerFilePath := filepath.Join(paths.Top(), MarkerFileName) - if _, err := os.Stat(markerFilePath); err != nil { - return false - } - - return true -} - -func CreateInstallMarker(topPath string, ownership utils.FileOwner) error { - markerFilePath := filepath.Join(topPath, MarkerFileName) - if _, err := os.Create(markerFilePath); err != nil { - return err - } - return fixInstallMarkerPermissions(markerFilePath, ownership) -} diff --git a/internal/pkg/agent/application/info/state_unix.go b/internal/pkg/agent/application/info/state_unix.go deleted file mode 100644 index 23f091aa4bb..00000000000 --- a/internal/pkg/agent/application/info/state_unix.go +++ /dev/null @@ -1,22 +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; -// you may not use this file except in compliance with the Elastic License. - -//go:build !windows - -package info - -import ( - "fmt" - "os" - - "github.com/elastic/elastic-agent/pkg/utils" -) - -func fixInstallMarkerPermissions(markerFilePath string, ownership utils.FileOwner) error { - err := os.Chown(markerFilePath, ownership.UID, ownership.GID) - if err != nil { - return fmt.Errorf("failed to chown %d:%d %s: %w", ownership.UID, ownership.GID, markerFilePath, err) - } - return nil -} diff --git a/internal/pkg/agent/application/paths/common.go b/internal/pkg/agent/application/paths/common.go index 64634db8094..61ff0bcdc49 100644 --- a/internal/pkg/agent/application/paths/common.go +++ b/internal/pkg/agent/application/paths/common.go @@ -14,15 +14,30 @@ import ( "sync" "github.com/elastic/elastic-agent/internal/pkg/release" + "github.com/elastic/elastic-agent/pkg/utils" ) const ( // DefaultConfigName is the default name of the configuration file. DefaultConfigName = "elastic-agent.yml" + // AgentLockFileName is the name of the overall Elastic Agent file lock. AgentLockFileName = "agent.lock" - tempSubdir = "tmp" - tempSubdirPerms = 0o770 + + // ControlSocketName is the control socket name. + ControlSocketName = "elastic-agent.sock" + + // WindowsControlSocketInstalledPath is the control socket path used when installed on Windows. + WindowsControlSocketInstalledPath = `npipe:///elastic-agent-system` + + // MarkerFileName is the name of the file that's created by + // `elastic-agent install` in the Agent's topPath folder to + // indicate that the Agent executing from the binary under + // the same topPath folder is an installed Agent. + MarkerFileName = ".installed" + + tempSubdir = "tmp" + tempSubdirPerms = 0o770 darwin = "darwin" ) @@ -31,21 +46,23 @@ const ( var ExternalInputsPattern = filepath.Join("inputs.d", "*.yml") var ( - topPath string - configPath string - configFilePath string - logsPath string - downloadsPath string - componentsPath string - installPath string - unversionedHome bool - tmpCreator sync.Once + topPath string + configPath string + configFilePath string + logsPath string + downloadsPath string + componentsPath string + installPath string + controlSocketPath string + unversionedHome bool + tmpCreator sync.Once ) func init() { topPath = initialTop() configPath = topPath logsPath = topPath + controlSocketPath = initialControlSocketPath(topPath) unversionedHome = false // only versioned by container subcommand // these should never change @@ -61,6 +78,7 @@ func init() { fs.StringVar(&configFilePath, "c", DefaultConfigName, "Configuration file, relative to path.config") fs.StringVar(&logsPath, "path.logs", logsPath, "Logs path contains Agent log output") fs.StringVar(&installPath, "path.install", installPath, "DEPRECATED, setting this flag has no effect since v8.6.0") + fs.StringVar(&controlSocketPath, "path.socket", controlSocketPath, "Control protocol socket path for the Agent") // enable user to download update artifacts to alternative place // TODO: remove path.downloads support on next major (this can be configured using `agent.download.targetDirectory`) @@ -199,6 +217,18 @@ func SetInstall(path string) { installPath = path } +// ControlSocket returns the control socket directory for Agent +func ControlSocket() string { + return controlSocketPath +} + +// SetControlSocket overrides the ControlSocket path. +// +// Used by the container subcommand to adjust the control socket path. +func SetControlSocket(path string) { + controlSocketPath = path +} + // initialTop returns the initial top-level path for the binary // // When nested in top-level/data/elastic-agent-${hash}/ the result is top-level/. @@ -267,3 +297,32 @@ func InstallPath(basePath string) string { func TopBinaryPath() string { return filepath.Join(Top(), BinaryName) } + +// RunningInstalled returns true when executing Agent is the installed Agent. +func RunningInstalled() bool { + // Check if install marker created by `elastic-agent install` exists + markerFilePath := filepath.Join(Top(), MarkerFileName) + if _, err := os.Stat(markerFilePath); err != nil { + return false + } + return true +} + +// ControlSocketFromPath returns the control socket path for an Elastic Agent running +// on the defined platform, and its executing directory. +func ControlSocketFromPath(platform string, path string) string { + // socket should be inside this directory + socketPath := filepath.Join(path, ControlSocketName) + if platform == "windows" { + // on windows the control socket always uses the fallback + return utils.SocketURLWithFallback(socketPath, path) + } + unixSocket := fmt.Sprintf("unix://%s", socketPath) + if len(unixSocket) < 104 { + // small enough to fit + return unixSocket + } + // place in global /tmp to ensure that its small enough to fit; current path is way to long + // for it to be used, but needs to be unique per Agent (in the case that multiple are running) + return utils.SocketURLWithFallback(socketPath, path) +} diff --git a/internal/pkg/agent/application/paths/paths.go b/internal/pkg/agent/application/paths/paths.go index af500b46695..6be701c6fa8 100644 --- a/internal/pkg/agent/application/paths/paths.go +++ b/internal/pkg/agent/application/paths/paths.go @@ -14,18 +14,6 @@ const ( // for installing Elastic Agent's files. DefaultBasePath = "/opt" - // ControlSocketPath is the control socket path used when installed. - ControlSocketPath = "unix:///run/elastic-agent.sock" - - // ControlSocketUnprivilegedPath is the control socket path used when installed as non-root. - // This must exist inside of a directory in '/run/' because the permissions need to be set - // on that directory during installation time, because once the service is spawned it will not - // have permissions to create the socket in the '/run/' directory. - ControlSocketUnprivilegedPath = "unix:///run/elastic-agent/elastic-agent.sock" - - // ShipperSocketPipePattern is the socket path used when installed for a shipper pipe. - ShipperSocketPipePattern = "unix:///run/elastic-agent-%s-pipe.sock" - // ServiceName is the service name when installed. ServiceName = "elastic-agent" diff --git a/internal/pkg/agent/application/paths/paths_darwin.go b/internal/pkg/agent/application/paths/paths_darwin.go index 54c310b8a4d..3ecbfef96e1 100644 --- a/internal/pkg/agent/application/paths/paths_darwin.go +++ b/internal/pkg/agent/application/paths/paths_darwin.go @@ -14,17 +14,9 @@ const ( // for installing Elastic Agent's files. DefaultBasePath = "/Library" - // ControlSocketPath is the control socket path used when installed. - ControlSocketPath = "unix:///var/run/elastic-agent.sock" - - // ControlSocketUnprivilegedPath is the control socket path used when installed as non-root. - // This must exist inside of a directory in '/var/run/' because the permissions need to be set - // on that directory during installation time, because once the service is spawned it will not - // have permissions to create the socket in the '/var/run/' directory. - ControlSocketUnprivilegedPath = "unix:///var/run/elastic-agent/elastic-agent.sock" - - // ShipperSocketPipePattern is the socket path used when installed for a shipper pipe. - ShipperSocketPipePattern = "unix:///var/run/elastic-agent-%s-pipe.sock" + // ControlSocketRunSymlink is the path to the symlink that should be + // created to the control socket when Elastic Agent is running with root. + ControlSocketRunSymlink = "/var/run/elastic-agent.sock" // ServiceName is the service name when installed. ServiceName = "co.elastic.elastic-agent" diff --git a/internal/pkg/agent/application/paths/paths_linux.go b/internal/pkg/agent/application/paths/paths_linux.go index dde45307ef7..a2f1ef6dd4a 100644 --- a/internal/pkg/agent/application/paths/paths_linux.go +++ b/internal/pkg/agent/application/paths/paths_linux.go @@ -8,8 +8,14 @@ package paths import "path/filepath" -// defaultAgentVaultPath is the directory for linux where the vault store is located or the -const defaultAgentVaultPath = "vault" +const ( + // ControlSocketRunSymlink is the path to the symlink that should be + // created to the control socket when Elastic Agent is running with root. + ControlSocketRunSymlink = "/run/elastic-agent.sock" + + // defaultAgentVaultPath is the directory for linux where the vault store is located or the + defaultAgentVaultPath = "vault" +) // AgentVaultPath is the directory that contains all the files for the value func AgentVaultPath() string { diff --git a/internal/pkg/agent/application/info/state_windows.go b/internal/pkg/agent/application/paths/paths_unix.go similarity index 50% rename from internal/pkg/agent/application/info/state_windows.go rename to internal/pkg/agent/application/paths/paths_unix.go index 7997c2d0f7d..da905a4bb84 100644 --- a/internal/pkg/agent/application/info/state_windows.go +++ b/internal/pkg/agent/application/paths/paths_unix.go @@ -2,15 +2,17 @@ // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. -//go:build windows +//go:build !windows -package info +package paths import ( - "github.com/elastic/elastic-agent/pkg/utils" + "runtime" ) -func fixInstallMarkerPermissions(markerFilePath string, ownership utils.FileOwner) error { - // TODO(blakerouse): Fix the market permissions on Windows. - return nil +func initialControlSocketPath(topPath string) string { + return ControlSocketFromPath(runtime.GOOS, topPath) } + +// ResolveControlSocket does nothing on non-Windows hosts. +func ResolveControlSocket() {} diff --git a/internal/pkg/agent/application/paths/paths_windows.go b/internal/pkg/agent/application/paths/paths_windows.go index b54b1ebe898..a6f7b14afe7 100644 --- a/internal/pkg/agent/application/paths/paths_windows.go +++ b/internal/pkg/agent/application/paths/paths_windows.go @@ -8,6 +8,7 @@ package paths import ( "path/filepath" + "runtime" "strings" ) @@ -19,14 +20,8 @@ const ( // for installing Elastic Agent's files. DefaultBasePath = `C:\Program Files` - // ControlSocketPath is the control socket path used when installed. - ControlSocketPath = `\\.\pipe\elastic-agent-system` - - // ControlSocketUnprivilegedPath is the control socket path used when installed as non-root. - ControlSocketUnprivilegedPath = ControlSocketPath - - // ShipperSocketPipePattern is the socket path used when installed for a shipper pipe. - ShipperSocketPipePattern = `\\.\pipe\elastic-agent-%s-pipe.sock` + // ControlSocketRunSymlink is not created on Windows. + ControlSocketRunSymlink = "" // ServiceName is the service name when installed. ServiceName = "Elastic Agent" @@ -50,3 +45,25 @@ func ArePathsEqual(expected, actual string) bool { func AgentVaultPath() string { return filepath.Join(Config(), defaultAgentVaultPath) } + +func initialControlSocketPath(topPath string) string { + // when installed the control address is fixed + if RunningInstalled() { + return WindowsControlSocketInstalledPath + } + return ControlSocketFromPath(runtime.GOOS, topPath) +} + +// ResolveControlSocket updates the control socket path. +// +// Called during the upgrade process from pre-8.8 versions. In pre-8.8 versions the +// RunningInstalled will always be false, even when it is an installed version. Once +// that is fixed from the upgrade process the control socket path needs to be updated. +func ResolveControlSocket() { + currentPath := ControlSocket() + if currentPath == ControlSocketFromPath(runtime.GOOS, topPath) && RunningInstalled() { + // path is not correct being that it's installed + // reset the control socket path to be the installed path + SetControlSocket(WindowsControlSocketInstalledPath) + } +} diff --git a/internal/pkg/agent/application/upgrade/upgrade.go b/internal/pkg/agent/application/upgrade/upgrade.go index 55807b388f8..5fd8d7d989d 100644 --- a/internal/pkg/agent/application/upgrade/upgrade.go +++ b/internal/pkg/agent/application/upgrade/upgrade.go @@ -63,7 +63,7 @@ type Upgrader struct { func IsUpgradeable() bool { // only upgradeable if running from Agent installer and running under the // control of the system supervisor (or built specifically with upgrading enabled) - return release.Upgradeable() || (info.RunningInstalled() && info.RunningUnderSupervisor()) + return release.Upgradeable() || (paths.RunningInstalled() && info.RunningUnderSupervisor()) } // NewUpgrader creates an upgrader which is capable of performing upgrade operation diff --git a/internal/pkg/agent/cmd/common.go b/internal/pkg/agent/cmd/common.go index 864aa900195..f3a00be5cd5 100644 --- a/internal/pkg/agent/cmd/common.go +++ b/internal/pkg/agent/cmd/common.go @@ -58,6 +58,7 @@ func NewCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command { cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.logs")) cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.downloads")) cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.install")) + cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("path.socket")) // logging flags cmd.PersistentFlags().AddGoFlag(flag.CommandLine.Lookup("v")) diff --git a/internal/pkg/agent/cmd/container.go b/internal/pkg/agent/cmd/container.go index 7d3fc1df022..6e4d80dd425 100644 --- a/internal/pkg/agent/cmd/container.go +++ b/internal/pkg/agent/cmd/container.go @@ -172,7 +172,7 @@ func logContainerCmd(streams *cli.IOStreams) error { func containerCmd(streams *cli.IOStreams) error { // set paths early so all action below use the defined paths - if err := setPaths("", "", "", true); err != nil { + if err := setPaths("", "", "", "", true); err != nil { return err } @@ -397,6 +397,7 @@ func buildEnrollArgs(cfg setupConfig, token string, policyID string) ([]string, "--path.home", paths.Top(), // --path.home actually maps to paths.Top() "--path.config", paths.Config(), "--path.logs", paths.Logs(), + "--path.socket", paths.ControlSocket(), "--skip-daemon-reload", } if paths.Downloads() != "" { @@ -748,7 +749,7 @@ func logToStderr(cfg *configuration.Configuration) { } } -func setPaths(statePath, configPath, logsPath string, writePaths bool) error { +func setPaths(statePath, configPath, logsPath, socketPath string, writePaths bool) error { statePath = envWithDefault(statePath, "STATE_PATH") if statePath == "" { statePath = defaultStateDirectory @@ -758,6 +759,9 @@ func setPaths(statePath, configPath, logsPath string, writePaths bool) error { if configPath == "" { configPath = statePath } + if socketPath == "" { + socketPath = fmt.Sprintf("unix://%s", filepath.Join(topPath, paths.ControlSocketName)) + } // ensure that the directory and sub-directory data exists if err := os.MkdirAll(topPath, 0755); err != nil { return fmt.Errorf("preparing STATE_PATH(%s) failed: %w", statePath, err) @@ -774,6 +778,7 @@ func setPaths(statePath, configPath, logsPath string, writePaths bool) error { originalTop := paths.Top() paths.SetTop(topPath) paths.SetConfig(configPath) + paths.SetControlSocket(socketPath) // when custom top path is provided the home directory is not versioned paths.SetVersionHome(false) // install path stays on container default mount (otherwise a bind mounted directory could have noexec set) @@ -796,7 +801,7 @@ func setPaths(statePath, configPath, logsPath string, writePaths bool) error { // persist the paths so other commands in the container will use the correct paths if writePaths { - if err := writeContainerPaths(originalTop, statePath, configPath, logsPath); err != nil { + if err := writeContainerPaths(originalTop, statePath, configPath, logsPath, socketPath); err != nil { return err } } @@ -807,9 +812,10 @@ type containerPaths struct { StatePath string `config:"state_path" yaml:"state_path"` ConfigPath string `config:"config_path" yaml:"config_path,omitempty"` LogsPath string `config:"logs_path" yaml:"logs_path,omitempty"` + SocketPath string `config:"socket_path" yaml:"socket_path,omitempty"` } -func writeContainerPaths(original, statePath, configPath, logsPath string) error { +func writeContainerPaths(original, statePath, configPath, logsPath, socketPath string) error { pathFile := filepath.Join(original, "container-paths.yml") fp, err := os.Create(pathFile) if err != nil { @@ -819,6 +825,7 @@ func writeContainerPaths(original, statePath, configPath, logsPath string) error StatePath: statePath, ConfigPath: configPath, LogsPath: logsPath, + SocketPath: socketPath, }) if err != nil { return fmt.Errorf("failed to marshal for %s: %w", pathFile, err) @@ -846,7 +853,7 @@ func tryContainerLoadPaths() error { if err != nil { return fmt.Errorf("failed to unpack %s: %w", pathFile, err) } - return setPaths(paths.StatePath, paths.ConfigPath, paths.LogsPath, false) + return setPaths(paths.StatePath, paths.ConfigPath, paths.LogsPath, paths.SocketPath, false) } func copyFile(destPath string, srcPath string, mode os.FileMode) error { diff --git a/internal/pkg/agent/cmd/enroll_cmd.go b/internal/pkg/agent/cmd/enroll_cmd.go index d3de412ba44..0d36e5cef36 100644 --- a/internal/pkg/agent/cmd/enroll_cmd.go +++ b/internal/pkg/agent/cmd/enroll_cmd.go @@ -629,7 +629,7 @@ func (c *enrollCmd) startAgent(ctx context.Context) (<-chan *os.ProcessState, er args := []string{ "run", "-e", "-c", paths.ConfigFile(), "--path.home", paths.Top(), "--path.config", paths.Config(), - "--path.logs", paths.Logs(), + "--path.logs", paths.Logs(), "--path.socket", paths.ControlSocket(), } if paths.Downloads() != "" { args = append(args, "--path.downloads", paths.Downloads()) diff --git a/internal/pkg/agent/cmd/install.go b/internal/pkg/agent/cmd/install.go index 002a8d1b98a..7a5ee5cacc9 100644 --- a/internal/pkg/agent/cmd/install.go +++ b/internal/pkg/agent/cmd/install.go @@ -15,7 +15,6 @@ import ( "github.com/spf13/cobra" "github.com/elastic/elastic-agent/internal/pkg/agent/application/filelock" - "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/install" "github.com/elastic/elastic-agent/internal/pkg/cli" @@ -265,10 +264,6 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error { progBar.Describe("Enroll Completed") } - if err := info.CreateInstallMarker(topPath, ownership); err != nil { - return fmt.Errorf("failed to create install marker: %w", err) - } - progBar.Describe("Done") _ = progBar.Finish() _ = progBar.Exit() diff --git a/internal/pkg/agent/cmd/run.go b/internal/pkg/agent/cmd/run.go index 63128e38b8c..97bbad3a954 100644 --- a/internal/pkg/agent/cmd/run.go +++ b/internal/pkg/agent/cmd/run.go @@ -7,11 +7,11 @@ package cmd import ( "context" "fmt" - "io/ioutil" "net/url" "os" "os/signal" "path/filepath" + "strings" "syscall" "time" @@ -39,6 +39,7 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/agent/application/upgrade" "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/install" "github.com/elastic/elastic-agent/internal/pkg/agent/migration" "github.com/elastic/elastic-agent/internal/pkg/agent/storage" "github.com/elastic/elastic-agent/internal/pkg/cli" @@ -176,6 +177,12 @@ func runElasticAgent(ctx context.Context, cancel context.CancelFunc, override cf } pathConfigFile := paths.AgentConfigFile() + // try early to check if running as root + isRoot, err := utils.HasRoot() + if err != nil { + return fmt.Errorf("failed to check for root permissions: %w", err) + } + // agent ID needs to stay empty in bootstrap mode createAgentID := true if cfg.Fleet != nil && cfg.Fleet.Server != nil && cfg.Fleet.Server.Bootstrap { @@ -271,7 +278,8 @@ func runElasticAgent(ctx context.Context, cancel context.CancelFunc, override cf diagHooks := diagnostics.GlobalHooks() diagHooks = append(diagHooks, coord.DiagnosticHooks()...) - control := server.New(l.Named("control"), agentInfo, coord, tracer, diagHooks, cfg.Settings.GRPC) + controlLog := l.Named("control") + control := server.New(controlLog, agentInfo, coord, tracer, diagHooks, cfg.Settings.GRPC) // if the configMgr implements the TestModeConfigSetter in means that Elastic Agent is in testing mode and // the configuration will come in over the control protocol, so we set the config setting on the control protocol @@ -287,6 +295,31 @@ func runElasticAgent(ctx context.Context, cancel context.CancelFunc, override cf } defer control.Stop() + // create symlink from /run/elastic-agent.sock to `paths.ControlSocket()` when running as root + // this provides backwards compatibility as the control socket was moved with the addition of --unprivileged + // option during installation + // + // Windows `paths.ControlSocketRunSymlink` is `""` so this is always skipped on Windows. + if isRoot && paths.RunningInstalled() && paths.ControlSocketRunSymlink != "" { + socketPath := strings.TrimPrefix(paths.ControlSocket(), "unix://") + socketLog := controlLog.With("path", socketPath).With("link", paths.ControlSocketRunSymlink) + // ensure it doesn't exist before creating the symlink + if err := os.Remove(paths.ControlSocketRunSymlink); err != nil && !errors.Is(err, os.ErrNotExist) { + socketLog.Errorf("Failed to remove existing control socket symlink %s: %s", paths.ControlSocketRunSymlink, err) + } + if err := os.Symlink(socketPath, paths.ControlSocketRunSymlink); err != nil { + socketLog.Errorf("Failed to create control socket symlink %s -> %s: %s", paths.ControlSocketRunSymlink, socketPath, err) + } else { + socketLog.Infof("Created control socket symlink %s -> %s; allowing unix://%s connection", paths.ControlSocketRunSymlink, socketPath, paths.ControlSocketRunSymlink) + } + defer func() { + // delete the symlink on exit; ignore the error + if err := os.Remove(paths.ControlSocketRunSymlink); err != nil { + socketLog.Errorf("Failed to remove control socket symlink %s: %s", paths.ControlSocketRunSymlink, err) + } + }() + } + appDone := make(chan bool) appErr := make(chan error) // Spawn the main Coordinator goroutine @@ -449,7 +482,7 @@ func tryDelayEnroll(ctx context.Context, logger *logger.Logger, cfg *configurati // no enrollment file exists or failed to stat it; nothing to do return cfg, nil } - contents, err := ioutil.ReadFile(enrollPath) + contents, err := os.ReadFile(enrollPath) if err != nil { return nil, errors.New( err, @@ -618,16 +651,25 @@ func ensureInstallMarkerPresent() error { // Only an installed Elastic Agent can be self-upgraded. So, if the // installation marker file is already present, we're all set. - if info.RunningInstalled() { + if paths.RunningInstalled() { return nil } // Otherwise, we're being upgraded from a version of an installed Agent // that didn't use an installation marker file (that is, before v8.8.0). // So create the file now. - if err := info.CreateInstallMarker(paths.Top(), utils.CurrentFileOwner()); err != nil { + if err := install.CreateInstallMarker(paths.Top(), utils.CurrentFileOwner()); err != nil { return fmt.Errorf("unable to create installation marker file during upgrade: %w", err) } + // In v8.14.0, the control socket was moved to be in the installation path instead at + // a system level location, except on Windows where it remained at `npipe:///elastic-agent-system`. + // For Windows to be able to determine if it is running installed is from the creation of + // `.installed` marker that was not created until v8.8.0. Upgrading from any pre-8.8 version results + // in the `paths.ControlSocket()` in returning the incorrect control socket (only on Windows). + // Now that the install marker has been created we need to ensure that `paths.ControlSocket()` will + // return the correct result. + paths.ResolveControlSocket() + return nil } diff --git a/internal/pkg/agent/cmd/uninstall.go b/internal/pkg/agent/cmd/uninstall.go index 96dc1f4d4ee..34d07c51683 100644 --- a/internal/pkg/agent/cmd/uninstall.go +++ b/internal/pkg/agent/cmd/uninstall.go @@ -10,7 +10,6 @@ import ( "github.com/spf13/cobra" - "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/install" "github.com/elastic/elastic-agent/internal/pkg/cli" @@ -51,7 +50,7 @@ func uninstallCmd(streams *cli.IOStreams, cmd *cobra.Command) error { if status == install.NotInstalled { return fmt.Errorf("not installed") } - if status == install.Installed && !info.RunningInstalled() { + if status == install.Installed && !paths.RunningInstalled() { return fmt.Errorf("can only be uninstalled by executing the installed Elastic Agent at: %s", install.ExecutablePath(paths.Top())) } diff --git a/internal/pkg/agent/install/install.go b/internal/pkg/agent/install/install.go index 4152479fb11..bc5fa26a148 100644 --- a/internal/pkg/agent/install/install.go +++ b/internal/pkg/agent/install/install.go @@ -185,6 +185,11 @@ func Install(cfgFile, topPath string, unprivileged bool, pt *progressbar.Progres } } + // create the install marker + if err := CreateInstallMarker(topPath, ownership); err != nil { + return utils.FileOwner{}, fmt.Errorf("failed to create install marker: %w", err) + } + // post install (per platform) err = postInstall(topPath) if err != nil { @@ -203,17 +208,6 @@ func Install(cfgFile, topPath string, unprivileged bool, pt *progressbar.Progres } } - // create socket path when installing as non-root - // now is the only time to do it while root is available (without doing this it will not be possible - // for the service to create the control socket) - // windows: uses npipe and doesn't need a directory created - if unprivileged { - err = createSocketDir(ownership) - if err != nil { - return ownership, fmt.Errorf("failed to create socket directory: %w", err) - } - } - // install service pt.Describe("Installing service") svc, err := newService(topPath, withUserGroup(username, groupName)) @@ -360,3 +354,11 @@ func hasAllSSDs(block ghw.BlockInfo) bool { return true } + +func CreateInstallMarker(topPath string, ownership utils.FileOwner) error { + markerFilePath := filepath.Join(topPath, paths.MarkerFileName) + if _, err := os.Create(markerFilePath); err != nil { + return err + } + return fixInstallMarkerPermissions(markerFilePath, ownership) +} diff --git a/internal/pkg/agent/install/install_unix.go b/internal/pkg/agent/install/install_unix.go index 9840dd90ead..52f9f92847e 100644 --- a/internal/pkg/agent/install/install_unix.go +++ b/internal/pkg/agent/install/install_unix.go @@ -9,10 +9,7 @@ package install import ( "fmt" "os" - "path/filepath" - "strings" - "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/pkg/utils" ) @@ -22,22 +19,10 @@ func postInstall(topPath string) error { return nil } -// createSocketDir creates the socket directory. -func createSocketDir(ownership utils.FileOwner) error { - path := filepath.Dir(strings.TrimPrefix(paths.ControlSocketUnprivilegedPath, "unix://")) - err := os.MkdirAll(path, 0770) +func fixInstallMarkerPermissions(markerFilePath string, ownership utils.FileOwner) error { + err := os.Chown(markerFilePath, ownership.UID, ownership.GID) if err != nil { - return fmt.Errorf("failed to create path %s: %w", path, err) - } - err = os.Chown(path, ownership.UID, ownership.GID) - if err != nil { - return fmt.Errorf("failed to chown path %s: %w", path, err) - } - // possible that the directory existed, still set the - // permission again to ensure that they are correct - err = os.Chmod(path, 0770) - if err != nil { - return fmt.Errorf("failed to chmod path %s: %w", path, err) + return fmt.Errorf("failed to chown %d:%d %s: %w", ownership.UID, ownership.GID, markerFilePath, err) } return nil } diff --git a/internal/pkg/agent/install/install_windows.go b/internal/pkg/agent/install/install_windows.go index 07b43591906..9822b79d2fd 100644 --- a/internal/pkg/agent/install/install_windows.go +++ b/internal/pkg/agent/install/install_windows.go @@ -46,8 +46,7 @@ func postInstall(topPath string) error { return nil } -// createSocketDir creates the socket directory. -func createSocketDir(ownership utils.FileOwner) error { - // doesn't do anything on windows, no directory is needed. +func fixInstallMarkerPermissions(markerFilePath string, ownership utils.FileOwner) error { + // TODO(blakerouse): Fix the market permissions on Windows. return nil } diff --git a/pkg/control/addr.go b/pkg/control/addr.go index 916b771097d..961f790ad8d 100644 --- a/pkg/control/addr.go +++ b/pkg/control/addr.go @@ -2,37 +2,33 @@ // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. -//go:build !windows - package control import ( - "crypto/sha256" "fmt" "path/filepath" - "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/pkg/utils" ) // Address returns the address to connect to Elastic Agent daemon. func Address() string { - // when installed the control address is fixed - if info.RunningInstalled() { - root, _ := utils.HasRoot() // error is ignored - if root { - return paths.ControlSocketPath - } - return paths.ControlSocketUnprivilegedPath - } + return paths.ControlSocket() +} - // unix socket path must be less than 104 characters - path := fmt.Sprintf("unix://%s.sock", filepath.Join(paths.TempDir(), "elastic-agent-control")) - if len(path) < 104 { - return path +// AddressFromPath returns the connection address for an Elastic Agent running on the defined platform, and its +// executing directory. +func AddressFromPath(platform string, path string) (string, error) { + // elastic-agent will always derive the path to the socket using an absolute path + absDir, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to get absolute path of %s: %w", path, err) + } + // elastic-agent is given a path from the OS that removes all symlinks, without then + // the path to the socket will not be the same. + noSyms, err := filepath.EvalSymlinks(absDir) + if err != nil { + return "", fmt.Errorf("failed to evaluate all symlinks of %s: %w", absDir, err) } - // place in global /tmp to ensure that its small enough to fit; current path is way to long - // for it to be used, but needs to be unique per Agent (in the case that multiple are running) - return fmt.Sprintf(`unix:///tmp/elastic-agent/%x.sock`, sha256.Sum256([]byte(path))) + return paths.ControlSocketFromPath(platform, noSyms), nil } diff --git a/pkg/control/addr_common.go b/pkg/control/addr_common.go deleted file mode 100644 index f9f35e57e35..00000000000 --- a/pkg/control/addr_common.go +++ /dev/null @@ -1,41 +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; -// you may not use this file except in compliance with the Elastic License. - -package control - -import ( - "crypto/sha256" - "fmt" - "path/filepath" -) - -// AddressFromPath returns the connection address for an Elastic Agent running on the defined platform, and its -// executing directory. -func AddressFromPath(platform string, path string) (string, error) { - // elastic-agent will always derive the path to the socket using an absolute path - absDir, err := filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("failed to get absolute path of %s: %w", path, err) - } - // elastic-agent is given a path from the OS that removes all symlinks, without then - // the path to the socket will not be the same. - noSyms, err := filepath.EvalSymlinks(absDir) - if err != nil { - return "", fmt.Errorf("failed to evaluate all symlinks of %s: %w", absDir, err) - } - - dataPath := filepath.Join(noSyms, "data") - if platform == "windows" { - return fmt.Sprintf(`\\.\pipe\elastic-agent-%x`, sha256.Sum256([]byte(dataPath))), nil - } - socketPath := filepath.Join(dataPath, "tmp", "elastic-agent-control") - socketPath = fmt.Sprintf("unix://%s.sock", socketPath) - // unix socket path must be less than 104 characters - if len(socketPath) < 104 { - return socketPath, nil - } - // place in global /tmp to ensure that its small enough to fit; current path is way to long - // for it to be used, but needs to be unique per Agent (in the case that multiple are running) - return fmt.Sprintf(`unix:///tmp/elastic-agent/%x.sock`, sha256.Sum256([]byte(socketPath))), nil -} diff --git a/pkg/control/addr_windows.go b/pkg/control/addr_windows.go deleted file mode 100644 index 71440e70ad6..00000000000 --- a/pkg/control/addr_windows.go +++ /dev/null @@ -1,29 +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; -// you may not use this file except in compliance with the Elastic License. - -//go:build windows - -package control - -import ( - "crypto/sha256" - "fmt" - - "github.com/elastic/elastic-agent/internal/pkg/agent/application/info" - "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" -) - -// Address returns the address to connect to Elastic Agent daemon. -func Address() string { - // when installed the control address is fixed - if info.RunningInstalled() { - return paths.ControlSocketPath - } - - // not install, adjust the path based on data path - data := paths.Data() - // entire string cannot be longer than 256 characters, this forces the - // length to always be 87 characters (but unique per data path) - return fmt.Sprintf(`\\.\pipe\elastic-agent-%x`, sha256.Sum256([]byte(data))) -} diff --git a/pkg/control/v1/client/dial_windows.go b/pkg/control/v1/client/dial_windows.go index d737cc36f53..70f8a3e7f60 100644 --- a/pkg/control/v1/client/dial_windows.go +++ b/pkg/control/v1/client/dial_windows.go @@ -28,5 +28,5 @@ func dialContext(ctx context.Context) (*grpc.ClientConn, error) { } func dialer(ctx context.Context, addr string) (net.Conn, error) { - return npipe.DialContext(addr)(ctx, "", "") + return npipe.DialContext(npipe.TransformString(addr))(ctx, "", "") } diff --git a/pkg/control/v2/client/dial_windows.go b/pkg/control/v2/client/dial_windows.go index 1bd2336d2bb..2f5e8ba2565 100644 --- a/pkg/control/v2/client/dial_windows.go +++ b/pkg/control/v2/client/dial_windows.go @@ -31,5 +31,5 @@ func dialer(ctx context.Context, addr string) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, "tcp", strings.TrimPrefix(addr, "http://")) } - return npipe.DialContext(addr)(ctx, "", "") + return npipe.DialContext(npipe.TransformString(addr))(ctx, "", "") } diff --git a/pkg/control/v2/server/listener.go b/pkg/control/v2/server/listener.go index d8b2982f17c..435dd86ee24 100644 --- a/pkg/control/v2/server/listener.go +++ b/pkg/control/v2/server/listener.go @@ -47,7 +47,7 @@ func createListener(log *logger.Logger) (net.Listener, error) { lis.Close() return nil, err } - return lis, err + return lis, nil } func cleanupListener(log *logger.Logger) { diff --git a/pkg/control/v2/server/listener_windows.go b/pkg/control/v2/server/listener_windows.go index 55e0ecf3cda..6aa82c4561a 100644 --- a/pkg/control/v2/server/listener_windows.go +++ b/pkg/control/v2/server/listener_windows.go @@ -22,9 +22,13 @@ import ( func createListener(log *logger.Logger) (net.Listener, error) { sd, err := securityDescriptor(log) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create security descriptor: %w", err) } - return npipe.NewListener(control.Address(), sd) + lis, err := npipe.NewListener(npipe.TransformString(control.Address()), sd) + if err != nil { + return nil, fmt.Errorf("failed to create npipe listener: %w", err) + } + return lis, nil } func cleanupListener(_ *logger.Logger) { diff --git a/pkg/control/v2/server/server.go b/pkg/control/v2/server/server.go index 44b1bdfca6a..2bfa3b06ec7 100644 --- a/pkg/control/v2/server/server.go +++ b/pkg/control/v2/server/server.go @@ -85,6 +85,7 @@ func (s *Server) Start() error { s.logger.Errorf("unable to create listener: %s", err) return err } + s.logger.With("address", control.Address()).Infof("GRPC control socket listening at %s", control.Address()) s.listener = lis if s.tracer != nil { apmInterceptor := apmgrpc.NewUnaryServerInterceptor(apmgrpc.WithRecovery(), apmgrpc.WithTracer(s.tracer)) diff --git a/pkg/testing/fixture_install.go b/pkg/testing/fixture_install.go index 395833dd226..cfd39d1f4d0 100644 --- a/pkg/testing/fixture_install.go +++ b/pkg/testing/fixture_install.go @@ -116,9 +116,15 @@ func (f *Fixture) Install(ctx context.Context, installOpts *InstallOpts, opts .. } // we just installed agent, the control socket is at a well-known location - socketPath := paths.ControlSocketPath - if installOpts.Unprivileged { - socketPath = paths.ControlSocketUnprivilegedPath + socketPath := fmt.Sprintf("unix://%s", paths.ControlSocketRunSymlink) // use symlink as that works for all versions + if runtime.GOOS == "windows" { + // Windows uses a fixed named pipe, that is always the same. + // It is the same even running in unprivileged mode. + socketPath = paths.WindowsControlSocketInstalledPath + } else if installOpts.Unprivileged { + // Unprivileged versions move the socket to inside the installed directory + // of the Elastic Agent. + socketPath = paths.ControlSocketFromPath(runtime.GOOS, f.workDir) } c := client.New(client.WithAddress(socketPath)) f.setClient(c) diff --git a/testing/integration/install_unprivileged_test.go b/testing/integration/install_unprivileged_test.go index a665a306659..87dd1b0e339 100644 --- a/testing/integration/install_unprivileged_test.go +++ b/testing/integration/install_unprivileged_test.go @@ -12,7 +12,6 @@ import ( "os/exec" "path/filepath" "runtime" - "strings" "syscall" "testing" "time" @@ -176,7 +175,7 @@ func checkInstallUnprivilegedSuccess(t *testing.T, topPath string) { require.NoError(t, err) // Check that the socket is created with the correct permissions. - socketPath := strings.TrimPrefix(paths.ControlSocketUnprivilegedPath, "unix://") + socketPath := filepath.Join(topPath, paths.ControlSocketName) require.Eventuallyf(t, func() bool { _, err = os.Stat(socketPath) return err == nil @@ -198,10 +197,10 @@ func checkInstallUnprivilegedSuccess(t *testing.T, topPath string) { // Executing `elastic-agent status` as the original user should fail, because that // user is not in the 'elastic-agent' group. - originalUser := os.Getenv("USER") + originalUser := os.Getenv("SUDO_USER") if originalUser != "" { cmd := exec.Command("sudo", "-u", originalUser, "elastic-agent", "status") output, err := cmd.CombinedOutput() - require.Error(t, err, "running elastic-agent status should have failed: %s", output) + require.Error(t, err, "running sudo -u %s elastic-agent status should have failed: %s", originalUser, output) } }