From c78364365868a71d8b418712c0ef890fd87eda06 Mon Sep 17 00:00:00 2001 From: Luca Bruno Date: Mon, 30 May 2016 19:34:45 +0200 Subject: [PATCH] ace: initial seccomp proposal --- actool/manifest.go | 56 +++++++- examples/image.json | 4 + schema/types/isolator_linux_specific.go | 128 +++++++++++++++++++ schema/types/isolator_linux_specific_test.go | 110 ++++++++++++++++ spec/ace.md | 63 ++++++++- 5 files changed, 358 insertions(+), 3 deletions(-) diff --git a/actool/manifest.go b/actool/manifest.go index 46662167..1a405efb 100644 --- a/actool/manifest.go +++ b/actool/manifest.go @@ -51,6 +51,8 @@ var ( patchMounts string patchPorts string patchIsolators string + patchSeccompMode string + patchSeccompSet string catPrettyPrint bool @@ -69,6 +71,8 @@ var ( [--ports=query,protocol=tcp,port=8080[:query2,...]] [--supplementary-groups=gid1,gid2,...] [--isolators=resource/cpu,request=50m,limit=100m[:resource/memory,...]] + [--seccomp-mode=remove|retain[,errno=EPERM]] + [--seccomp-set=syscall1,syscall2,...]] [--replace] INPUT_ACI_FILE [OUTPUT_ACI_FILE]`, @@ -99,6 +103,8 @@ func init() { cmdPatchManifest.Flags.StringVar(&patchMounts, "mounts", "", "Replace mount points") cmdPatchManifest.Flags.StringVar(&patchPorts, "ports", "", "Replace ports") cmdPatchManifest.Flags.StringVar(&patchIsolators, "isolators", "", "Replace isolators") + cmdPatchManifest.Flags.StringVar(&patchSeccompMode, "seccomp-mode", "", "Enable and configure seccomp isolator") + cmdPatchManifest.Flags.StringVar(&patchSeccompSet, "seccomp-set", "", "Set of syscalls for seccomp isolator enforcing") cmdCatManifest.Flags.BoolVar(&catPrettyPrint, "pretty-print", false, "Print with better style") } @@ -250,6 +256,48 @@ func patchManifest(im *schema.ImageManifest) error { } } + if patchSeccompMode != "" { + var errno, mode string + retainIsolator := app.Isolators.GetByName(types.LinuxSeccompRetainSetName) + removeIsolator := app.Isolators.GetByName(types.LinuxSeccompRemoveSetName) + if removeIsolator != nil || retainIsolator != nil { + return fmt.Errorf("a seccomp isolator already exists") + } + + args := strings.Split(patchSeccompMode, ",") + for _, a := range args { + kv := strings.Split(a, "=") + switch len(kv) { + case 1: + // mode, either "remove" or "retain" + mode = kv[0] + case 2: + // k=v argument, only "errno" allowed for now + if kv[0] == "errno" { + errno = kv[1] + } + default: + return fmt.Errorf("cannot parse seccomp-mode argument: %s", a) + } + } + + // Instantiate a Isolator with the content specified by the --seccomp-set parameter. + var err error + var seccomp types.AsIsolator + switch mode { + case "remove": + seccomp, err = types.NewLinuxSeccompRemoveSet(errno, strings.Split(patchSeccompSet, ",")...) + case "retain": + seccomp, err = types.NewLinuxSeccompRetainSet(errno, strings.Split(patchSeccompSet, ",")...) + default: + err = fmt.Errorf("Unknown seccomp mode %s", mode) + } + if err != nil { + return fmt.Errorf("cannot parse seccomp isolator: %s", err) + } + app.Isolators = append(app.Isolators, seccomp.AsIsolator()) + } + if patchIsolators != "" { isolators := strings.Split(patchIsolators, ":") for _, is := range isolators { @@ -260,14 +308,18 @@ func patchManifest(im *schema.ImageManifest) error { _, ok := types.ResourceIsolatorNames[name] - if name == types.LinuxNoNewPrivilegesName { + switch name { + case types.LinuxNoNewPrivilegesName: ok = true kv := strings.Split(is, ",") if len(kv) != 2 { return fmt.Errorf("isolator %s: invalid format", name) } - isolatorStr = fmt.Sprintf(`{ "name": "%s", "value": %s }`, name, kv[1]) + case types.LinuxSeccompRemoveSetName: + fallthrough + case types.LinuxSeccompRetainSetName: + ok = false } if !ok { diff --git a/examples/image.json b/examples/image.json index 7f9a4c72..06ad02d3 100644 --- a/examples/image.json +++ b/examples/image.json @@ -60,6 +60,10 @@ { "name": "os/linux/no-new-privileges", "value": true + }, + { + "name": "os/linux/seccomp-remove-set", + "value": {"errno": "EACCESS", "set": ["clock_settime", "clock_adjtime", "reboot"]} } ], "mountPoints": [ diff --git a/schema/types/isolator_linux_specific.go b/schema/types/isolator_linux_specific.go index 678e0bf1..bbee5ecb 100644 --- a/schema/types/isolator_linux_specific.go +++ b/schema/types/isolator_linux_specific.go @@ -17,12 +17,15 @@ package types import ( "encoding/json" "errors" + "unicode" ) const ( LinuxCapabilitiesRetainSetName = "os/linux/capabilities-retain-set" LinuxCapabilitiesRevokeSetName = "os/linux/capabilities-remove-set" LinuxNoNewPrivilegesName = "os/linux/no-new-privileges" + LinuxSeccompRemoveSetName = "os/linux/seccomp-remove-set" + LinuxSeccompRetainSetName = "os/linux/seccomp-retain-set" ) var LinuxIsolatorNames = make(map[ACIdentifier]struct{}) @@ -32,6 +35,8 @@ func init() { LinuxCapabilitiesRevokeSetName: func() IsolatorValue { return &LinuxCapabilitiesRevokeSet{} }, LinuxCapabilitiesRetainSetName: func() IsolatorValue { return &LinuxCapabilitiesRetainSet{} }, LinuxNoNewPrivilegesName: func() IsolatorValue { v := LinuxNoNewPrivileges(false); return &v }, + LinuxSeccompRemoveSetName: func() IsolatorValue { return &LinuxSeccompRemoveSet{} }, + LinuxSeccompRetainSetName: func() IsolatorValue { return &LinuxSeccompRetainSet{} }, } { AddIsolatorName(name, LinuxIsolatorNames) AddIsolatorValueConstructor(name, con) @@ -56,6 +61,10 @@ func (l *LinuxNoNewPrivileges) UnmarshalJSON(b []byte) error { return nil } +type AsIsolator interface { + AsIsolator() Isolator +} + type LinuxCapabilitiesSet interface { Set() []LinuxCapability AssertValid() error @@ -161,3 +170,122 @@ func (l LinuxCapabilitiesRevokeSet) AsIsolator() Isolator { value: &l, } } + +type LinuxSeccompSet interface { + Set() []LinuxSeccompEntry + Errno() LinuxSeccompErrno + AssertValid() error +} + +type LinuxSeccompEntry string +type LinuxSeccompErrno string + +type linuxSeccompValue struct { + Set []LinuxSeccompEntry `json:"set"` + Errno LinuxSeccompErrno `json:"errno"` +} + +type linuxSeccompBase struct { + val linuxSeccompValue +} + +func (l linuxSeccompBase) AssertValid() error { + if l.val.Errno != "" { + for _, c := range l.val.Errno { + if !unicode.IsUpper(c) { + return errors.New("invalid errno") + } + } + } + return nil +} + +func (l *linuxSeccompBase) UnmarshalJSON(b []byte) error { + var v linuxSeccompValue + err := json.Unmarshal(b, &v) + if err != nil { + return err + } + + l.val = v + + return err +} + +func (l linuxSeccompBase) Set() []LinuxSeccompEntry { + return l.val.Set +} + +func (l linuxSeccompBase) Errno() LinuxSeccompErrno { + return l.val.Errno +} + +type LinuxSeccompRetainSet struct { + linuxSeccompBase +} + +func NewLinuxSeccompRetainSet(errno string, syscall ...string) (*LinuxSeccompRetainSet, error) { + l := LinuxSeccompRetainSet{ + linuxSeccompBase{ + linuxSeccompValue{ + make([]LinuxSeccompEntry, len(syscall)), + LinuxSeccompErrno(errno), + }, + }, + } + for i, c := range syscall { + l.linuxSeccompBase.val.Set[i] = LinuxSeccompEntry(c) + } + if err := l.AssertValid(); err != nil { + return nil, err + } + return &l, nil +} + +func (l LinuxSeccompRetainSet) AsIsolator() Isolator { + b, err := json.Marshal(l.linuxSeccompBase.val) + if err != nil { + panic(err) + } + rm := json.RawMessage(b) + return Isolator{ + Name: LinuxSeccompRetainSetName, + ValueRaw: &rm, + value: &l, + } +} + +type LinuxSeccompRemoveSet struct { + linuxSeccompBase +} + +func NewLinuxSeccompRemoveSet(errno string, syscall ...string) (*LinuxSeccompRemoveSet, error) { + l := LinuxSeccompRemoveSet{ + linuxSeccompBase{ + linuxSeccompValue{ + make([]LinuxSeccompEntry, len(syscall)), + LinuxSeccompErrno(errno), + }, + }, + } + for i, c := range syscall { + l.linuxSeccompBase.val.Set[i] = LinuxSeccompEntry(c) + } + if err := l.AssertValid(); err != nil { + return nil, err + } + return &l, nil +} + +func (l LinuxSeccompRemoveSet) AsIsolator() Isolator { + b, err := json.Marshal(l.linuxSeccompBase.val) + if err != nil { + panic(err) + } + rm := json.RawMessage(b) + return Isolator{ + Name: LinuxSeccompRemoveSetName, + ValueRaw: &rm, + value: &l, + } +} diff --git a/schema/types/isolator_linux_specific_test.go b/schema/types/isolator_linux_specific_test.go index 04b373d3..4b8bf67c 100644 --- a/schema/types/isolator_linux_specific_test.go +++ b/schema/types/isolator_linux_specific_test.go @@ -94,3 +94,113 @@ func TestNewLinuxCapabilitiesRevokeSet(t *testing.T) { } } + +func TestNewLinuxSeccompRemoveSet(t *testing.T) { + tests := []struct { + set []string + errno string + + wset []LinuxSeccompEntry + werrno LinuxSeccompErrno + werr bool + }{ + { + []string{}, + "-EPERM", + nil, + "", + true, + }, + { + []string{"@all"}, + "EACCESS", + []LinuxSeccompEntry{"@all"}, + LinuxSeccompErrno("EACCESS"), + false, + }, + { + []string{"chmod", "chown"}, + "", + []LinuxSeccompEntry{"chmod", "chown"}, + LinuxSeccompErrno(""), + false, + }, + { + []string{}, + "", + []LinuxSeccompEntry{}, + LinuxSeccompErrno(""), + false, + }, + } + for i, tt := range tests { + c, err := NewLinuxSeccompRemoveSet(tt.errno, tt.set...) + if tt.werr { + if err == nil { + t.Errorf("#%d: did not get expected error", i) + } + continue + } + if gset := c.Set(); !reflect.DeepEqual(gset, tt.wset) { + t.Errorf("#%d: got set %#v, expected set %#v", i, gset, tt.wset) + } + if gerrno := c.Errno(); !reflect.DeepEqual(gerrno, tt.werrno) { + t.Errorf("#%d: got errno %#v, expected errno %#v", i, gerrno, tt.werrno) + } + } +} + +func TestNewLinuxSeccompRetainSet(t *testing.T) { + tests := []struct { + set []string + errno string + + wset []LinuxSeccompEntry + werrno LinuxSeccompErrno + werr bool + }{ + { + []string{}, + "eaccess", + nil, + "", + true, + }, + { + []string{"chmod"}, + "EACCESS", + []LinuxSeccompEntry{"chmod"}, + LinuxSeccompErrno("EACCESS"), + false, + }, + { + []string{"chmod", "chown"}, + "", + []LinuxSeccompEntry{"chmod", "chown"}, + LinuxSeccompErrno(""), + false, + }, + { + []string{}, + "", + []LinuxSeccompEntry{}, + LinuxSeccompErrno(""), + false, + }, + } + for i, tt := range tests { + c, err := NewLinuxSeccompRetainSet(tt.errno, tt.set...) + if tt.werr { + if err == nil { + t.Errorf("#%d: did not get expected error", i) + } + continue + } + if gset := c.Set(); !reflect.DeepEqual(gset, tt.wset) { + t.Errorf("#%d: got set %#v, expected set %#v", i, gset, tt.wset) + } + if gerrno := c.Errno(); !reflect.DeepEqual(gerrno, tt.werrno) { + t.Errorf("#%d: got errno %#v, expected errno %#v", i, gerrno, tt.werrno) + } + } +} diff --git a/spec/ace.md b/spec/ace.md index 296257d7..fc6dc283 100644 --- a/spec/ace.md +++ b/spec/ace.md @@ -131,7 +131,68 @@ An executor MAY implement a "strict mode" where an image cannot run unless all i ### Linux Isolators These isolators are specific to the Linux kernel and are impossible to represent as a 1-to-1 mapping on other kernels. -The first example is "capabilities" but this will be expanded to include things such as SELinux, SMACK or AppArmor. +This set includes Linux-specific security isolators, which may relies on kernel technologies like seccomp, SELinux, SMACK or AppArmor. + +#### os/linux/seccomp-remove-set + +* Scope: app + +**Parameters:** + +* **set** case-sensitive list of syscall names that will be blacklisted (ie. blocked); values starting with `@` MUST be handled as scoped special values (see notes below). All syscalls specified in this set MUST be blocked. This set MAY be augmented by an implementation-specific default blacklist set (see notes below). Syscalls not specified in the union of these two sets MUST NOT be blocked. +* **errno** all-uppercase name of a single [errno code](http://man7.org/linux/man-pages/man3/errno.3.html) that will be returned by blocked system calls, instead of terminating. If missing or empty, by default blocked syscalls MUST result in app termination via `SIGSYS` signal. + +**Notes:** + 1. The `os/linux/seccomp-remove-set` isolator cannot be used in conjunction with the `os/linux/seccomp-retain-set` isolator. + 2. When an app does not have any seccomp isolator (neither `os/linux/seccomp-remove-set` nor `os/linux/seccomp-retain-set` are specified), implementations MAY apply their own set of blocked syscalls. + 3. The implementation-specific default blacklist SHOULD be a security-focused set of syscalls typically not required by apps. For example, implementations may require the `reboot(2)` syscall to be always blocked. This set MAY be empty. + 4. Values starting with `@` identify special wildcards and are scoped by the `/` separator. The `@appc/` scope is reserved for usage in this specification, implementations MAY provide additional wildcards in their own namespace (eg. `@implementation/wildcard`). The following special values are currently defined: + * `@appc/all` represents the set of all available syscalls. + + +**Example:** +```json +"name": "os/linux/seccomp-remove-set", +"value": { + "errno": "ENOTSUP", + "set": [ + "chown", + "chmod" + ] +} +``` + +In the example above, the process will not be allowed to invoke `chown(2)` and `chmod(2)` (and any other syscall in the implementation-specific blacklist). When invoked, such syscalls will immediately return a `ENOTSUP` error code. All other syscalls will behave as usual. + +#### os/linux/seccomp-retain-set + +* Scope: app + +**Parameters:** + +* **set** case-sensitive list of syscall names that will be whitelisted (ie. not blocked). All syscalls specified in this set MUST NOT be blocked. This set MAY be augmented by an implementation-specific default whitelist set (see notes below). Syscalls not specified in the union of these two sets MUST be blocked. +* **errno** all-uppercase name of a single [errno code](http://man7.org/linux/man-pages/man3/errno.3.html) that will be returned by blocked system calls, instead of terminating. If missing or empty string, by default blocked syscalls MUST result in app termination via `SIGSYS` signal. + +**Notes:** + 1. The `os/linux/seccomp-retain-set` isolator cannot be used in conjunction with the `os/linux/seccomp-remove-set` isolator. + 2. When an app does not have any seccomp isolator (neither `os/linux/seccomp-remove-set` nor `os/linux/seccomp-retain-set` are specified), implementations MAY apply their own set of blocked syscalls. + 3. The implementation-specific default whitelist MUST be the minimum set of syscalls required for app life-cycle. For example, implementations may require the `exit(2)` syscall to be always allowed for clean app termination. This set MAY be empty. + 4. Values starting with `@` identify special wildcards and are scoped by the `/` separator. The `@appc/` top-level scope is reserved for usage in this specification. Implementations MAY provide additional wildcards in their own namespace (eg. `@implementation/wildcard`). The following special values are currently defined: + * `@appc/all` represents the set of all available syscalls. + +**Example:** +```json +"name": "os/linux/seccomp-retain-set", +"value": { + "errno": "", + "set": [ + "chown", + "chmod" + ] +} +``` + +In the example above, the process will be only allowed to invoke `chown(2)` and `chmod(2)` (and any other syscall in the implementation-specific whitelist). All other syscalls will result in app termination via `SIGSYS` signal. #### os/linux/capabilities-remove-set