diff --git a/go.mod b/go.mod index a45e2da843c..fda93a91b9c 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-20210828133255-ec6c6b87a946 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 7937c033bdb..975be3c061f 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-20210828133255-ec6c6b87a946 h1:RRTOwBnwZR4a3IMyPq1uchxJcrLKWF4NTCHB2fbvo5Y= +github.com/landlock-lsm/go-landlock v0.0.0-20210828133255-ec6c6b87a946/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 2dad5c8b575..6595fc7fa75 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -78,6 +78,53 @@ 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 AccessFS `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 AccessFS `json:"allowedAccess"` + Paths []string `json:"paths"` +} + +// AccessFS is taken upon ruleset and rule setup in Landlock. +type AccessFS uint64 + +// Landlock access rights for FS. +// +// Please see the full documentation at +// https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights. +const ( + Execute AccessFS = (1 << 0) + WriteFile AccessFS = (1 << 1) + ReadFile AccessFS = (1 << 2) + ReadDir AccessFS = (1 << 3) + RemoveDir AccessFS = (1 << 4) + RemoveFile AccessFS = (1 << 5) + MakeChar AccessFS = (1 << 6) + MakeDir AccessFS = (1 << 7) + MakeReg AccessFS = (1 << 8) + MakeSock AccessFS = (1 << 9) + MakeFifo AccessFS = (1 << 10) + MakeBlock AccessFS = (1 << 11) + MakeSym AccessFS = (1 << 12) +) + // TODO Windows. Many of these fields should be factored out into those parts // which are common across platforms, and those which are platform specific. @@ -206,6 +253,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..c91ef4d3720 --- /dev/null +++ b/libcontainer/landlock/config.go @@ -0,0 +1,31 @@ +package landlock + +import ( + "fmt" + + "github.com/opencontainers/runc/libcontainer/configs" +) + +var accessFSs = map[string]configs.AccessFS{ + "execute": configs.Execute, + "write_file": configs.WriteFile, + "read_file": configs.ReadFile, + "read_dir": configs.ReadDir, + "remove_dir": configs.RemoveDir, + "remove_file": configs.RemoveFile, + "make_char": configs.MakeChar, + "make_dir": configs.MakeDir, + "make_reg": configs.MakeReg, + "make_sock": configs.MakeSock, + "make_fifo": configs.MakeFifo, + "make_block": configs.MakeBlock, + "make_sym": configs.MakeSym, +} + +// ConvertStringToAccessFS converts a string into a Landlock access right. +func ConvertStringToAccessFS(in string) (configs.AccessFS, error) { + if access, ok := accessFSs[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_linux.go b/libcontainer/landlock/landlock_linux.go new file mode 100644 index 00000000000..158f23cc126 --- /dev/null +++ b/libcontainer/landlock/landlock_linux.go @@ -0,0 +1,67 @@ +// +build linux + +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") + } + + var llConfig landlock.Config + + ruleset := getAccess(config.Ruleset.HandledAccessFS) + // Panic on error when constructing the Landlock configuration using invalid config values. + if config.DisableBestEffort { + llConfig = landlock.MustConfig(ruleset) + } else { + llConfig = landlock.MustConfig(ruleset).BestEffort() + } + + if err := llConfig.RestrictPaths( + getPathAccesses(config.Rules)..., + ); err != nil { + return fmt.Errorf("Could not restrict paths: %v", err) + } + + return nil +} + +// Convert Libcontainer AccessFS to go-landlock AccessFSSet. +func getAccess(access configs.AccessFS) landlock.AccessFSSet { + return landlock.AccessFSSet(access) +} + +// Convert Libcontainer RulePathBeneath to go-landlock PathOpt. +func getPathAccess(rule *configs.RulePathBeneath) landlock.PathOpt { + return landlock.PathAccess( + getAccess(rule.AllowedAccess), + rule.Paths...) +} + +// Convert Libcontainer Rules to an array of go-landlock PathOpt. +func getPathAccesses(rules *configs.Rules) []landlock.PathOpt { + pathAccesses := []landlock.PathOpt{} + + for _, rule := range rules.PathBeneath { + opt := getPathAccess(rule) + pathAccesses = append(pathAccesses, opt) + } + + return pathAccesses +} diff --git a/libcontainer/landlock/landlock_unsupported.go b/libcontainer/landlock/landlock_unsupported.go new file mode 100644 index 00000000000..e2fe222ee99 --- /dev/null +++ b/libcontainer/landlock/landlock_unsupported.go @@ -0,0 +1,19 @@ +// +build !linux + +package landlock + +import ( + "errors" + + "github.com/opencontainers/runc/libcontainer/configs" +) + +var ErrLandlockNotSupported = errors.New("land: config provided but Landlock not supported") + +// InitLandlock does nothing because Landlock is not supported. +func InitSLandlock(config *configs.Landlock) error { + if config != nil { + return ErrLandlockNotSupported + } + return nil +} diff --git a/libcontainer/setns_init_linux.go b/libcontainer/setns_init_linux.go index 89b5609a6b6..1d9aaebfade 100644 --- a/libcontainer/setns_init_linux.go +++ b/libcontainer/setns_init_linux.go @@ -15,6 +15,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" ) @@ -83,6 +84,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 89172316358..f3ae2aa815c 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -18,6 +18,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" @@ -320,6 +321,14 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { Ambient: spec.Process.Capabilities.Ambient, } } + if spec.Process.Landlock != nil { + landlock, err := SetupLandlock(spec.Process.Landlock) + if err != nil { + return nil, err + } + config.Landlock = landlock + } + } createHooks(spec, config) config.Version = specs.Version @@ -837,6 +846,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.ConvertStringToAccessFS(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.ConvertStringToAccessFS(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/standard_init_linux.go b/libcontainer/standard_init_linux.go index f1e20207ae0..6bceca0116b 100644 --- a/libcontainer/standard_init_linux.go +++ b/libcontainer/standard_init_linux.go @@ -18,6 +18,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" ) @@ -211,6 +212,14 @@ func (l *linuxStandardInit) Init() error { // since been resolved. // 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) + } + } + // 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).