diff --git a/go.mod b/go.mod index 64d19452f6b..e849271220f 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.3 github.com/docker/go-units v0.4.0 github.com/godbus/dbus/v5 v5.0.4 + github.com/landlock-lsm/go-landlock v0.0.0-20210908180355-c56710719da4 github.com/moby/sys/mountinfo v0.4.1 github.com/mrunalp/fileutils v0.5.0 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 @@ -22,6 +23,6 @@ require ( github.com/urfave/cli v1.22.1 github.com/vishvananda/netlink v1.1.0 golang.org/x/net v0.0.0-20201224014010-6772e930b67b - golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 + golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf google.golang.org/protobuf v1.27.1 ) diff --git a/go.sum b/go.sum index 186bf5354f9..59568008d61 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/landlock-lsm/go-landlock v0.0.0-20210908180355-c56710719da4 h1:5FNPB9FxONNZ10VtNC2n15+0O4O6wfCqCBmkxm2O5x0= +github.com/landlock-lsm/go-landlock v0.0.0-20210908180355-c56710719da4/go.mod h1:wjznJ04q4Tvsbx3vkzfmgfEOe6w5dSGlXFa+xbSl9X8= github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= @@ -76,8 +78,8 @@ golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -94,3 +96,5 @@ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+Rur google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.51 h1:VXVXjnTUsA9zeHIolNb6moSXZavDe1pD8Q0lPXZEOwc= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.51/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index e0db8e01782..ee7506d92fb 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -9,6 +9,7 @@ import ( "github.com/sirupsen/logrus" + "github.com/landlock-lsm/go-landlock/landlock" "github.com/opencontainers/runc/libcontainer/devices" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -81,6 +82,30 @@ type Syscall struct { Args []*Arg `json:"args"` } +// Landlock specifies the Landlock unprivileged access control settings for the container process. +type Landlock struct { + Ruleset *Ruleset `json:"ruleset"` + Rules *Rules `json:"rules"` + DisableBestEffort bool `json:"disableBestEffort"` +} + +// Ruleset identifies a set of rules (i.e., actions on objects) that need to be handled in Landlock. +type Ruleset struct { + HandledAccessFS landlock.AccessFSSet `json:"handledAccessFS"` +} + +// Rules represents the security policies (i.e., actions allowed on objects) in Landlock. +type Rules struct { + PathBeneath []*RulePathBeneath `json:"pathBeneath"` +} + +// RulePathBeneath defines the file-hierarchy typed rule that grants the access rights specified by +// AllowedAccess to the file hierarchies under the given Paths in Landlock. +type RulePathBeneath struct { + AllowedAccess landlock.AccessFSSet `json:"allowedAccess"` + Paths []string `json:"paths"` +} + // TODO Windows. Many of these fields should be factored out into those parts // which are common across platforms, and those which are platform specific. @@ -209,6 +234,10 @@ type Config struct { // RootlessCgroups is set when unlikely to have the full access to cgroups. // When RootlessCgroups is set, cgroups errors are ignored. RootlessCgroups bool `json:"rootless_cgroups,omitempty"` + + // Landlock specifies the Landlock unprivileged access control settings for the container process. + // NoNewPrivileges must be enabled to use Landlock. + Landlock *Landlock `json:"landlock,omitempty"` } type ( diff --git a/libcontainer/landlock/config.go b/libcontainer/landlock/config.go new file mode 100644 index 00000000000..b2b4302b0ec --- /dev/null +++ b/libcontainer/landlock/config.go @@ -0,0 +1,35 @@ +package landlock + +import ( + "fmt" + + "github.com/landlock-lsm/go-landlock/landlock" + ll "github.com/landlock-lsm/go-landlock/landlock/syscall" +) + +var accessFSSets = map[string]landlock.AccessFSSet{ + "execute": ll.AccessFSExecute, + "write_file": ll.AccessFSWriteFile, + "read_file": ll.AccessFSReadFile, + "read_dir": ll.AccessFSReadDir, + "remove_dir": ll.AccessFSRemoveDir, + "remove_file": ll.AccessFSRemoveFile, + "make_char": ll.AccessFSMakeChar, + "make_dir": ll.AccessFSMakeDir, + "make_reg": ll.AccessFSMakeReg, + "make_sock": ll.AccessFSMakeSock, + "make_fifo": ll.AccessFSMakeFifo, + "make_block": ll.AccessFSMakeBlock, + "make_sym": ll.AccessFSMakeSym, +} + +// ConvertStringToAccessFSSet converts a string into a go-landlock AccessFSSet +// access right. +// This gives more explicit control over the mapping between the permitted +// values in the spec and the ones supported in go-landlock library. +func ConvertStringToAccessFSSet(in string) (landlock.AccessFSSet, error) { + if access, ok := accessFSSets[in]; ok { + return access, nil + } + return 0, fmt.Errorf("string %s is not a valid access right for landlock", in) +} diff --git a/libcontainer/landlock/landlock.go b/libcontainer/landlock/landlock.go new file mode 100644 index 00000000000..52b7aa49a02 --- /dev/null +++ b/libcontainer/landlock/landlock.go @@ -0,0 +1,58 @@ +package landlock + +import ( + "errors" + "fmt" + + "github.com/landlock-lsm/go-landlock/landlock" + + "github.com/opencontainers/runc/libcontainer/configs" +) + +// Initialize Landlock unprivileged access control for the container process +// based on the given settings. +// The specified `ruleset` identifies a set of rules (i.e., actions on objects) +// that need to be handled (i.e., restricted) by Landlock. And if no `rule` +// explicitly allow them, they should then be forbidden. +// The `disableBestEffort` input gives control over whether the best-effort +// security approach should be applied for Landlock access rights. +func InitLandlock(config *configs.Landlock) error { + if config == nil { + return errors.New("cannot initialize Landlock - nil config passed") + } + + ruleset := config.Ruleset.HandledAccessFS + llConfig, err := landlock.NewConfig(ruleset) + if err != nil { + return fmt.Errorf("could not create ruleset: %w", err) + } + + if !config.DisableBestEffort { + *llConfig = llConfig.BestEffort() + } + + if err := llConfig.RestrictPaths( + pathAccesses(config.Rules)..., + ); err != nil { + return fmt.Errorf("could not restrict paths: %w", err) + } + + return nil +} + +// Convert Libcontainer RulePathBeneath to go-landlock PathOpt. +func pathAccess(rule *configs.RulePathBeneath) landlock.PathOpt { + return landlock.PathAccess(rule.AllowedAccess, rule.Paths...) +} + +// Convert Libcontainer Rules to an array of go-landlock PathOpt. +func pathAccesses(rules *configs.Rules) []landlock.PathOpt { + pathAccesses := []landlock.PathOpt{} + + for _, rule := range rules.PathBeneath { + opt := pathAccess(rule) + pathAccesses = append(pathAccesses, opt) + } + + return pathAccesses +} diff --git a/libcontainer/setns_init_linux.go b/libcontainer/setns_init_linux.go index a2c9efc4f2a..759baa3d340 100644 --- a/libcontainer/setns_init_linux.go +++ b/libcontainer/setns_init_linux.go @@ -13,6 +13,7 @@ import ( "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runc/libcontainer/keys" + "github.com/opencontainers/runc/libcontainer/landlock" "github.com/opencontainers/runc/libcontainer/seccomp" "github.com/opencontainers/runc/libcontainer/system" ) @@ -86,6 +87,12 @@ func (l *linuxSetnsInit) Init() error { if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil { return err } + // `noNewPrivileges` must be enabled to use Landlock. + if l.config.Config.Landlock != nil && l.config.NoNewPrivileges { + if err := landlock.InitLandlock(l.config.Config.Landlock); err != nil { + return fmt.Errorf("unable to init Landlock: %w", err) + } + } // Set seccomp as close to execve as possible, so as few syscalls take // place afterward (reducing the amount of syscalls that users need to // enable in their seccomp profiles). diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 991c08a1996..18266ae37ab 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -16,6 +16,7 @@ import ( "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/devices" + "github.com/opencontainers/runc/libcontainer/landlock" "github.com/opencontainers/runc/libcontainer/seccomp" libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils" "github.com/opencontainers/runtime-spec/specs-go" @@ -319,6 +320,12 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { Ambient: spec.Process.Capabilities.Ambient, } } + + landlock, err := SetupLandlock(spec.Process.Landlock) + if err != nil { + return nil, err + } + config.Landlock = landlock } createHooks(spec, config) config.Version = specs.Version @@ -845,6 +852,55 @@ func parseMountOptions(options []string) (int, []int, string, int) { return flag, pgflag, strings.Join(data, ","), extFlags } +func SetupLandlock(ll *specs.Landlock) (*configs.Landlock, error) { + if ll == nil { + return nil, nil + } + + // No ruleset specified, assume landlock disabled. + if ll.Ruleset == nil || len(ll.Ruleset.HandledAccessFS) == 0 { + return nil, nil + } + + newConfig := &configs.Landlock{ + Ruleset: new(configs.Ruleset), + Rules: &configs.Rules{ + PathBeneath: []*configs.RulePathBeneath{}, + }, + DisableBestEffort: ll.DisableBestEffort, + } + + for _, access := range ll.Ruleset.HandledAccessFS { + newAccessFS, err := landlock.ConvertStringToAccessFSSet(string(access)) + if err != nil { + return nil, err + } + newConfig.Ruleset.HandledAccessFS |= newAccessFS + } + + // Loop through all Landlock path beneath rule blocks and convert them to libcontainer format. + for _, rulePath := range ll.Rules.PathBeneath { + if len(rulePath.AllowedAccess) > 0 { + newRule := configs.RulePathBeneath{ + AllowedAccess: 0, + Paths: rulePath.Paths, + } + + for _, access := range rulePath.AllowedAccess { + newAllowedAccess, err := landlock.ConvertStringToAccessFSSet(string(access)) + if err != nil { + return nil, err + } + newRule.AllowedAccess |= newAllowedAccess + } + + newConfig.Rules.PathBeneath = append(newConfig.Rules.PathBeneath, &newRule) + } + } + + return newConfig, nil +} + func SetupSeccomp(config *specs.LinuxSeccomp) (*configs.Seccomp, error) { if config == nil { return nil, nil diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go index 963d803a6a2..63361106acd 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -2,10 +2,12 @@ package specconv import ( "os" + "reflect" "strings" "testing" dbus "github.com/godbus/dbus/v5" + ll "github.com/landlock-lsm/go-landlock/landlock" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/configs/validate" "github.com/opencontainers/runc/libcontainer/devices" @@ -185,6 +187,107 @@ func TestSetupSeccompWrongArchitecture(t *testing.T) { } } +func TestSetupLandlock(t *testing.T) { + conf := &specs.Landlock{ + Ruleset: &specs.LandlockRuleset{ + HandledAccessFS: []specs.LandlockFSAction{ + specs.FSActExecute, + specs.FSActWriteFile, + specs.FSActReadFile, + specs.FSActReadDir, + specs.FSActRemoveDir, + specs.FSActRemoveFile, + specs.FSActMakeChar, + specs.FSActMakeDir, + specs.FSActMakeReg, + specs.FSActMakeSock, + specs.FSActMakeFifo, + specs.FSActMakeBlock, + specs.FSActMakeSym, + }, + }, + Rules: &specs.LandlockRules{ + PathBeneath: []specs.LandlockRulePathBeneath{ + { + AllowedAccess: []specs.LandlockFSAction{ + specs.FSActExecute, + specs.FSActReadFile, + specs.FSActReadDir, + }, + Paths: []string{ + "/usr", + "/bin", + }, + }, + { + AllowedAccess: []specs.LandlockFSAction{ + specs.FSActExecute, + specs.FSActWriteFile, + specs.FSActReadFile, + specs.FSActRemoveFile, + specs.FSActMakeChar, + specs.FSActMakeReg, + specs.FSActMakeSock, + specs.FSActMakeFifo, + specs.FSActMakeBlock, + specs.FSActMakeSym, + }, + Paths: []string{ + "/tmp", + }, + }, + }, + }, + DisableBestEffort: false, + } + + landlock, err := SetupLandlock(conf) + if err != nil { + t.Errorf("Couldn't create Landlock config: %v", err) + } + + // Execute | WriteFile | ReadFile | ReadDir | RemoveDir | RemoveFile | MakeChar | + // MakeDir | MakeReg | MakeSock | MakeFifo | MakeBlock | MakeSym + expectedRulesetAccess := ll.AccessFSSet(0x1FFF) + ruleset := landlock.Ruleset + if ruleset.HandledAccessFS != expectedRulesetAccess { + t.Errorf("Expected ruleset not found, expected %v, got: %v", + expectedRulesetAccess, ruleset.HandledAccessFS) + } + + pathRules := landlock.Rules.PathBeneath + + pathRulesLength := len(pathRules) + if pathRulesLength != 2 { + t.Errorf("Expected 2 path beneath rules, got :%d", pathRulesLength) + } + + expectedPathRulesAccess := []configs.RulePathBeneath{ + { + // Execute | ReadFile | ReadDir + AllowedAccess: 0xD, + Paths: []string{"/usr", "/bin"}, + }, + { + // Execute | WriteFile | ReadFile | RemoveFile | MakeChar | MakeReg | MakeSock | MakeFifo | + // MakeBlock | MakeSym + AllowedAccess: 0x1F67, + Paths: []string{"/tmp"}, + }, + } + + for i, rule := range pathRules { + if !reflect.DeepEqual(*rule, expectedPathRulesAccess[i]) { + t.Errorf("Wrong rule conversion for the rule %d under test, expected %v, got: %v", + i, expectedPathRulesAccess[i], rule) + } + } + + if landlock.DisableBestEffort { + t.Error("Wrong conversion for DisableBestEffort") + } +} + func TestSetupSeccomp(t *testing.T) { errnoRet := uint(55) conf := &specs.LinuxSeccomp{ diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go index 6dfea99983a..2d0be66a57c 100644 --- a/libcontainer/standard_init_linux.go +++ b/libcontainer/standard_init_linux.go @@ -16,6 +16,7 @@ import ( "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/keys" + "github.com/opencontainers/runc/libcontainer/landlock" "github.com/opencontainers/runc/libcontainer/seccomp" "github.com/opencontainers/runc/libcontainer/system" ) @@ -231,6 +232,13 @@ func (l *linuxStandardInit) Init() error { // https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318 _ = unix.Close(l.fifoFd) + // NoNewPrivileges must be enabled to use Landlock. + if l.config.Config.Landlock != nil && l.config.NoNewPrivileges { + if err := landlock.InitLandlock(l.config.Config.Landlock); err != nil { + return fmt.Errorf("unable to init Landlock: %w", err) + } + } + s := l.config.SpecState s.Pid = unix.Getpid() s.Status = specs.StateCreated