diff --git a/.travis.yml b/.travis.yml index 14f19f3e1..24e70b1e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ script: | make coverage elif [[ "${TEST_SUITE}" = "criv1alpha1test" ]]; then make build - TEST_FLAGS= make build-daemon-integration + TEST_FLAGS= BUILDTAGS="selinux seccomp apparmor" make build-daemon-integration sudo env "PATH=$PATH" make install sudo env "PATH=$PATH" make download-dependencies @@ -46,7 +46,7 @@ script: | make coverage elif [[ "${TEST_SUITE}" = "criv1alpha2test" ]]; then make build - TEST_FLAGS= make build-daemon-integration + TEST_FLAGS= BUILDTAGS="selinux seccomp apparmor" make build-daemon-integration sudo env "PATH=$PATH" make install sudo env "PATH=$PATH" make download-dependencies diff --git a/INSTALLATION.md b/INSTALLATION.md index d1b5ad6d9..8fd7addca 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -237,6 +237,22 @@ Makefile target named `build` will compile the pouch and pouchd binaries into cu make build && make install ``` +#### Build Tags + +Pouch not use build tags by default, if you want pouchd to support additional security options, build pouchd with tags like + +``` +make BUILDTAGS='seccomp apparmor selinux' +``` + +Supported build Tags + +| Tags | Description | +|----------|---------------------------------------------------------| +| seccomp | filter syscalls | +| apparmor | restrict program capabilities with per-program profiles | +| selinux | selinux process and mount label | + ### Start PouchContainer With all needed binaries installed, you could start pouchd via: diff --git a/Makefile b/Makefile index 8273b42ad..2d5a20404 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,8 @@ DEFAULT_LDFLAGS="-X ${VERSION_PKG}/version.GitCommit=${GIT_COMMIT} \ -X ${VERSION_PKG}/version.ApiVersion=${API_VERSION} \ -X ${VERSION_PKG}/version.BuildTime=${BUILD_TIME}" +GOBUILD_TAGS=$(if $(BUILDTAGS),-tags "$(BUILDTAGS)",) + # COVERAGE_PACKAGES is the coverage we care about. COVERAGE_PACKAGES=$(shell go list ./... | \ grep -v github.com/alibaba/pouch$$ | \ @@ -74,7 +76,7 @@ build: build-daemon build-cli ## build PouchContainer both daemon and cli binari build-daemon: modules plugin ## build PouchContainer daemon binary @echo "$@: bin/${DAEMON_BINARY_NAME}" @mkdir -p bin - @GOOS=linux go build -ldflags ${DEFAULT_LDFLAGS} -o bin/${DAEMON_BINARY_NAME} -tags 'selinux' + @GOOS=linux go build -ldflags ${DEFAULT_LDFLAGS} ${GOBUILD_TAGS} -o bin/${DAEMON_BINARY_NAME} build-cli: ## build PouchContainer cli binary @echo "$@: bin/${CLI_BINARY_NAME}" @@ -84,7 +86,7 @@ build-cli: ## build PouchContainer cli binary build-daemon-integration: modules plugin ## build PouchContainer daemon integration testing binary @echo $@ @mkdir -p bin - go test -c ${TEST_FLAGS} \ + go test -c ${TEST_FLAGS} ${GOBUILD_TAGS} \ -cover -covermode=atomic -coverpkg ${COVERAGE_PACKAGES_LIST} \ -o bin/${DAEMON_INTEGRATION_BINARY_NAME} diff --git a/daemon/mgr/container_types.go b/daemon/mgr/container_types.go index 943d87216..b5472a11a 100644 --- a/daemon/mgr/container_types.go +++ b/daemon/mgr/container_types.go @@ -30,6 +30,9 @@ var ( // DefaultStatsInterval is the interval configured for stats. DefaultStatsInterval = time.Duration(time.Second) + + // ProfileUnconfined means run a container without the default seccomp profile. + ProfileUnconfined = "unconfined" ) var ( diff --git a/daemon/mgr/container_utils.go b/daemon/mgr/container_utils.go index 530bcdd0e..679ca3a38 100644 --- a/daemon/mgr/container_utils.go +++ b/daemon/mgr/container_utils.go @@ -169,12 +169,6 @@ func parseSecurityOpts(c *Container, securityOpts []string) error { c.AppArmorProfile = value case "seccomp": c.SeccompProfile = value - case "no-new-privileges": - noNewPrivileges, err := strconv.ParseBool(value) - if err != nil { - return fmt.Errorf("invalid --security-opt: %q", securityOpt) - } - c.NoNewPrivileges = noNewPrivileges case "label": labelOpts = append(labelOpts, value) default: diff --git a/daemon/mgr/container_validation.go b/daemon/mgr/container_validation.go index 6a39edf59..cce2706c1 100644 --- a/daemon/mgr/container_validation.go +++ b/daemon/mgr/container_validation.go @@ -57,7 +57,23 @@ func (mgr *ContainerManager) validateConfig(c *Container, update bool) ([]string return warnings, err } - // TODO: validate config + // validate seccomp, apparmor security parameters + sysInfo := system.NewInfo() + if !sysInfo.Seccomp { + if c.SeccompProfile != "" || c.SeccompProfile != ProfileUnconfined { + warnings = append(warnings, fmt.Sprintf("Current Kernel does not support seccomp, discard --security-opt seccomp=%s", c.SeccompProfile)) + } + // always set SeccompProfile to unconfined if kernel not support seccomp + c.SeccompProfile = ProfileUnconfined + + } + if !sysInfo.AppArmor { + if c.AppArmorProfile != "" { + warnings = append(warnings, fmt.Sprintf("Current Kernel does not support apparmor, discard --security-opt apparmor=%s", c.AppArmorProfile)) + } + c.AppArmorProfile = "" + } + return warnings, nil } diff --git a/daemon/mgr/spec_apparmor_linux.go b/daemon/mgr/spec_apparmor_linux.go new file mode 100644 index 000000000..413c1f9f5 --- /dev/null +++ b/daemon/mgr/spec_apparmor_linux.go @@ -0,0 +1,28 @@ +// +build apparmor,linux + +package mgr + +import ( + "context" + + "github.com/opencontainers/runc/libcontainer/apparmor" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +func setupAppArmor(ctx context.Context, c *Container, s *specs.Spec) error { + if apparmor.IsEnabled() { + appArmorProfile := "" + if c.AppArmorProfile != "" { + appArmorProfile = c.AppArmorProfile + } else if c.HostConfig.Privileged { + appArmorProfile = "unconfined" + } else { + // TODO: generate pouch-default apparmor profile + // appArmorProfile = "pouch-default" + } + + s.Process.ApparmorProfile = appArmorProfile + } + + return nil +} diff --git a/daemon/mgr/spec_apparmor_linux_unsupported.go b/daemon/mgr/spec_apparmor_linux_unsupported.go new file mode 100644 index 000000000..3e9f79363 --- /dev/null +++ b/daemon/mgr/spec_apparmor_linux_unsupported.go @@ -0,0 +1,14 @@ +// +build !apparmor + +package mgr + +import ( + "context" + + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// do nothing in this case +func setupAppArmor(ctx context.Context, c *Container, s *specs.Spec) error { + return nil +} diff --git a/daemon/mgr/spec_linux.go b/daemon/mgr/spec_linux.go index f9d0f2657..5be7425b8 100644 --- a/daemon/mgr/spec_linux.go +++ b/daemon/mgr/spec_linux.go @@ -2,9 +2,7 @@ package mgr import ( "context" - "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" "strconv" @@ -14,7 +12,6 @@ import ( "github.com/alibaba/pouch/apis/opts" "github.com/alibaba/pouch/apis/types" - "github.com/containerd/containerd/contrib/seccomp" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/devices" specs "github.com/opencontainers/runtime-spec/specs-go" @@ -84,37 +81,6 @@ func populatePlatform(ctx context.Context, c *Container, specWrapper *SpecWrappe return setupNamespaces(ctx, c, specWrapper) } -// setupSeccomp creates seccomp security settings spec. -func setupSeccomp(ctx context.Context, c *Container, s *specs.Spec) error { - if c.HostConfig.Privileged { - return nil - } - - if s.Linux.Seccomp == nil { - s.Linux.Seccomp = &specs.LinuxSeccomp{} - } - - // TODO: check whether seccomp is enable in your kernel, if not, cannot run a custom seccomp prifle. - seccompProfile := c.SeccompProfile - switch seccompProfile { - case ProfileNameUnconfined: - return nil - case ProfilePouchDefault, "": - s.Linux.Seccomp = seccomp.DefaultProfile(s) - default: - data, err := ioutil.ReadFile(seccompProfile) - if err != nil { - return fmt.Errorf("failed to load seccomp profile %q: %v", seccompProfile, err) - } - err = json.Unmarshal(data, s.Linux.Seccomp) - if err != nil { - return fmt.Errorf("failed to decode seccomp profile %q: %v", seccompProfile, err) - } - } - - return nil -} - // setupResource creates linux resource spec. func setupResource(ctx context.Context, c *Container, s *specs.Spec) error { if s.Linux.Resources == nil { diff --git a/daemon/mgr/spec_process.go b/daemon/mgr/spec_process.go index 45a03eafe..e2308f634 100644 --- a/daemon/mgr/spec_process.go +++ b/daemon/mgr/spec_process.go @@ -3,8 +3,6 @@ package mgr import ( "context" "fmt" - "io/ioutil" - "os" "strings" "github.com/alibaba/pouch/apis/types" @@ -38,7 +36,6 @@ func setupProcess(ctx context.Context, c *Container, s *specs.Spec) error { if !c.HostConfig.Privileged { s.Process.SelinuxLabel = c.ProcessLabel s.Process.NoNewPrivileges = c.NoNewPrivileges - } if err := setupUser(ctx, c, s); err != nil { @@ -110,47 +107,6 @@ func setupCapabilities(ctx context.Context, hostConfig *types.HostConfig, s *spe return nil } -// isAppArmorEnabled returns true if apparmor is enabled for the host. -// This function is forked from -// https://github.com/opencontainers/runc/blob/1a81e9ab1f138c091fe5c86d0883f87716088527/libcontainer/apparmor/apparmor.go -// to avoid the libapparmor dependency. -func isAppArmorEnabled() bool { - if _, err := os.Stat("/sys/kernel/security/apparmor"); err == nil && os.Getenv("container") == "" { - if _, err = os.Stat("/sbin/apparmor_parser"); err == nil { - buf, err := ioutil.ReadFile("/sys/module/apparmor/parameters/enabled") - return err == nil && len(buf) > 1 && buf[0] == 'Y' - } - } - return false -} - -func setupAppArmor(ctx context.Context, c *Container, s *specs.Spec) error { - if !isAppArmorEnabled() { - // Return if the apparmor is disabled. - return nil - } - - appArmorProfile := c.AppArmorProfile - switch appArmorProfile { - case ProfileNameUnconfined: - return nil - case ProfileRuntimeDefault: - // TODO: handle runtime default case. - return nil - case "": - if c.HostConfig.Privileged { - return nil - } - // TODO: if user does not specify the AppArmor and the container is not in privilege mode, - // we need to specify it as default case, handle it later. - return nil - default: - s.Process.ApparmorProfile = appArmorProfile - } - - return nil -} - func setupRlimits(ctx context.Context, hostConfig *types.HostConfig, s *specs.Spec) error { var rlimits []specs.POSIXRlimit for _, ul := range hostConfig.Ulimits { diff --git a/daemon/mgr/spec_seccomp_linux.go b/daemon/mgr/spec_seccomp_linux.go new file mode 100644 index 000000000..c3519110e --- /dev/null +++ b/daemon/mgr/spec_seccomp_linux.go @@ -0,0 +1,43 @@ +// +build linux,seccomp + +package mgr + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/containerd/containerd/contrib/seccomp" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +// setupSeccomp creates seccomp security settings spec. +func setupSeccomp(ctx context.Context, c *Container, s *specs.Spec) error { + if c.HostConfig.Privileged { + return nil + } + + if s.Linux.Seccomp == nil { + s.Linux.Seccomp = &specs.LinuxSeccomp{} + } + + seccompProfile := c.SeccompProfile + switch seccompProfile { + case ProfileNameUnconfined: + return nil + case ProfilePouchDefault, "": + s.Linux.Seccomp = seccomp.DefaultProfile(s) + default: + data, err := ioutil.ReadFile(seccompProfile) + if err != nil { + return fmt.Errorf("failed to load seccomp profile %q: %v", seccompProfile, err) + } + err = json.Unmarshal(data, s.Linux.Seccomp) + if err != nil { + return fmt.Errorf("failed to decode seccomp profile %q: %v", seccompProfile, err) + } + } + + return nil +} diff --git a/daemon/mgr/spec_seccomp_unsupported.go b/daemon/mgr/spec_seccomp_unsupported.go new file mode 100644 index 000000000..1a6dd09fc --- /dev/null +++ b/daemon/mgr/spec_seccomp_unsupported.go @@ -0,0 +1,18 @@ +// +build linux,!seccomp + +package mgr + +import ( + "context" + "fmt" + + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +func setupSeccomp(ctx context.Context, c *Container, s *specs.Spec) error { + if c.SeccompProfile != "" && c.SeccompProfile != "unconfined" { + return fmt.Errorf("Seccomp is not support by pouch, can not set seccomp profile %s", c.SeccompProfile) + } + + return nil +} diff --git a/pkg/system/system.go b/pkg/system/system.go index 55ba186f9..13ecc9b34 100644 --- a/pkg/system/system.go +++ b/pkg/system/system.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "fmt" + "io/ioutil" "net" "os" "os/exec" @@ -12,11 +13,49 @@ import ( "time" "github.com/pkg/errors" + "golang.org/x/sys/unix" ) // file to check to determine Operating System const etcOsRelease = "/etc/os-release" +// Info defines system info on current machine +type Info struct { + AppArmor bool + Seccomp bool + + *CgroupInfo +} + +// NewInfo creates a system info about current machine. +func NewInfo() *Info { + info := &Info{CgroupInfo: NewCgroupInfo()} + + // Check if AppArmor is supported. + // isAppArmorEnabled returns true if apparmor is enabled for the host. + // This function is forked from + // https://github.com/opencontainers/runc/blob/1a81e9ab1f138c091fe5c86d0883f87716088527/libcontainer/apparmor/apparmor.go + // to avoid the libapparmor dependency. + if _, err := os.Stat("/sys/kernel/security/apparmor"); err == nil && os.Getenv("container") == "" { + if _, err = os.Stat("/sbin/apparmor_parser"); err == nil { + buf, err := ioutil.ReadFile("/sys/module/apparmor/parameters/enabled") + if err == nil && len(buf) > 1 && buf[0] == 'Y' { + info.AppArmor = true + } + } + } + + // Check if Seccomp is supported, via CONFIG_SECCOMP. + if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL { + // Make sure the kernel has CONFIG_SECCOMP_FILTER. + if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL { + info.Seccomp = true + } + } + + return info +} + // getSysInfo gets sysinfo. func getSysInfo() (*syscall.Sysinfo_t, error) { si := &syscall.Sysinfo_t{} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor.go b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor.go new file mode 100644 index 000000000..7fff0627f --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor.go @@ -0,0 +1,54 @@ +// +build apparmor,linux + +package apparmor + +import ( + "fmt" + "io/ioutil" + "os" +) + +// IsEnabled returns true if apparmor is enabled for the host. +func IsEnabled() bool { + if _, err := os.Stat("/sys/kernel/security/apparmor"); err == nil && os.Getenv("container") == "" { + if _, err = os.Stat("/sbin/apparmor_parser"); err == nil { + buf, err := ioutil.ReadFile("/sys/module/apparmor/parameters/enabled") + return err == nil && len(buf) > 1 && buf[0] == 'Y' + } + } + return false +} + +func setprocattr(attr, value string) error { + // Under AppArmor you can only change your own attr, so use /proc/self/ + // instead of /proc// like libapparmor does + path := fmt.Sprintf("/proc/self/attr/%s", attr) + + f, err := os.OpenFile(path, os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + + _, err = fmt.Fprintf(f, "%s", value) + return err +} + +// changeOnExec reimplements aa_change_onexec from libapparmor in Go +func changeOnExec(name string) error { + value := "exec " + name + if err := setprocattr("exec", value); err != nil { + return fmt.Errorf("apparmor failed to apply profile: %s", err) + } + return nil +} + +// ApplyProfile will apply the profile with the specified name to the process after +// the next exec. +func ApplyProfile(name string) error { + if name == "" { + return nil + } + + return changeOnExec(name) +} diff --git a/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor_disabled.go b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor_disabled.go new file mode 100644 index 000000000..d4110cf0b --- /dev/null +++ b/vendor/github.com/opencontainers/runc/libcontainer/apparmor/apparmor_disabled.go @@ -0,0 +1,20 @@ +// +build !apparmor !linux + +package apparmor + +import ( + "errors" +) + +var ErrApparmorNotEnabled = errors.New("apparmor: config provided but apparmor not supported") + +func IsEnabled() bool { + return false +} + +func ApplyProfile(name string) error { + if name != "" { + return ErrApparmorNotEnabled + } + return nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 2591decdd..09ee97958 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1521,6 +1521,12 @@ "revision": "2b8ed96d2a422bf205adcdfa05272dc12c81613b", "revisionTime": "2017-11-08T19:22:26Z" }, + { + "checksumSHA1": "gVVY8k2G3ws+V1czsfxfuRs8log=", + "path": "github.com/opencontainers/runc/libcontainer/apparmor", + "revision": "e6516b3d5dc780cb57a976013c242a9a93052543", + "revisionTime": "2017-12-15T16:47:07Z" + }, { "checksumSHA1": "s0A2QT/y6WHMQx6djbl4TopB/34=", "path": "github.com/opencontainers/runc/libcontainer/configs",