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
52 changes: 18 additions & 34 deletions heartbeat/security/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"strconv"
"syscall"

"github.com/elastic/go-sysinfo"
sysinfo "github.com/elastic/go-sysinfo"

"kernel.org/pub/linux/libs/security/libcap/cap"
)
Expand All @@ -44,7 +44,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 +57,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 +71,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 raising effective cap set: %w", err)
}

// 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
27 changes: 27 additions & 0 deletions heartbeat/security/security_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

//go:build linux || darwin
// +build linux darwin

package security

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
11 changes: 9 additions & 2 deletions x-pack/heartbeat/monitors/browser/synthexec/synthexec_linux.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
// 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 linux || darwin
// +build linux darwin
//go:build linux
// +build linux
Copy link
Collaborator Author

@emilioalvap emilioalvap Dec 5, 2022

Choose a reason for hiding this comment

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

Pdeathsig is Linux only, removing darwin from build tags to reflect that


package synthexec

import (
"os"
"os/exec"

"golang.org/x/sys/unix"

"github.com/elastic/beats/v7/heartbeat/security"
"github.com/elastic/elastic-agent-libs/logp"
)

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,
}
}
}