Bläddra i källkod

Restore AppArmor profile generation

Will attempt to load profiles automatically. If loading fails
but the profiles are already loaded, execution will continue.

A hard failure will only occur if Docker cannot load
the profiles *and* they have not already been loaded via
some other means.

Also introduces documentation for AppArmor.

Signed-off-by: Eric Windisch <eric@windisch.us>
Eric Windisch 10 år sedan
förälder
incheckning
3edc88f76d

+ 0 - 25
contrib/apparmor/docker

@@ -1,25 +0,0 @@
-#include <tunables/global>
-
-profile docker-default flags=(attach_disconnected,mediate_deleted) {
-  #include <abstractions/base>
-
-  network,
-  capability,
-  file,
-  umount,
-
-  deny @{PROC}/sys/fs/** wklx,
-  deny @{PROC}/sysrq-trigger rwklx,
-  deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
-  deny @{PROC}/sys/kernel/*/** wklx,
-
-  deny mount,
-
-  deny /sys/[^f]*/** wklx,
-  deny /sys/f[^s]*/** wklx,
-  deny /sys/fs/[^c]*/** wklx,
-  deny /sys/fs/c[^g]*/** wklx,
-  deny /sys/fs/cg[^r]*/** wklx,
-  deny /sys/firmware/efi/efivars/** rwklx,
-  deny /sys/kernel/security/** rwklx,
-}

+ 146 - 0
daemon/execdriver/native/apparmor.go

@@ -0,0 +1,146 @@
+// +build linux
+
+package native
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"path"
+	"strings"
+	"text/template"
+
+	"github.com/opencontainers/runc/libcontainer/apparmor"
+)
+
+const (
+	apparmorProfilePath = "/etc/apparmor.d/docker"
+)
+
+type data struct {
+	Name         string
+	Imports      []string
+	InnerImports []string
+}
+
+const baseTemplate = `
+{{range $value := .Imports}}
+{{$value}}
+{{end}}
+
+profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
+{{range $value := .InnerImports}}
+  {{$value}}
+{{end}}
+
+  network,
+  capability,
+  file,
+  umount,
+
+  deny @{PROC}/sys/fs/** wklx,
+  deny @{PROC}/fs/** wklx,
+  deny @{PROC}/sysrq-trigger rwklx,
+  deny @{PROC}/mem rwklx,
+  deny @{PROC}/kmem rwklx,
+  deny @{PROC}/kore rwklx,
+  deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
+  deny @{PROC}/sys/kernel/*/** wklx,
+
+  deny mount,
+  deny ptrace,
+
+  deny /sys/[^f]*/** wklx,
+  deny /sys/f[^s]*/** wklx,
+  deny /sys/fs/[^c]*/** wklx,
+  deny /sys/fs/c[^g]*/** wklx,
+  deny /sys/fs/cg[^r]*/** wklx,
+  deny /sys/firmware/efi/efivars/** rwklx,
+  deny /sys/kernel/security/** rwklx,
+}
+`
+
+func generateProfile(out io.Writer) error {
+	compiled, err := template.New("apparmor_profile").Parse(baseTemplate)
+	if err != nil {
+		return err
+	}
+	data := &data{
+		Name: "docker-default",
+	}
+	if tunablesExists() {
+		data.Imports = append(data.Imports, "#include <tunables/global>")
+	} else {
+		data.Imports = append(data.Imports, "@{PROC}=/proc/")
+	}
+	if abstractionsExists() {
+		data.InnerImports = append(data.InnerImports, "#include <abstractions/base>")
+	}
+	if err := compiled.Execute(out, data); err != nil {
+		return err
+	}
+	return nil
+}
+
+// check if the tunables/global exist
+func tunablesExists() bool {
+	_, err := os.Stat("/etc/apparmor.d/tunables/global")
+	return err == nil
+}
+
+// check if abstractions/base exist
+func abstractionsExists() bool {
+	_, err := os.Stat("/etc/apparmor.d/abstractions/base")
+	return err == nil
+}
+
+func installAppArmorProfile() error {
+	if !apparmor.IsEnabled() {
+		return nil
+	}
+
+	// Make sure /etc/apparmor.d exists
+	if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil {
+		return err
+	}
+
+	f, err := os.OpenFile(apparmorProfilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+	if err != nil {
+		return err
+	}
+	if err := generateProfile(f); err != nil {
+		f.Close()
+		return err
+	}
+	f.Close()
+
+	cmd := exec.Command("/sbin/apparmor_parser", "-r", "-W", "docker")
+	// to use the parser directly we have to make sure we are in the correct
+	// dir with the profile
+	cmd.Dir = "/etc/apparmor.d"
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("Error loading docker apparmor profile: %s (%s)", err, output)
+	}
+	return nil
+}
+
+func hasAppArmorProfileLoaded(profile string) error {
+	file, err := os.Open("/sys/kernel/security/apparmor/profiles")
+	if err != nil {
+		return err
+	}
+	r := bufio.NewReader(file)
+	for {
+		p, err := r.ReadString('\n')
+		if err != nil {
+			return err
+		}
+		if strings.HasPrefix(p, profile+" ") {
+			return nil
+		}
+	}
+}

+ 15 - 0
daemon/execdriver/native/driver.go

@@ -21,6 +21,7 @@ import (
 	sysinfo "github.com/docker/docker/pkg/system"
 	"github.com/docker/docker/pkg/term"
 	"github.com/opencontainers/runc/libcontainer"
+	"github.com/opencontainers/runc/libcontainer/apparmor"
 	"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
 	"github.com/opencontainers/runc/libcontainer/configs"
 	"github.com/opencontainers/runc/libcontainer/system"
@@ -51,6 +52,20 @@ func NewDriver(root, initPath string, options []string) (*driver, error) {
 		return nil, err
 	}
 
+	if apparmor.IsEnabled() {
+		if err := installAppArmorProfile(); err != nil {
+			apparmor_profiles := []string{"docker-default", "docker-unconfined"}
+
+			// Allow daemon to run if loading failed, but are active
+			// (possibly through another run, manually, or via system startup)
+			for _, policy := range apparmor_profiles {
+				if err := hasAppArmorProfileLoaded(policy); err != nil {
+					return nil, fmt.Errorf("AppArmor enabled on system but the %s profile could not be loaded.", policy)
+				}
+			}
+		}
+	}
+
 	// choose cgroup manager
 	// this makes sure there are no breaking changes to people
 	// who upgrade from versions without native.cgroupdriver opt

+ 45 - 0
docs/security/apparmor.md

@@ -0,0 +1,45 @@
+AppArmor security profiles for Docker
+--------------------------------------
+
+AppArmor (Application Armor) is a security module that allows a system
+administrator to associate a security profile with each program. Docker
+expects to find an AppArmor policy loaded and enforced.
+
+Container profiles are loaded automatically by Docker. A profile
+for the Docker Engine itself also exists and is installed
+with the official *.deb* packages. Advanced users and package
+managers may find the profile for */usr/bin/docker* underneath
+[contrib/apparmor](https://github.com/docker/docker/tree/master/contrib/apparmor)
+in the Docker Engine source repository.
+
+
+Understand the policies
+------------------------
+
+The `docker-default` profile the default for running
+containers. It is moderately protective while
+providing wide application compatability.
+
+The `docker-unconfined` profile is intended for
+privileged applications and is the default when runing
+a container with the *--privileged* flag.
+
+The system's standard `unconfined` profile inherits all
+system-wide policies, applying path-based policies
+intended for the host system inside of containers.
+This was the default for privileged containers
+prior to Docker 1.8.
+
+
+Overriding the profile for a container
+---------------------------------------
+
+Users may override the AppArmor profile using the
+`security-opt` option (per-container).
+
+For example, the following explicitly specifies the default policy:
+
+```
+$ docker run --rm -it --security-opt apparmor:docker-default hello-world
+```
+

+ 27 - 2
integration-cli/docker_cli_run_test.go

@@ -2397,7 +2397,10 @@ func (s *DockerSuite) TestRunWriteToProcAsound(c *check.C) {
 func (s *DockerSuite) TestRunReadProcTimer(c *check.C) {
 	testRequires(c, NativeExecDriver)
 	out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/timer_stats")
-	if err != nil || code != 0 {
+	if code != 0 {
+		return
+	}
+	if err != nil {
 		c.Fatal(err)
 	}
 	if strings.Trim(out, "\n ") != "" {
@@ -2414,7 +2417,10 @@ func (s *DockerSuite) TestRunReadProcLatency(c *check.C) {
 		return
 	}
 	out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/latency_stats")
-	if err != nil || code != 0 {
+	if code != 0 {
+		return
+	}
+	if err != nil {
 		c.Fatal(err)
 	}
 	if strings.Trim(out, "\n ") != "" {
@@ -2422,6 +2428,24 @@ func (s *DockerSuite) TestRunReadProcLatency(c *check.C) {
 	}
 }
 
+func (s *DockerSuite) TestRunReadFilteredProc(c *check.C) {
+	testRequires(c, Apparmor)
+
+	testReadPaths := []string{
+		"/proc/latency_stats",
+		"/proc/timer_stats",
+		"/proc/kcore",
+	}
+	for i, filePath := range testReadPaths {
+		name := fmt.Sprintf("procsieve-%d", i)
+		shellCmd := fmt.Sprintf("exec 3<%s", filePath)
+
+		if out, exitCode, err := dockerCmdWithError("run", "--privileged", "--security-opt", "apparmor:docker-default", "--name", name, "busybox", "sh", "-c", shellCmd); err == nil || exitCode == 0 {
+			c.Fatalf("Open FD for read should have failed with permission denied, got: %s, %v", out, err)
+		}
+	}
+}
+
 func (s *DockerSuite) TestMountIntoProc(c *check.C) {
 	testRequires(c, NativeExecDriver)
 	_, code, err := dockerCmdWithError("run", "-v", "/proc//sys", "busybox", "true")
@@ -2515,6 +2539,7 @@ func (s *DockerSuite) TestRunWriteFilteredProc(c *check.C) {
 		"/proc/sys/kernel/modprobe",
 		"/proc/sys/kernel/core_pattern",
 		"/proc/sysrq-trigger",
+		"/proc/kcore",
 	}
 	for i, filePath := range testWritePaths {
 		name := fmt.Sprintf("writeprocsieve-%d", i)