浏览代码

Merge pull request #15099 from ewindisch/apparmor-restore-en

Restore AppArmor generation + fixes
Jessie Frazelle 10 年之前
父节点
当前提交
d7661cb48b

+ 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,
-}

+ 108 - 28
contrib/apparmor/docker-engine

@@ -1,6 +1,6 @@
 @{DOCKER_GRAPH_PATH}=/var/lib/docker
 
-profile /usr/bin/docker (attach_disconnected) {
+profile /usr/bin/docker (attach_disconnected, complain) {
   # Prevent following links to these files during container setup.
   deny /etc/** mkl,
   deny /dev/** kl,
@@ -21,51 +21,131 @@ profile /usr/bin/docker (attach_disconnected) {
   ipc rw,
   network,
   capability,
-  file,
+  owner /** rw,
+  /var/lib/docker/** rwl,
+
+  # For non-root client use:
+  /dev/urandom r,
+  /run/docker.sock rw,
+  /proc/** r,
+  /sys/kernel/mm/hugepages/ r,
+  /etc/localtime r,
 
   ptrace peer=@{profile_name},
+  ptrace (read) peer=docker-default,
+  deny ptrace (trace) peer=docker-default,
+  deny ptrace peer=/usr/bin/docker///bin/ps,
 
   /usr/bin/docker pix,
-  /sbin/xtables-multi rCix,
+  /sbin/xtables-multi rCx,
   /sbin/iptables rCx,
   /sbin/modprobe rCx,
   /sbin/auplink rCx,
+  /bin/kmod rCx,
   /usr/bin/xz rCx,
+  /bin/ps rCx,
+  /bin/cat rCx,
+  /sbin/zfs rCx,
 
   # Transitions
   change_profile -> docker-*,
   change_profile -> unconfined,
 
-  profile /sbin/iptables {
-   signal (receive) peer=/usr/bin/docker,
-   capability net_admin,
+  profile /bin/cat (complain) {
+    /etc/ld.so.cache r,
+    /lib/** r,
+    /dev/null rw,
+    /proc r,
+    /bin/cat mr,
+
+    # For reading in 'docker stats':
+    /proc/[0-9]*/net/dev r,
   }
-  profile /sbin/auplink flags=(attach_disconnected) {
-   signal (receive) peer=/usr/bin/docker,
-   capability sys_admin,
-   capability dac_override,
+  profile /bin/ps (complain) {
+    /etc/ld.so.cache r,
+    /etc/localtime r,
+    /etc/passwd r,
+    /etc/nsswitch.conf r,
+    /lib/** r,
+    /proc/[0-9]*/** r,
+    /dev/null rw,
+    /bin/ps mr,
+
+    # We don't need ptrace so we'll deny and ignore the error.
+    deny ptrace (read, trace),
 
-   @{DOCKER_GRAPH_PATH}/aufs/** rw,
-   # For user namespaces:
-   @{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/** rw,
+    # Quiet dac_override denials
+    deny capability dac_override,
+    deny capability dac_read_search,
+    deny capability sys_ptrace,
 
-   # The following may be removed via delegates
-   /sys/fs/aufs/** r,
-   /lib/** r,
-   /apparmor/.null r,
-   /dev/null rw,
-   /etc/ld.so.cache r,
-   /sbin/auplink rm,
-   /proc/fs/aufs/** rw,
-   /proc/[0-9]*/mounts rw,
+    /dev/tty r,
+    /proc/stat r,
+    /proc/cpuinfo r,
+    /proc/meminfo r,
+    /proc/uptime r,
+    /sys/devices/system/cpu/online r,
+    /proc/sys/kernel/pid_max r,
+    /proc/ r,
+    /proc/tty/drivers r,
   }
-  profile /sbin/modprobe {
-   signal (receive) peer=/usr/bin/docker,
-   capability sys_module,
-   file,
+  profile /sbin/iptables (complain) {
+    signal (receive) peer=/usr/bin/docker,
+    capability net_admin,
+  }
+  profile /sbin/auplink flags=(attach_disconnected, complain) {
+    signal (receive) peer=/usr/bin/docker,
+    capability sys_admin,
+    capability dac_override,
+
+    @{DOCKER_GRAPH_PATH}/aufs/** rw,
+    @{DOCKER_GRAPH_PATH}/tmp/** rw,
+    # For user namespaces:
+    @{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/** rw,
+
+    /sys/fs/aufs/** r,
+    /lib/** r,
+    /apparmor/.null r,
+    /dev/null rw,
+    /etc/ld.so.cache r,
+    /sbin/auplink rm,
+    /proc/fs/aufs/** rw,
+    /proc/[0-9]*/mounts rw,
+  }
+  profile /sbin/modprobe /bin/kmod (complain) {
+    signal (receive) peer=/usr/bin/docker,
+    capability sys_module,
+    /etc/ld.so.cache r,
+    /lib/** r,
+    /dev/null rw,
+    /apparmor/.null rw,
+    /sbin/modprobe rm,
+    /bin/kmod rm,
+    /proc/cmdline r,
+    /sys/module/** r,
+    /etc/modprobe.d{/,/**} r,
   }
   # xz works via pipes, so we do not need access to the filesystem.
-  profile /usr/bin/xz {
-   signal (receive) peer=/usr/bin/docker,
+  profile /usr/bin/xz (complain) {
+    signal (receive) peer=/usr/bin/docker,
+    /etc/ld.so.cache r,
+    /lib/** r,
+    /usr/bin/xz rm,
+    deny /proc/** rw,
+    deny /sys/** rw,
+  }
+  profile /sbin/xtables-multi (attach_disconnected, complain) {
+    /etc/ld.so.cache r,
+    /lib/** r,
+    /sbin/xtables-multi rm,
+    /apparmor/.null w,
+    /dev/null rw,
+    capability net_raw,
+    capability net_admin,
+    network raw,
+  }
+  profile /sbin/zfs (attach_disconnected, complain) {
+    file,
+    capability,
   }
 }

+ 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"
@@ -55,6 +56,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)