From e9341f2076c8a89187109ac02cb727764156d107 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Wed, 1 Sep 2021 13:42:31 -0400 Subject: [PATCH] libcontainer: add support for Landlock This patch introduces Landlock Linux Security Module (LSM) support in runc, which was landed in Linux kernel 5.13. This allows unprivileged processes to create safe security sandboxes that can securely restrict the ambient rights (e.g. global filesystem access) for themselves. runtime-spec: https://github.com/opencontainers/runtime-spec/pull/1111 Fixes https://github.com/opencontainers/runc/issues/2859 Signed-off-by: Kailun Qin --- go.mod | 3 +- go.sum | 8 ++- libcontainer/configs/config.go | 51 ++++++++++++++ libcontainer/landlock/config.go | 31 +++++++++ libcontainer/landlock/landlock_linux.go | 67 +++++++++++++++++++ libcontainer/landlock/landlock_unsupported.go | 19 ++++++ libcontainer/setns_init_linux.go | 7 ++ libcontainer/specconv/spec_linux.go | 58 ++++++++++++++++ libcontainer/standard_init_linux.go | 8 +++ 9 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 libcontainer/landlock/config.go create mode 100644 libcontainer/landlock/landlock_linux.go create mode 100644 libcontainer/landlock/landlock_unsupported.go diff --git a/go.mod b/go.mod index 64d19452f6b..dfeb440c16c 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 186bf5354f9..39ac0e0726c 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 e0db8e01782..e6278a029a6 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -81,6 +81,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. @@ -209,6 +256,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 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..25c2b96434c 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,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 @@ -845,6 +854,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 6dfea99983a..15a1e7d7f1b 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