From 5b3db10409f7735596218d9ed9bfd8f00a7b9578 Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Thu, 21 Nov 2024 17:15:19 +0100 Subject: [PATCH 1/3] chore(decl/caps): add capabilities manipulation package Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- go.mod | 2 + go.sum | 4 ++ pkg/capability/capability.go | 109 +++++++++++++++++++++++++++++++++++ pkg/capability/doc.go | 17 ++++++ 4 files changed, 132 insertions(+) create mode 100644 pkg/capability/capability.go create mode 100644 pkg/capability/doc.go diff --git a/go.mod b/go.mod index 56ecca40..c950549f 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( k8s.io/cli-runtime v0.31.1 k8s.io/client-go v0.31.1 k8s.io/kubectl v0.31.1 + kernel.org/pub/linux/libs/security/libcap/cap v1.2.72 ) // avoid indirect dependency mergo error: @@ -122,6 +123,7 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect + kernel.org/pub/linux/libs/security/libcap/psx v1.2.72 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.17.3 // indirect sigs.k8s.io/kustomize/kyaml v0.17.2 // indirect diff --git a/go.sum b/go.sum index 58f7d160..d2ceceae 100644 --- a/go.sum +++ b/go.sum @@ -416,6 +416,10 @@ k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24= k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.72 h1:SqLZbTqwcNxctcdM5yb6OcO3lFJNtRgDJoFeca+5hP0= +kernel.org/pub/linux/libs/security/libcap/cap v1.2.72/go.mod h1:DqOj+O+b5nV0YW49IVU6WlhVw0oHpC1LmerDRHvcbjE= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.72 h1:Lnu8woGlvNdwCbBa5urxGO+L5gp647lIZMg0vP+upuU= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.72/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.17.3 h1:6GCuHSsxq7fN5yhF2XrC+AAr8gxQwhexgHflOAD/JJU= diff --git a/pkg/capability/capability.go b/pkg/capability/capability.go new file mode 100644 index 00000000..5b652c5c --- /dev/null +++ b/pkg/capability/capability.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 The Falco 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 capability + +import ( + "fmt" + "runtime" + + "golang.org/x/sys/unix" + "kernel.org/pub/linux/libs/security/libcap/cap" +) + +// Parse the provided capabilities string and returns the parsed capability state. The provided capabilities must be +// encoded using the syntax specified in cap_from_text(3). +func Parse(capabilities string) (*cap.Set, error) { + return cap.FromText(capabilities) +} + +// RunWithSecBitNoRootEnabled runs the provided function with the thread secure bit SECBIT_NOROOT enabled. +func RunWithSecBitNoRootEnabled(f func() error) (err error) { + runtime.LockOSThread() + secureBits, err := unix.PrctlRetInt(unix.PR_GET_SECUREBITS, 0, 0, 0, 0) + if err != nil { + return fmt.Errorf("error retrieving thread secure bits: %w", err) + } + + secureBitsPlusSecBitNoRoot := secureBits | int(cap.SecbitNoRoot) + if secureBits == secureBitsPlusSecBitNoRoot { + return runFuncAndWrapErr(f) + } + + if err := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBitsPlusSecBitNoRoot), 0, 0, 0); err != nil { + return fmt.Errorf("error setting thread secure bits: %w", err) + } + defer func() { + if e := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBits), 0, 0, 0); e != nil { + e = fmt.Errorf("error restoring secure bits: %w", err) + if err != nil { + err = fmt.Errorf("%w; %w", err, e) + } else { + err = e + } + } + }() + + return runFuncAndWrapErr(f) +} + +// RunWithSecBitNoRootDisabled runs the provided function with the thread secure bit SECBIT_NOROOT disable. +func RunWithSecBitNoRootDisabled(f func() error) (err error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + secureBits, err := unix.PrctlRetInt(unix.PR_GET_SECUREBITS, 0, 0, 0, 0) + if err != nil { + return fmt.Errorf("error retrieving thread secure bits: %w", err) + } + + secureBitsMinusSecBitNoRoot := secureBits & ^int(cap.SecbitNoRoot) + if secureBits == secureBitsMinusSecBitNoRoot { + return runFuncAndWrapErr(f) + } + + if err := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBitsMinusSecBitNoRoot), 0, 0, 0); err != nil { + return fmt.Errorf("error setting thread secure bits: %w", err) + } + defer func() { + if e := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBits), 0, 0, 0); e != nil { + e = fmt.Errorf("error restoring secure bits: %w", err) + if err != nil { + err = fmt.Errorf("%w; %w", err, e) + } else { + err = e + } + } + }() + + return runFuncAndWrapErr(f) +} + +// runFuncAndWrapErr runs the provided function and wraps the returned error with FuncError. +func runFuncAndWrapErr(f func() error) error { + if err := f(); err != nil { + return &FuncError{err: err} + } + return nil +} + +// FuncError wraps the error produced by the execution of the function provided to RunWithSecBitNoRootEnabled and +// RunWithSecBitNoRootDisabled. +type FuncError struct { + err error +} + +func (e *FuncError) Error() string { + return e.err.Error() +} diff --git a/pkg/capability/doc.go b/pkg/capability/doc.go new file mode 100644 index 00000000..249f6d07 --- /dev/null +++ b/pkg/capability/doc.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2024 The Falco 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 capability provides utilities for capabilities manipulation. +package capability From 955486ad7e0067707a06e9e67510c79e9315affd Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Thu, 21 Nov 2024 17:27:18 +0100 Subject: [PATCH 2/3] feat(decl/loader): add user and capabilities fields YAML definition Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/test/loader/loader.go | 52 +++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/pkg/test/loader/loader.go b/pkg/test/loader/loader.go index c3d9fd98..1fb924bc 100644 --- a/pkg/test/loader/loader.go +++ b/pkg/test/loader/loader.go @@ -74,25 +74,16 @@ func (c *Description) validate() error { for testIndex := range c.Tests { test := &c.Tests[testIndex] - if err := validateNameUniqueness(test); err != nil { + if err := test.validateNameUniqueness(); err != nil { return fmt.Errorf("error validating name uniqueness in test %q (index: %d): %w", test.Name, testIndex, err) } - } - - return nil -} -// validateNameUniqueness validates that names used for test resources and steps are unique. -func validateNameUniqueness(test *Test) error { - for resourceIndex, testResource := range test.Resources { - for stepIndex, testStep := range test.Steps { - if testStep.Name == testResource.Name { - return fmt.Errorf("test resource %d and test step %d have the same name %q", resourceIndex, - stepIndex, testResource.Name) - } + if err := test.validateContext(); err != nil { + return fmt.Errorf("error validating test context: %w", err) } } + return nil } @@ -110,6 +101,35 @@ type Test struct { ExpectedOutcome TestExpectedOutcome `yaml:"expectedOutcome"` } +// validateNameUniqueness validates that names used for test resources and steps are unique. +func (t *Test) validateNameUniqueness() error { + for resourceIndex, testResource := range t.Resources { + for stepIndex, testStep := range t.Steps { + if testStep.Name == testResource.Name { + return fmt.Errorf("test resource at index %d and test step at index %d have the same name %q", + resourceIndex, stepIndex, testResource.Name) + } + } + } + return nil +} + +// validateContext validates that names used for test resources and steps are unique. +func (t *Test) validateContext() error { + processes := t.Context.Processes + processesLen := len(processes) + if processesLen <= 1 { + return nil + } + + for processIndex, process := range processes[:processesLen-1] { + if process.Capabilities != nil && *process.Capabilities != "" { + return fmt.Errorf("process at index %d specifies capabilities but is not the leaf process", processIndex) + } + } + return nil +} + // TestRunnerType is the type of test runner. type TestRunnerType string @@ -163,6 +183,12 @@ type ProcessContext struct { Name *string `yaml:"name,omitempty" validate:"omitempty,min=1"` // Env is the set of environment variables that must be provided to the process (in addition to the default ones). Env map[string]string `yaml:"env,omitempty" validate:"omitempty,min=1"` + // User is the name of the user that must run the process. If omitted, the current process user is used. If the user + // does not exist, it is created before running the test and deleted after test execution. + User *string `yaml:"user,omitempty" validate:"omitempty,min=1"` + // Capabilities are the capabilities of the process. The syntax follows the conventions specified by + // cap_from_text(3). If omitted or empty, it defaults to "all=iep". + Capabilities *string `yaml:"capabilities,omitempty" validate:"omitempty,min=1"` } // TestResource describes a test resource. From 0d1d4891add03961479828ca37bb995e50a0dc2c Mon Sep 17 00:00:00 2001 From: Leonardo Di Giovanna Date: Thu, 21 Nov 2024 17:45:45 +0100 Subject: [PATCH 3/3] feat(decl/proc-chain): add user and capabilities support Add the capability to specify the user and the linux capabilities a process in the process chain can be run with. Capabilities can only be specified for the leaf process. Omitting capabilities is equivalent to specify 'all=iep'. Each process in the chain runs with real user/group ID equals to 0 (root). Specifying a user sets the effective and the saved set-user/group-ID to the corresponding user/group IDs. If a user specified in the chain doesn't exist, it is created before running the test and deleted after test execution. The securebit SECBBIT_NOROOT is enabled before creating any child process: this is done in order to prevent the kernel from ignoring the specified capabilities when the real user ID is zero (see 'Capabilities and execution of programs by root' in capabilities(7)). Users who wish to run the before and after script or creating a 'process' test resource must take into account to provide at least CAP_SETPCAP in its permitted and effective set. Signed-off-by: Leonardo Di Giovanna Co-authored-by: Aldo Lacuku --- pkg/capability/capability.go | 47 +++--- pkg/process/process.go | 282 +++++++++++++++++++++++++++++---- pkg/test/runner/host/host.go | 25 ++- pkg/test/script/shell/shell.go | 9 +- 4 files changed, 301 insertions(+), 62 deletions(-) diff --git a/pkg/capability/capability.go b/pkg/capability/capability.go index 5b652c5c..7f150686 100644 --- a/pkg/capability/capability.go +++ b/pkg/capability/capability.go @@ -16,6 +16,7 @@ package capability import ( + "errors" "fmt" "runtime" @@ -30,7 +31,7 @@ func Parse(capabilities string) (*cap.Set, error) { } // RunWithSecBitNoRootEnabled runs the provided function with the thread secure bit SECBIT_NOROOT enabled. -func RunWithSecBitNoRootEnabled(f func() error) (err error) { +func RunWithSecBitNoRootEnabled(f func() error) error { runtime.LockOSThread() secureBits, err := unix.PrctlRetInt(unix.PR_GET_SECUREBITS, 0, 0, 0, 0) if err != nil { @@ -38,29 +39,11 @@ func RunWithSecBitNoRootEnabled(f func() error) (err error) { } secureBitsPlusSecBitNoRoot := secureBits | int(cap.SecbitNoRoot) - if secureBits == secureBitsPlusSecBitNoRoot { - return runFuncAndWrapErr(f) - } - - if err := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBitsPlusSecBitNoRoot), 0, 0, 0); err != nil { - return fmt.Errorf("error setting thread secure bits: %w", err) - } - defer func() { - if e := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBits), 0, 0, 0); e != nil { - e = fmt.Errorf("error restoring secure bits: %w", err) - if err != nil { - err = fmt.Errorf("%w; %w", err, e) - } else { - err = e - } - } - }() - - return runFuncAndWrapErr(f) + return runWithSecBits(f, secureBits, secureBitsPlusSecBitNoRoot) } -// RunWithSecBitNoRootDisabled runs the provided function with the thread secure bit SECBIT_NOROOT disable. -func RunWithSecBitNoRootDisabled(f func() error) (err error) { +// RunWithSecBitNoRootDisabled runs the provided function with the thread secure bit SECBIT_NOROOT disabled. +func RunWithSecBitNoRootDisabled(f func() error) error { runtime.LockOSThread() defer runtime.UnlockOSThread() secureBits, err := unix.PrctlRetInt(unix.PR_GET_SECUREBITS, 0, 0, 0, 0) @@ -69,16 +52,28 @@ func RunWithSecBitNoRootDisabled(f func() error) (err error) { } secureBitsMinusSecBitNoRoot := secureBits & ^int(cap.SecbitNoRoot) - if secureBits == secureBitsMinusSecBitNoRoot { + return runWithSecBits(f, secureBits, secureBitsMinusSecBitNoRoot) +} + +// runWithSecBits runs the provided function with the new secure bits set, and restores the old secure bits set at the +// end of its execution. +func runWithSecBits(f func() error, oldSecBits, newSecBits int) (err error) { + if oldSecBits == newSecBits { return runFuncAndWrapErr(f) } - if err := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBitsMinusSecBitNoRoot), 0, 0, 0); err != nil { + if err := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(newSecBits), 0, 0, 0); err != nil { + if errors.Is(err, unix.EPERM) { + err = fmt.Errorf("%w (consider adding the CAP_SETPCAP capability)", err) + } return fmt.Errorf("error setting thread secure bits: %w", err) } defer func() { - if e := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(secureBits), 0, 0, 0); e != nil { - e = fmt.Errorf("error restoring secure bits: %w", err) + if e := unix.Prctl(unix.PR_SET_SECUREBITS, uintptr(oldSecBits), 0, 0, 0); e != nil { + if errors.Is(e, unix.EPERM) { + e = fmt.Errorf("%w (consider adding the CAP_SETPCAP capability)", e) + } + e = fmt.Errorf("error restoring thread secure bits: %w", e) if err != nil { err = fmt.Errorf("%w; %w", err, e) } else { diff --git a/pkg/process/process.go b/pkg/process/process.go index f03c933c..a74e1e37 100644 --- a/pkg/process/process.go +++ b/pkg/process/process.go @@ -19,14 +19,18 @@ import ( "context" "errors" "fmt" + "io" "os" "os/exec" + "os/user" "path/filepath" "regexp" + "strconv" "github.com/go-logr/logr" "golang.org/x/sys/unix" + "github.com/falcosecurity/event-generator/pkg/capability" "github.com/falcosecurity/event-generator/pkg/random" ) @@ -38,10 +42,18 @@ type Process struct { // simExePath is the "simulated" executable path. This sets the executable path accessible through // `readlink -f /proc/