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>
This commit is contained in:
Eric Windisch 2015-07-28 14:48:18 -04:00
parent be60047b43
commit 3edc88f76d
5 changed files with 233 additions and 27 deletions

View File

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

View File

@ -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
}
}
}

View File

@ -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
docs/security/apparmor.md Normal file
View File

@ -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
```

View File

@ -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)