diff --git a/container/container_unix.go b/container/container_unix.go index 53c19fc2a9..6c7d30a379 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -51,6 +51,7 @@ type Container struct { ShmPath string MqueuePath string ResolvConfPath string + SeccompProfile string } // CreateDaemonEnvironment returns the list of all environment variables given the list of diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go index f660bd7593..d735cda8c7 100644 --- a/daemon/container_operations_unix.go +++ b/daemon/container_operations_unix.go @@ -236,6 +236,7 @@ func (daemon *Daemon) populateCommand(c *container.Container, env []string) erro Pid: pid, ReadonlyRootfs: c.HostConfig.ReadonlyRootfs, RemappedRoot: remappedRoot, + SeccompProfile: c.SeccompProfile, UIDMapping: uidMap, UTS: uts, } diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go index de59c224fb..5f6cc43369 100644 --- a/daemon/daemon_test.go +++ b/daemon/daemon_test.go @@ -152,6 +152,16 @@ func TestParseSecurityOpt(t *testing.T) { t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", container.AppArmorProfile) } + // test seccomp + sp := "/path/to/seccomp_test.json" + config.SecurityOpt = []string{"seccomp:" + sp} + if err := parseSecurityOpt(container, config); err != nil { + t.Fatalf("Unexpected parseSecurityOpt error: %v", err) + } + if container.SeccompProfile != sp { + t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, container.SeccompProfile) + } + // test valid label config.SecurityOpt = []string{"label:user:USER"} if err := parseSecurityOpt(container, config); err != nil { diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go old mode 100644 new mode 100755 index 202cdb22db..ddd7f2367b --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -74,6 +74,8 @@ func parseSecurityOpt(container *container.Container, config *runconfig.HostConf labelOpts = append(labelOpts, con[1]) case "apparmor": container.AppArmorProfile = con[1] + case "seccomp": + container.SeccompProfile = con[1] default: return fmt.Errorf("Invalid --security-opt: %q", opt) } diff --git a/daemon/execdriver/driver_unix.go b/daemon/execdriver/driver_unix.go index eb4482f8e0..8f18f78fc7 100644 --- a/daemon/execdriver/driver_unix.go +++ b/daemon/execdriver/driver_unix.go @@ -118,6 +118,7 @@ type Command struct { Pid *Pid `json:"pid"` ReadonlyRootfs bool `json:"readonly_rootfs"` RemappedRoot *User `json:"remap_root"` + SeccompProfile string `json:"seccomp_profile"` UIDMapping []idtools.IDMap `json:"uidmapping"` UTS *UTS `json:"uts"` } diff --git a/daemon/execdriver/native/create.go b/daemon/execdriver/native/create.go index a3d33d98ce..d0da544588 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -19,8 +19,8 @@ import ( // createContainer populates and configures the container type with the // data provided by the execdriver.Command -func (d *Driver) createContainer(c *execdriver.Command, hooks execdriver.Hooks) (*configs.Config, error) { - container := execdriver.InitContainer(c) +func (d *Driver) createContainer(c *execdriver.Command, hooks execdriver.Hooks) (container *configs.Config, err error) { + container = execdriver.InitContainer(c) if err := d.createIpc(container, c); err != nil { return nil, err @@ -82,6 +82,12 @@ func (d *Driver) createContainer(c *execdriver.Command, hooks execdriver.Hooks) container.AppArmorProfile = c.AppArmorProfile } + if c.SeccompProfile != "" { + container.Seccomp, err = loadSeccompProfile(c.SeccompProfile) + if err != nil { + return nil, err + } + } if err := execdriver.SetupCgroups(container, c); err != nil { return nil, err } diff --git a/daemon/execdriver/native/seccomp.go b/daemon/execdriver/native/seccomp.go new file mode 100644 index 0000000000..b6c5ae0d7d --- /dev/null +++ b/daemon/execdriver/native/seccomp.go @@ -0,0 +1,94 @@ +// +build linux + +package native + +import ( + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/opencontainers/runc/libcontainer/configs" + "github.com/opencontainers/runc/libcontainer/seccomp" + "github.com/opencontainers/specs" +) + +func loadSeccompProfile(path string) (*configs.Seccomp, error) { + f, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("Opening seccomp profile failed: %v", err) + } + + var config specs.Seccomp + if err := json.Unmarshal(f, &config); err != nil { + return nil, fmt.Errorf("Decoding seccomp profile failed: %v", err) + } + + return setupSeccomp(&config) +} + +func setupSeccomp(config *specs.Seccomp) (newConfig *configs.Seccomp, err error) { + if config == nil { + return nil, nil + } + + // No default action specified, no syscalls listed, assume seccomp disabled + if config.DefaultAction == "" && len(config.Syscalls) == 0 { + return nil, nil + } + + newConfig = new(configs.Seccomp) + newConfig.Syscalls = []*configs.Syscall{} + + // if config.Architectures == 0 then libseccomp will figure out the architecture to use + if len(config.Architectures) > 0 { + newConfig.Architectures = []string{} + for _, arch := range config.Architectures { + newArch, err := seccomp.ConvertStringToArch(string(arch)) + if err != nil { + return nil, err + } + newConfig.Architectures = append(newConfig.Architectures, newArch) + } + } + + // Convert default action from string representation + newConfig.DefaultAction, err = seccomp.ConvertStringToAction(string(config.DefaultAction)) + if err != nil { + return nil, err + } + + // Loop through all syscall blocks and convert them to libcontainer format + for _, call := range config.Syscalls { + newAction, err := seccomp.ConvertStringToAction(string(call.Action)) + if err != nil { + return nil, err + } + + newCall := configs.Syscall{ + Name: call.Name, + Action: newAction, + Args: []*configs.Arg{}, + } + + // Loop through all the arguments of the syscall and convert them + for _, arg := range call.Args { + newOp, err := seccomp.ConvertStringToOperator(string(arg.Op)) + if err != nil { + return nil, err + } + + newArg := configs.Arg{ + Index: arg.Index, + Value: arg.Value, + ValueTwo: arg.ValueTwo, + Op: newOp, + } + + newCall.Args = append(newCall.Args, &newArg) + } + + newConfig.Syscalls = append(newConfig.Syscalls, &newCall) + } + + return newConfig, nil +} diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 1e847d805e..528c0fa7c7 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -3789,3 +3789,59 @@ func (s *DockerSuite) TestRunWithOomScoreAdjInvalidRange(c *check.C) { c.Fatalf("Expected output to contain %q, got %q instead", expected, out) } } + +// TestRunSeccompProfileDenyUnshare checks that 'docker run --security-opt seccomp:/tmp/profile.json jess/unshare unshare' exits with operation not permitted. +func (s *DockerSuite) TestRunSeccompProfileDenyUnshare(c *check.C) { + testRequires(c, SameHostDaemon) + jsonData := `{ + "defaultAction": "SCMP_ACT_ALLOW", + "syscalls": [ + { + "name": "unshare", + "action": "SCMP_ACT_ERRNO" + } + ] +}` + tmpFile, err := ioutil.TempFile("", "profile.json") + defer tmpFile.Close() + if err != nil { + c.Fatal(err) + } + + if _, err := tmpFile.Write([]byte(jsonData)); err != nil { + c.Fatal(err) + } + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "seccomp:"+tmpFile.Name(), "jess/unshare", "unshare", "-p", "-m", "-f", "-r", "mount", "-t", "proc", "none", "/proc") + out, _, _ := runCommandWithOutput(runCmd) + if !strings.Contains(out, "Operation not permitted") { + c.Fatalf("expected unshare with seccomp profile denied to fail, got %s", out) + } +} + +// TestRunSeccompProfileDenyChmod checks that 'docker run --security-opt seccomp:/tmp/profile.json busybox chmod 400 /etc/hostname' exits with operation not permitted. +func (s *DockerSuite) TestRunSeccompProfileDenyChmod(c *check.C) { + testRequires(c, SameHostDaemon) + jsonData := `{ + "defaultAction": "SCMP_ACT_ALLOW", + "syscalls": [ + { + "name": "chmod", + "action": "SCMP_ACT_ERRNO" + } + ] +}` + tmpFile, err := ioutil.TempFile("", "profile.json") + defer tmpFile.Close() + if err != nil { + c.Fatal(err) + } + + if _, err := tmpFile.Write([]byte(jsonData)); err != nil { + c.Fatal(err) + } + runCmd := exec.Command(dockerBinary, "run", "--security-opt", "seccomp:"+tmpFile.Name(), "busybox", "chmod", "400", "/etc/hostname") + out, _, _ := runCommandWithOutput(runCmd) + if !strings.Contains(out, "Operation not permitted") { + c.Fatalf("expected chmod with seccomp profile denied to fail, got %s", out) + } +}