Skip to content

Commit

Permalink
spoc record: drop sudo privileges
Browse files Browse the repository at this point in the history
  • Loading branch information
mhils authored and saschagrunert committed Aug 26, 2024
1 parent 5f15893 commit b7209c9
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 16 deletions.
4 changes: 4 additions & 0 deletions cmd/spoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ func main() {
Name: recorder.FlagNoProcStart,
Usage: "do not start the target command and record until ctrl+c/SIGINT.",
},
&cli.BoolFlag{
Name: recorder.FlagPrivileged,
Usage: "do not drop sudo privileges when running the target command.",
},
},
},
&cli.Command{
Expand Down
73 changes: 73 additions & 0 deletions internal/pkg/cli/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ limitations under the License.
package command

import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"slices"
"strconv"
"strings"
"syscall"
)

var errNoSudoEnvironment = errors.New("not in a sudo environment")

type Command struct {
impl
options *Options
Expand All @@ -40,6 +48,12 @@ func New(options *Options) *Command {
// Run the Command.
func (c *Command) Run() (pid uint32, err error) {
c.cmd = c.Command(c.options.command, c.options.args...)
if c.options.DropSudoPrivileges {
err := c.DropSudoPrivileges()
if err != nil && !errors.Is(err, errNoSudoEnvironment) {
log.Printf("Failed to drop sudo privileges: %v", err)
}
}
if err := c.CmdStart(c.cmd); err != nil {
return pid, fmt.Errorf("start command: %w", err)
}
Expand All @@ -62,6 +76,65 @@ func (c *Command) Run() (pid uint32, err error) {
return pid, nil
}

func getHomeDirectory(uid uint32) (string, error) {
usr, err := user.LookupId(strconv.FormatUint(uint64(uid), 10))
if err == nil && usr.HomeDir != "" {
return usr.HomeDir, nil
}
//nolint:gosec // uid is trusted here
cmd := exec.Command(
"sudo",
fmt.Sprintf("--user=#%d", uid),
"--set-home",
"bash",
"-c",
"echo -n ~",
)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("home dir lookup failed: %w", err)
}
if len(out) == 0 {
return "", errors.New("home dir lookup failed: no output")
}
return string(out), nil
}

func (c *Command) DropSudoPrivileges() error {
uid, err := strconv.ParseUint(os.Getenv("SUDO_UID"), 10, 32)
if err != nil {
return errNoSudoEnvironment
}
gid, err := strconv.ParseUint(os.Getenv("SUDO_GID"), 10, 32)
if err != nil {
return errNoSudoEnvironment
}
userName := os.Getenv("SUDO_USER")
if userName == "" {
return errNoSudoEnvironment
}
home, err := c.GetHomeDirectory(uint32(uid))
if err != nil {
return fmt.Errorf("failed to drop privileges: %w", err)
}

c.cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
}
c.cmd.Env = append(
slices.DeleteFunc(
c.cmd.Environ(),
func(s string) bool { return strings.HasPrefix(s, "SUDO_") },
),
"HOME="+home,
"USER="+userName,
)
return nil
}

func (c *Command) Wait() error {
return c.CmdWait(c.cmd)
}
79 changes: 79 additions & 0 deletions internal/pkg/cli/command/commandfakes/fake_impl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions internal/pkg/cli/command/consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed 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.
*/

package command

const (
// FlagPrivileged is the flag for running commands without dropping sudo privileges.
FlagPrivileged string = "privileged"
)
5 changes: 5 additions & 0 deletions internal/pkg/cli/command/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type impl interface {
CmdStart(*exec.Cmd) error
CmdPid(*exec.Cmd) uint32
CmdWait(*exec.Cmd) error
GetHomeDirectory(uid uint32) (string, error)
}

func (*defaultImpl) Notify(c chan<- os.Signal, sig ...os.Signal) {
Expand Down Expand Up @@ -61,3 +62,7 @@ func (*defaultImpl) CmdPid(cmd *exec.Cmd) uint32 {
func (*defaultImpl) CmdWait(cmd *exec.Cmd) error {
return cmd.Wait()
}

func (*defaultImpl) GetHomeDirectory(uid uint32) (string, error) {
return getHomeDirectory(uid)
}
13 changes: 10 additions & 3 deletions internal/pkg/cli/command/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (

// Options define all possible options for the command.
type Options struct {
command string
args []string
command string
args []string
DropSudoPrivileges bool
}

// Command returns the command name.
Expand All @@ -35,7 +36,9 @@ func (o *Options) Command() string {

// Default returns a default options instance.
func Default() *Options {
return &Options{}
return &Options{
DropSudoPrivileges: true,
}
}

// FromContext can be used to create Options from an CLI context.
Expand All @@ -49,5 +52,9 @@ func FromContext(ctx *cli.Context) (*Options, error) {
options.command = args[0]
options.args = args[1:]

if ctx.IsSet(FlagPrivileged) {
options.DropSudoPrivileges = false
}

return options, nil
}
8 changes: 7 additions & 1 deletion internal/pkg/cli/recorder/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ limitations under the License.

package recorder

import "sigs.k8s.io/security-profiles-operator/internal/pkg/cli"
import (
"sigs.k8s.io/security-profiles-operator/internal/pkg/cli"
"sigs.k8s.io/security-profiles-operator/internal/pkg/cli/command"
)

const (
// FlagOutputFile is the flag for defining the output file location.
Expand All @@ -36,6 +39,9 @@ const (
// FlagNoProcStart can be used to indicate that the target process is managed
// externally and should not be started.
FlagNoProcStart string = "no-proc-start"

// FlagPrivileged is the flag for running commands without dropping sudo privileges.
FlagPrivileged string = command.FlagPrivileged
)

// Type is the enum for all available recorder types.
Expand Down
2 changes: 1 addition & 1 deletion test/spoc/demobinary.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func main() {
// make file writable for other users so that sudo/non-sudo testing works.
err = os.Chmod(*fileWrite, fileMode)
if err != nil {
log.Fatal("❌ Error setting file permissions:", err)
log.Println("Error setting file permissions:", err)
}
}
if *fileSymlink != "" {
Expand Down
25 changes: 14 additions & 11 deletions test/spoc/e2e_spoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func recordAppArmorTest(t *testing.T) {
t.Skip("BPF LSM disabled")
}
fileRead := fmt.Sprintf("../../README.md,/proc/1/limits,/proc/%d/limits", os.Getpid())
profile := recordAppArmor(t, "--file-read", fileRead, "--file-write", "/dev/null")
profile := recordAppArmor(t, "./demobinary", "--file-read", fileRead, "--file-write", "/dev/null")
readme, err := filepath.Abs("../../README.md")
require.NoError(t, err)
require.NotNil(t, profile.Filesystem)
Expand All @@ -84,44 +84,47 @@ func recordAppArmorTest(t *testing.T) {
}
require.Equal(t, 1, count)

profile = recordAppArmor(t, "--file-read", "/dev/null", "--file-write", "/dev/null")
profile = recordAppArmor(t, "./demobinary", "--file-read", "/dev/null", "--file-write", "/dev/null")
require.Contains(t, *profile.Filesystem.ReadWritePaths, "/dev/null")
})
t.Run("sockets", func(t *testing.T) {
if !bpfrecorder.BPFLSMEnabled() {
t.Skip("BPF LSM disabled")
}
profile := recordAppArmor(t, "--net-tcp")
profile := recordAppArmor(t, "./demobinary", "--net-tcp")
require.True(t, *profile.Network.Protocols.AllowTCP)
require.Nil(t, profile.Capability)
profile = recordAppArmor(t, "--net-udp")
profile = recordAppArmor(t, "./demobinary", "--net-udp")
require.True(t, *profile.Network.Protocols.AllowUDP)
require.Nil(t, profile.Capability)
profile = recordAppArmor(t, "--net-icmp")
profile = recordAppArmor(t, "--privileged", "./demobinary", "--net-icmp")
require.True(t, *profile.Network.AllowRaw)
require.Contains(t, profile.Capability.AllowedCapabilities, "net_raw")
})
t.Run("capabilities", func(t *testing.T) {
if !bpfrecorder.BPFLSMEnabled() {
t.Skip("BPF LSM disabled")
}
profile := recordAppArmor(t, "--cap-sys-admin")
profile := recordAppArmor(t, "--privileged", "./demobinary", "--cap-sys-admin")
require.Contains(t, profile.Capability.AllowedCapabilities, "sys_admin")

profile = recordAppArmor(t, "./demobinary", "--cap-sys-admin")
require.NotContains(t, profile.Capability.AllowedCapabilities, "sys_admin")
})

t.Run("subprocess", func(t *testing.T) {
if !bpfrecorder.BPFLSMEnabled() {
t.Skip("BPF LSM disabled")
}
profile := recordAppArmor(t, "./demobinary-child", "--file-read", "/dev/null")
profile := recordAppArmor(t, "./demobinary", "./demobinary-child", "--file-read", "/dev/null")
require.Contains(t, (*profile.Executable.AllowedExecutables)[0], "/demobinary-child")
require.Contains(t, *profile.Filesystem.ReadOnlyPaths, "/dev/null")

profile = recordAppArmor(t, "./demobinary", "--file-read", "/dev/null")
profile = recordAppArmor(t, "./demobinary", "./demobinary", "--file-read", "/dev/null")
require.Contains(t, (*profile.Executable.AllowedExecutables)[0], "/demobinary")
require.Contains(t, *profile.Filesystem.ReadOnlyPaths, "/dev/null")

profile = recordAppArmor(t, "./demobinary-child", "./demobinary-child", "--file-read", "/dev/null")
profile = recordAppArmor(t, "./demobinary", "./demobinary-child", "./demobinary-child", "--file-read", "/dev/null")
require.Contains(t, (*profile.Executable.AllowedExecutables)[0], "/demobinary-child")
require.Contains(t, *profile.Filesystem.ReadOnlyPaths, "/dev/null")
})
Expand Down Expand Up @@ -214,7 +217,7 @@ func recordAppArmorTest(t *testing.T) {
}

func recordSeccompTest(t *testing.T) {
profile := recordSeccomp(t, "--net-tcp")
profile := recordSeccomp(t, "./demobinary", "--net-tcp")
require.Contains(t, profile.Syscalls[0].Names, "listen")
}

Expand All @@ -233,7 +236,7 @@ func runSpoc(t *testing.T, args ...string) ([]byte, error) {
func record(t *testing.T, typ string, profile client.Object, args ...string) {
t.Helper()
args = append([]string{
"record", "-t", typ, "-o", "/dev/stdout", "--no-base-syscalls", "./demobinary",
"record", "-t", typ, "-o", "/dev/stdout", "--no-base-syscalls",
}, args...)
content, err := runSpoc(t, args...)
require.NoError(t, err, "failed to run spoc")
Expand Down

0 comments on commit b7209c9

Please sign in to comment.