From 3edc88f76df6a3bc9d887de8157ec71730c9057a Mon Sep 17 00:00:00 2001 From: Eric Windisch Date: Tue, 28 Jul 2015 14:48:18 -0400 Subject: [PATCH 1/3] 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 --- contrib/apparmor/docker | 25 ----- daemon/execdriver/native/apparmor.go | 146 +++++++++++++++++++++++++ daemon/execdriver/native/driver.go | 15 +++ docs/security/apparmor.md | 45 ++++++++ integration-cli/docker_cli_run_test.go | 29 ++++- 5 files changed, 233 insertions(+), 27 deletions(-) delete mode 100644 contrib/apparmor/docker create mode 100644 daemon/execdriver/native/apparmor.go create mode 100644 docs/security/apparmor.md diff --git a/contrib/apparmor/docker b/contrib/apparmor/docker deleted file mode 100644 index 4674ecf6e9..0000000000 --- a/contrib/apparmor/docker +++ /dev/null @@ -1,25 +0,0 @@ -#include - -profile docker-default flags=(attach_disconnected,mediate_deleted) { - #include - - 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, -} diff --git a/daemon/execdriver/native/apparmor.go b/daemon/execdriver/native/apparmor.go new file mode 100644 index 0000000000..ecbaf33510 --- /dev/null +++ b/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 ") + } else { + data.Imports = append(data.Imports, "@{PROC}=/proc/") + } + if abstractionsExists() { + data.InnerImports = append(data.InnerImports, "#include ") + } + 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 + } + } +} diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index a94de3d18f..351f26794f 100644 --- a/daemon/execdriver/native/driver.go +++ b/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 diff --git a/docs/security/apparmor.md b/docs/security/apparmor.md new file mode 100644 index 0000000000..7cd88d314b --- /dev/null +++ b/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 +``` + diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 75b346edb7..0b7d8b2965 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/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) From 8b2fcddcd251e58473abf6c4949573e03f44bb96 Mon Sep 17 00:00:00 2001 From: Eric Windisch Date: Thu, 23 Jul 2015 12:01:24 -0400 Subject: [PATCH 2/3] AA: Eliminate 'file' permission Implements the policies for the remaining binaries called by the Docker engine and eliminates the giant whitelisted 'all files' permission in favor of granular whitelisting and child-specific policies. It should be possible now to remove the 'file' permission, but for the sake of keeping Docker unbroken, we'll try to gradually tighten the policy. Signed-off-by: Eric Windisch --- contrib/apparmor/docker-engine | 128 ++++++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 24 deletions(-) diff --git a/contrib/apparmor/docker-engine b/contrib/apparmor/docker-engine index 07b5dd864a..a174ee440b 100644 --- a/contrib/apparmor/docker-engine +++ b/contrib/apparmor/docker-engine @@ -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 /bin/cat { + /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 /bin/ps { + /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), + + # Quiet dac_override denials + deny capability dac_override, + deny capability dac_read_search, + deny capability sys_ptrace, + + /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/iptables { - signal (receive) peer=/usr/bin/docker, - capability net_admin, + signal (receive) peer=/usr/bin/docker, + capability net_admin, } profile /sbin/auplink flags=(attach_disconnected) { - signal (receive) peer=/usr/bin/docker, - capability sys_admin, - capability dac_override, + signal (receive) peer=/usr/bin/docker, + capability sys_admin, + capability dac_override, - @{DOCKER_GRAPH_PATH}/aufs/** rw, - # For user namespaces: - @{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/** rw, + @{DOCKER_GRAPH_PATH}/aufs/** rw, + @{DOCKER_GRAPH_PATH}/tmp/** rw, + # For user namespaces: + @{DOCKER_GRAPH_PATH}/[0-9]*.[0-9]*/** rw, - # 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, + /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 { - signal (receive) peer=/usr/bin/docker, - capability sys_module, - file, + profile /sbin/modprobe /bin/kmod { + 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, + 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) { + /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) { + file, + capability, } } From 6c887be76951e802900a07e16aeaf0a079ac4534 Mon Sep 17 00:00:00 2001 From: Eric Windisch Date: Thu, 23 Jul 2015 23:23:05 -0400 Subject: [PATCH 3/3] Mark engine AA policy as complain-only The engine policy will now only complain as a temporary measure to ensure we do not cause breakages while users exercise this policy. This is NOT the policy for containers, but for the newly-introduced policy for the daemon itself. Signed-off-by: Eric Windisch --- contrib/apparmor/docker-engine | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contrib/apparmor/docker-engine b/contrib/apparmor/docker-engine index a174ee440b..bdfc207568 100644 --- a/contrib/apparmor/docker-engine +++ b/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, @@ -51,7 +51,7 @@ profile /usr/bin/docker (attach_disconnected) { change_profile -> docker-*, change_profile -> unconfined, - profile /bin/cat { + profile /bin/cat (complain) { /etc/ld.so.cache r, /lib/** r, /dev/null rw, @@ -61,7 +61,7 @@ profile /usr/bin/docker (attach_disconnected) { # For reading in 'docker stats': /proc/[0-9]*/net/dev r, } - profile /bin/ps { + profile /bin/ps (complain) { /etc/ld.so.cache r, /etc/localtime r, /etc/passwd r, @@ -89,11 +89,11 @@ profile /usr/bin/docker (attach_disconnected) { /proc/ r, /proc/tty/drivers r, } - profile /sbin/iptables { + profile /sbin/iptables (complain) { signal (receive) peer=/usr/bin/docker, capability net_admin, } - profile /sbin/auplink flags=(attach_disconnected) { + profile /sbin/auplink flags=(attach_disconnected, complain) { signal (receive) peer=/usr/bin/docker, capability sys_admin, capability dac_override, @@ -112,7 +112,7 @@ profile /usr/bin/docker (attach_disconnected) { /proc/fs/aufs/** rw, /proc/[0-9]*/mounts rw, } - profile /sbin/modprobe /bin/kmod { + profile /sbin/modprobe /bin/kmod (complain) { signal (receive) peer=/usr/bin/docker, capability sys_module, /etc/ld.so.cache r, @@ -126,7 +126,7 @@ profile /usr/bin/docker (attach_disconnected) { /etc/modprobe.d{/,/**} r, } # xz works via pipes, so we do not need access to the filesystem. - profile /usr/bin/xz { + profile /usr/bin/xz (complain) { signal (receive) peer=/usr/bin/docker, /etc/ld.so.cache r, /lib/** r, @@ -134,7 +134,7 @@ profile /usr/bin/docker (attach_disconnected) { deny /proc/** rw, deny /sys/** rw, } - profile /sbin/xtables-multi (attach_disconnected) { + profile /sbin/xtables-multi (attach_disconnected, complain) { /etc/ld.so.cache r, /lib/** r, /sbin/xtables-multi rm, @@ -144,7 +144,7 @@ profile /usr/bin/docker (attach_disconnected) { capability net_admin, network raw, } - profile /sbin/zfs (attach_disconnected) { + profile /sbin/zfs (attach_disconnected, complain) { file, capability, }