Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for pre existing Active Directory user #5988

Merged
merged 34 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5c5763a
unpriviledged ad works'
michalpristas Oct 30, 2024
b6b1e9d
Merge branch 'main' of github.com:elastic/elastic-agent into feat/unp…
michalpristas Nov 5, 2024
4041e3e
Ensure rights for AD users
michalpristas Nov 6, 2024
d4d253f
Merge branch 'main' of github.com:elastic/elastic-agent into feat/unp…
michalpristas Nov 11, 2024
43feb5e
changelog
michalpristas Nov 11, 2024
d115977
Merge branch 'main' of github.com:elastic/elastic-agent into feat/unp…
michalpristas Nov 13, 2024
ab9debd
support for mac and unix
michalpristas Nov 13, 2024
6a2613f
Update internal/pkg/agent/cmd/install.go
michalpristas Nov 13, 2024
233497f
Update internal/pkg/agent/cmd/unprivileged.go
michalpristas Nov 14, 2024
fa3fa8f
Merge branch 'main' into feat/unprivileged/ad
michalpristas Nov 14, 2024
a549377
added e2e tests for darwin and linux
michalpristas Nov 14, 2024
ffdf4a4
reverted sample_test
michalpristas Nov 14, 2024
fb78394
mage fmt
michalpristas Nov 14, 2024
a283ea2
Merge branch 'main' into feat/unprivileged/ad
michalpristas Nov 15, 2024
2bc5531
Merge branch 'main' of github.com:elastic/elastic-agent into feat/unp…
michalpristas Nov 18, 2024
3e9a9ab
Merge branch 'feat/unprivileged/ad' of github.com:michalpristas/elast…
michalpristas Nov 18, 2024
dec27ef
do not require password
michalpristas Nov 18, 2024
7f0ae82
more strict regex
michalpristas Nov 20, 2024
d9b137b
fix description in changelog
michalpristas Nov 20, 2024
111b576
Merge branch 'main' into feat/unprivileged/ad
michalpristas Nov 20, 2024
8c550c4
Merge branch 'main' of github.com:elastic/elastic-agent into feat/unp…
michalpristas Dec 2, 2024
e831bd2
resolved review comments
michalpristas Dec 2, 2024
658d5d9
Merge branch 'feat/unprivileged/ad' of github.com:michalpristas/elast…
michalpristas Dec 2, 2024
13ce0d6
linter
michalpristas Dec 2, 2024
674e92a
handle g115 in user_windows.go
michalpristas Dec 2, 2024
e1590cc
Merge branch 'main' into feat/unprivileged/ad
michalpristas Dec 2, 2024
d5d3122
Merge branch 'main' of github.com:elastic/elastic-agent into feat/unp…
michalpristas Dec 3, 2024
ae74bc4
more test coverage
michalpristas Dec 3, 2024
edc32f7
Merge branch 'feat/unprivileged/ad' of github.com:michalpristas/elast…
michalpristas Dec 3, 2024
998eca7
fix broken windows UT
michalpristas Dec 3, 2024
626a8a3
coverage
michalpristas Dec 3, 2024
ac9ac30
fixed logic for UnprivilegedUser
michalpristas Dec 3, 2024
902686a
fixed windows tests
michalpristas Dec 3, 2024
48a3fd7
Merge branch 'main' into feat/unprivileged/ad
michalpristas Dec 3, 2024
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,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: Added support for pre-existing Active Directory user for unprivileged mode

# 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: User can specify custom pre-existing user for running unprivileged mode. This user will be gived permissions to log on as a service.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This user will be given permissions to log on as a service


# 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/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/4585
26 changes: 25 additions & 1 deletion internal/pkg/agent/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"

"github.com/spf13/cobra"

Expand All @@ -28,6 +29,10 @@ const (
flagInstallDevelopment = "develop"
flagInstallNamespace = "namespace"
flagInstallRunUninstallFromBinary = "run-uninstall-from-binary"

flagInstallCustomUser = "user"
flagInstallCustomGroup = "group"
flagInstallCustomPass = "password"
)

func newInstallCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command {
Expand Down Expand Up @@ -61,6 +66,13 @@ would like the Agent to operate.
cmd.Flags().Bool(flagInstallDevelopment, false, "Install into a standardized development namespace, may enable development specific options. Allows multiple Elastic Agents to be installed at once. (experimental)")
_ = cmd.Flags().MarkHidden(flagInstallDevelopment) // For internal use only.

// Active directory user specification
cmd.Flags().String(flagInstallCustomUser, "", "Custom user used to run Elastic Agent")
cmd.Flags().String(flagInstallCustomGroup, "", "Custom group used to access Elastic Agent files")
if runtime.GOOS == "windows" {
cmd.Flags().String(flagInstallCustomPass, "", "Password for Active directory user used to run Elastic Agent")
michalpristas marked this conversation as resolved.
Show resolved Hide resolved
}

addEnrollFlags(cmd)

return cmd
Expand Down Expand Up @@ -249,7 +261,19 @@ func installCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
progBar.Describe("Successfully uninstalled Elastic Agent")
}
if status != install.PackageInstall {
ownership, err = install.Install(cfgFile, topPath, unprivileged, log, progBar, streams)
customUser, _ := cmd.Flags().GetString(flagInstallCustomUser)
customGroup, _ := cmd.Flags().GetString(flagInstallCustomGroup)
customPass := ""
if runtime.GOOS == "windows" {
customPass, _ = cmd.Flags().GetString(flagInstallCustomPass)

if (customUser != "" || customPass != "") &&
(customUser == "" || customPass == "") {
return fmt.Errorf("error installing package: all Active Directory parameters must be provided")
michalpristas marked this conversation as resolved.
Show resolved Hide resolved
}
}

ownership, err = install.Install(cfgFile, topPath, unprivileged, log, progBar, streams, customUser, customGroup, customPass)
if err != nil {
return fmt.Errorf("error installing package: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/agent/cmd/privileged.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func privilegedCmd(streams *cli.IOStreams, cmd *cobra.Command) (err error) {
}

pt := install.CreateAndStartNewSpinner(streams.Out, "Converting Elastic Agent to privileged...")
err = install.SwitchExecutingMode(topPath, pt, "", "")
err = install.SwitchExecutingMode(topPath, pt, "", "", "")
if err != nil {
// error already adds context
return err
Expand Down
25 changes: 24 additions & 1 deletion internal/pkg/agent/cmd/unprivileged.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"os"
"runtime"

"github.com/spf13/cobra"

Expand Down Expand Up @@ -44,6 +45,13 @@ unprivileged it will still perform all the same work, including stopping and sta
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)")

// Custom user specification
cmd.Flags().String(flagInstallCustomUser, "", "Custom user used to run Elastic Agent")
cmd.Flags().String(flagInstallCustomGroup, "", "Custom group used to access Elastic Agent files")
if runtime.GOOS == "windows" {
cmd.Flags().String(flagInstallCustomPass, "", "Password for Active directory user used to run Elastic Agent")
michalpristas marked this conversation as resolved.
Show resolved Hide resolved
}

return cmd
}

Expand All @@ -56,6 +64,18 @@ func unprivilegedCmd(streams *cli.IOStreams, cmd *cobra.Command) (err error) {
return fmt.Errorf("unable to perform unprivileged command, not executed with %s permissions", utils.PermissionUser)
}

customUser, _ := cmd.Flags().GetString(flagInstallCustomUser)
customGroup, _ := cmd.Flags().GetString(flagInstallCustomGroup)
customPass := ""
if runtime.GOOS == "windows" {
customPass, _ = cmd.Flags().GetString(flagInstallCustomPass)

if (customUser != "" || customPass != "") &&
(customUser == "" || customPass == "") {
return fmt.Errorf("error installing package: all Active Directory parameters must be provided")
michalpristas marked this conversation as resolved.
Show resolved Hide resolved
}
}

// cannot switch to unprivileged when service components have issues
err = ensureNoServiceComponentIssues()
if err != nil {
Expand All @@ -77,7 +97,10 @@ func unprivilegedCmd(streams *cli.IOStreams, cmd *cobra.Command) (err error) {
}

pt := install.CreateAndStartNewSpinner(streams.Out, "Converting Elastic Agent to unprivileged...")
err = install.SwitchExecutingMode(topPath, pt, install.ElasticUsername, install.ElasticGroupName)

username, password := install.UnprivilegedUser(customUser, customPass)
groupName := install.UnprivilegedGroup(customGroup)
err = install.SwitchExecutingMode(topPath, pt, username, groupName, password)
if err != nil {
// error already adds context
return err
Expand Down
38 changes: 31 additions & 7 deletions internal/pkg/agent/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const (
)

// Install installs Elastic Agent persistently on the system including creating and starting its service.
func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *progressbar.ProgressBar, streams *cli.IOStreams) (utils.FileOwner, error) {
func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *progressbar.ProgressBar, streams *cli.IOStreams, customUser, customGroup, userPassword string) (utils.FileOwner, error) {
dir, err := findDirectory()
if err != nil {
return utils.FileOwner{}, errors.New(err, "failed to discover the source directory for installation", errors.TypeFilesystem)
Expand All @@ -49,13 +49,15 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p
var ownership utils.FileOwner
username := ""
groupName := ""
password := ""
if unprivileged {
username = ElasticUsername
groupName = ElasticGroupName
ownership, err = EnsureUserAndGroup(username, groupName, pt)
username, password = UnprivilegedUser(customUser, userPassword)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only allow custom user for unprivileged mode? Do we not allow to specify custom user when installing in privileged mode?

groupName = UnprivilegedGroup(customGroup)
ownership, err = EnsureUserAndGroup(username, groupName, pt, username == ElasticUsername && password == "") // force create only elastic user
if err != nil {
// error context already added by EnsureUserAndGroup
return utils.FileOwner{}, err

}
}

Expand Down Expand Up @@ -147,7 +149,7 @@ func Install(cfgFile, topPath string, unprivileged bool, log *logp.Logger, pt *p

// install service
pt.Describe("Installing service")
err = InstallService(topPath, ownership, username, groupName)
err = InstallService(topPath, ownership, username, groupName, password)
if err != nil {
pt.Describe("Failed to install service")
// error context already added by InstallService
Expand Down Expand Up @@ -371,11 +373,12 @@ func StatusService(topPath string) (service.Status, error) {
}

// InstallService installs the service.
func InstallService(topPath string, ownership utils.FileOwner, username string, groupName string) error {
opts, err := withServiceOptions(username, groupName)
func InstallService(topPath string, ownership utils.FileOwner, username string, groupName string, password string) error {
opts, err := withServiceOptions(username, groupName, password)
if err != nil {
return fmt.Errorf("error getting service installation options: %w", err)
}

svc, err := newService(topPath, opts...)
if err != nil {
return fmt.Errorf("error creating new service handler for install: %w", err)
Expand Down Expand Up @@ -482,3 +485,24 @@ func CreateInstallMarker(topPath string, ownership utils.FileOwner) error {
_ = handle.Close()
return fixInstallMarkerPermissions(markerFilePath, ownership)
}

func UnprivilegedUser(username, password string) (string, string) {
if username != "" && password != "" {
return username, password
}

if username != "" && runtime.GOOS != "windows" {
// password only required for windows
return username, password
}

return ElasticUsername, ""
}

func UnprivilegedGroup(groupName string) string {
if groupName != "" {
return groupName
}

return ElasticGroupName
}
2 changes: 1 addition & 1 deletion internal/pkg/agent/install/install_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func fixInstallMarkerPermissions(markerFilePath string, ownership utils.FileOwne
}

// withServiceOptions just sets the user/group for the service.
func withServiceOptions(username string, groupName string) ([]serviceOpt, error) {
func withServiceOptions(username string, groupName string, _ string) ([]serviceOpt, error) {
return []serviceOpt{withUserGroup(username, groupName)}, nil
}

Expand Down
7 changes: 6 additions & 1 deletion internal/pkg/agent/install/install_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@ func fixInstallMarkerPermissions(markerFilePath string, ownership utils.FileOwne
}

// withServiceOptions just sets the user/group for the service.
func withServiceOptions(username string, groupName string) ([]serviceOpt, error) {
func withServiceOptions(username string, groupName string, password string) ([]serviceOpt, error) {
if username == "" {
// not installed with --unprivileged; nothing to do
return []serviceOpt{}, nil
}

if password != "" {
// existing user
return []serviceOpt{withUserGroup(username, groupName), withPassword(password)}, nil
michalpristas marked this conversation as resolved.
Show resolved Hide resolved
}

// service requires a password to launch as the user
// this sets it to a random password that is only known by the service
password, err := RandomPassword()
Expand Down
11 changes: 8 additions & 3 deletions internal/pkg/agent/install/prereq.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

// EnsureUserAndGroup creates the given username and group returning the file ownership information for that
// user and group.
func EnsureUserAndGroup(username string, groupName string, pt *progressbar.ProgressBar) (utils.FileOwner, error) {
func EnsureUserAndGroup(username string, groupName string, pt *progressbar.ProgressBar, forceCreate bool) (utils.FileOwner, error) {
var err error
var ownership utils.FileOwner

Expand All @@ -24,7 +24,7 @@ func EnsureUserAndGroup(username string, groupName string, pt *progressbar.Progr
if err != nil && !errors.Is(err, ErrGroupNotFound) {
return utils.FileOwner{}, fmt.Errorf("failed finding group %s: %w", groupName, err)
}
if errors.Is(err, ErrGroupNotFound) {
if forceCreate && errors.Is(err, ErrGroupNotFound) {
pt.Describe(fmt.Sprintf("Creating group %s", groupName))
ownership.GID, err = CreateGroup(groupName)
if err != nil {
Expand All @@ -39,7 +39,7 @@ func EnsureUserAndGroup(username string, groupName string, pt *progressbar.Progr
if err != nil && !errors.Is(err, ErrUserNotFound) {
return utils.FileOwner{}, fmt.Errorf("failed finding username %s: %w", username, err)
}
if errors.Is(err, ErrUserNotFound) {
if forceCreate && errors.Is(err, ErrUserNotFound) {
pt.Describe(fmt.Sprintf("Creating user %s", username))
ownership.UID, err = CreateUser(username, ownership.GID)
if err != nil {
Expand All @@ -53,5 +53,10 @@ func EnsureUserAndGroup(username string, groupName string, pt *progressbar.Progr
}
pt.Describe(fmt.Sprintf("Successfully created user %s", username))
}

if err := EnsureRights(username); err != nil {
pt.Describe(fmt.Sprintf("Failed to assign rights to user %s", username))
return utils.FileOwner{}, fmt.Errorf("failed to set proper rights to user %s: %w", username, err)
}
return ownership, nil
}
6 changes: 3 additions & 3 deletions internal/pkg/agent/install/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
//
// When username and groupName are blank then it switched back to root/Administrator and when a username/groupName is
// provided then it switched to running with that username and groupName.
func SwitchExecutingMode(topPath string, pt *progressbar.ProgressBar, username string, groupName string) error {
func SwitchExecutingMode(topPath string, pt *progressbar.ProgressBar, username string, groupName string, password string) error {
// ensure service is stopped
status, err := EnsureStoppedService(topPath, pt)
if err != nil {
Expand All @@ -38,7 +38,7 @@ func SwitchExecutingMode(topPath string, pt *progressbar.ProgressBar, username s
// ensure user/group are created
var ownership utils.FileOwner
if username != "" && groupName != "" {
ownership, err = EnsureUserAndGroup(username, groupName, pt)
ownership, err = EnsureUserAndGroup(username, groupName, pt, username == ElasticUsername)
if err != nil {
// context for the error already provided in the EnsureUserAndGroup function
return err
Expand Down Expand Up @@ -78,7 +78,7 @@ func SwitchExecutingMode(topPath string, pt *progressbar.ProgressBar, username s

// re-install service
pt.Describe("Installing service")
err = InstallService(topPath, ownership, username, groupName)
err = InstallService(topPath, ownership, username, groupName, password)
if err != nil {
pt.Describe("Failed to install service")
// error context already added by InstallService
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/agent/install/user_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,5 @@ func dsclExec(args ...string) error {
}
return nil
}

func EnsureRights(_ string) error { return nil }
2 changes: 2 additions & 0 deletions internal/pkg/agent/install/user_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,5 @@ func getentGetID(database string, key string) (int, error) {
}
return val, nil
}

func EnsureRights(_ string) error { return nil }
20 changes: 12 additions & 8 deletions internal/pkg/agent/install/user_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,38 +127,42 @@ func CreateUser(name string, _ string) (string, error) {
return "", fmt.Errorf("call to NetUserAdd failed: status=%d error=%d", ret, parmErr)
}

return FindUID(name)
}

func EnsureRights(name string) error {
// adjust the local security policy to ensure that the created user is scoped to a service only
sid, _, _, err := gowin32.GetLocalAccountByName(name)
if err != nil {
return "", fmt.Errorf("failed to get SID for %s: %w", name, err)
return fmt.Errorf("failed to get SID for %s: %w", name, err)
}
sp, err := gowin32.OpenLocalSecurityPolicy()
if err != nil {
return "", fmt.Errorf("failed to open local security policy: %w", err)
return fmt.Errorf("failed to open local security policy: %w", err)
}
defer sp.Close()
err = sp.AddAccountRight(sid, gowin32.AccountRightDenyInteractiveLogon)
if err != nil {
return "", fmt.Errorf("failed to set deny interactive logon: %w", err)
return fmt.Errorf("failed to set deny interactive logon: %w", err)
}
err = sp.AddAccountRight(sid, gowin32.AccountRightDenyNetworkLogon)
if err != nil {
return "", fmt.Errorf("failed to set deny network logon: %w", err)
return fmt.Errorf("failed to set deny network logon: %w", err)
}
err = sp.AddAccountRight(sid, gowin32.AccountRightDenyRemoteInteractiveLogon)
if err != nil {
return "", fmt.Errorf("failed to set deny remote interactive logon: %w", err)
return fmt.Errorf("failed to set deny remote interactive logon: %w", err)
}
err = sp.AddAccountRight(sid, gowin32.AccountRightServiceLogon)
if err != nil {
return "", fmt.Errorf("failed to set service logon: %w", err)
return fmt.Errorf("failed to set service logon: %w", err)
}
err = sp.AddAccountRight(sid, accountRightCreateSymbolicLink)
if err != nil {
return "", fmt.Errorf("failed to add right to create symbolic link: %w", err)
return fmt.Errorf("failed to add right to create symbolic link: %w", err)
}

return FindUID(name)
return nil
}

// AddUserToGroup adds a user to a group.
Expand Down