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

[Heartbeat] Drop only inheritable cap set and defer setuid to node fork #33584

Merged
merged 11 commits into from
Jan 24, 2023
3 changes: 3 additions & 0 deletions heartbeat/security/policy_linux_386.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,11 @@ func init() {
"sendto",
"set_robust_list",
"set_tid_address",
"setgid",
"setgroups",
"setpriority",
"setsid",
"setuid",
"sigaltstack",
"socket",
"socketpair",
Expand Down
3 changes: 3 additions & 0 deletions heartbeat/security/policy_linux_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,11 @@ func init() {
"sendto",
"set_robust_list",
"set_tid_address",
"setgid",
"setgroups",
"setpriority",
"setsid",
"setuid",
"sigaltstack",
"socket",
"socketpair",
Expand Down
51 changes: 17 additions & 34 deletions heartbeat/security/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"syscall"

"github.com/elastic/go-sysinfo"

"kernel.org/pub/linux/libs/security/libcap/cap"
)

Expand All @@ -44,7 +43,7 @@ func init() {
}

if localUserName := os.Getenv("BEAT_SETUID_AS"); isContainer && localUserName != "" && syscall.Geteuid() == 0 {
err := changeUser(localUserName)
err := setNodeProcAttr(localUserName)
if err != nil {
panic(err)
}
Expand All @@ -57,7 +56,8 @@ func init() {
_ = setCapabilities()
}

func changeUser(localUserName string) error {
func setNodeProcAttr(localUserName string) error {

localUser, err := user.Lookup(localUserName)
if err != nil {
return fmt.Errorf("could not lookup '%s': %w", localUser, err)
Expand All @@ -70,50 +70,33 @@ func changeUser(localUserName string) error {
if err != nil {
return fmt.Errorf("could not parse GID '%s' as int: %w", localUser.Uid, err)
}

// We include the root group because the docker image contains many directories (data,logs)
// that are owned by root:root with 0775 perms. The heartbeat user is in both groups
// in the container, but we need to repeat that here.
err = syscall.Setgroups([]int{localUserGID, 0})
if err != nil {
return fmt.Errorf("could not set groups: %w", err)
}

// Set the main group as localUserUid so new files created are owned by the user's group
err = syscall.Setgid(localUserGID)
if err != nil {
return fmt.Errorf("could not set gid to %d: %w", localUserGID, err)
NodeChildProcCred = &syscall.Credential{
Uid: uint32(localUserUID),
Gid: uint32(localUserGID),
Groups: []uint32{0},
NoSetGroups: false,
}

// Note this is not the regular SetUID! Look at the 'cap' package docs for it, it preserves
// capabilities post-SetUID, which we use to lock things down immediately
err = cap.SetUID(localUserUID)
if err != nil {
return fmt.Errorf("could not setuid to %d: %w", localUserUID, err)
}

// This may not be necessary, but is good hygiene, we do some shelling out to node/npm etc.
// and $HOME should reflect the user's preferences
return os.Setenv("HOME", localUser.HomeDir)
}

func setCapabilities() error {
// Start with an empty capability set
newcaps := cap.NewSet()
// Both permitted and effective are required! Permitted makes the permmission
// possible to get, effective makes it 'active'
err := newcaps.SetFlag(cap.Permitted, true, cap.NET_RAW)
if err != nil {
return fmt.Errorf("error setting permitted setcap: %w", err)
}
err = newcaps.SetFlag(cap.Effective, true, cap.NET_RAW)
newcaps := cap.GetProc()

// Raise all permitted caps to effective
err := newcaps.Fill(cap.Effective, cap.Permitted)
if err != nil {
return fmt.Errorf("error setting effective setcap: %w", err)
return fmt.Errorf("error clearing inheritable cap set: %w", err)
emilioalvap marked this conversation as resolved.
Show resolved Hide resolved
}

// We do not want these capabilities to be inherited by subprocesses
err = newcaps.SetFlag(cap.Inheritable, false, cap.NET_RAW)
// Drop all inheritable caps to stop propagation to child proc
err = newcaps.ClearFlag(cap.Inheritable)
if err != nil {
return fmt.Errorf("error setting inheritable setcap: %w", err)
return fmt.Errorf("error clearing inheritable cap set: %w", err)
}

// Apply the new capabilities to the current process (incl. all threads)
Expand Down
6 changes: 3 additions & 3 deletions heartbeat/security/security_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@

package security

// Empty file so that non-linux platforms have *something*
// to import, thus preventing mage from complaining
// no files are imported from the package
import "syscall"

var NodeChildProcCred *syscall.Credential = nil
3 changes: 2 additions & 1 deletion x-pack/heartbeat/monitors/browser/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/elastic/beats/v7/heartbeat/ecserr"
"github.com/elastic/beats/v7/heartbeat/monitors/plugin"
"github.com/elastic/beats/v7/heartbeat/security"
)

func init() {
Expand All @@ -38,7 +39,7 @@ func create(name string, cfg *config.C) (p plugin.Plugin, err error) {
})

// We do not use user.Current() which does not reflect setuid changes!
if syscall.Geteuid() == 0 {
if syscall.Geteuid() == 0 && security.NodeChildProcCred == nil {
return plugin.Plugin{}, fmt.Errorf("script monitors cannot be run as root")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@
package synthexec

import (
"os"
"os/exec"

"github.com/elastic/beats/v7/heartbeat/security"
"github.com/elastic/elastic-agent-libs/logp"
"golang.org/x/sys/unix"
)

func init() {
platformCmdMutate = func(cmd *exec.Cmd) {
logp.L().Warn("invoking node as:", security.NodeChildProcCred, " from: ", os.Getenv("HOME"))
// Note that while cmd.SysProcAtr takes a syscall.SysProcAttr object
// we are passing in a unix.SysProcAttr object
// this is equivalent, but the unix package is not considered deprecated
// as the syscall package is
cmd.SysProcAttr = &unix.SysProcAttr{
// Ensure node subprocesses are killed if this process dies (linux only)
Pdeathsig: unix.SIGKILL,
// Apply restricted user if available
Credential: security.NodeChildProcCred,
}
}
}