Skip to content

Commit 3edc88f

Browse files
committed
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 <[email protected]>
1 parent be60047 commit 3edc88f

5 files changed

Lines changed: 233 additions & 27 deletions

File tree

contrib/apparmor/docker

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// +build linux
2+
3+
package native
4+
5+
import (
6+
"bufio"
7+
"fmt"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"path"
12+
"strings"
13+
"text/template"
14+
15+
"github.com/opencontainers/runc/libcontainer/apparmor"
16+
)
17+
18+
const (
19+
apparmorProfilePath = "/etc/apparmor.d/docker"
20+
)
21+
22+
type data struct {
23+
Name string
24+
Imports []string
25+
InnerImports []string
26+
}
27+
28+
const baseTemplate = `
29+
{{range $value := .Imports}}
30+
{{$value}}
31+
{{end}}
32+
33+
profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
34+
{{range $value := .InnerImports}}
35+
{{$value}}
36+
{{end}}
37+
38+
network,
39+
capability,
40+
file,
41+
umount,
42+
43+
deny @{PROC}/sys/fs/** wklx,
44+
deny @{PROC}/fs/** wklx,
45+
deny @{PROC}/sysrq-trigger rwklx,
46+
deny @{PROC}/mem rwklx,
47+
deny @{PROC}/kmem rwklx,
48+
deny @{PROC}/kore rwklx,
49+
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
50+
deny @{PROC}/sys/kernel/*/** wklx,
51+
52+
deny mount,
53+
deny ptrace,
54+
55+
deny /sys/[^f]*/** wklx,
56+
deny /sys/f[^s]*/** wklx,
57+
deny /sys/fs/[^c]*/** wklx,
58+
deny /sys/fs/c[^g]*/** wklx,
59+
deny /sys/fs/cg[^r]*/** wklx,
60+
deny /sys/firmware/efi/efivars/** rwklx,
61+
deny /sys/kernel/security/** rwklx,
62+
}
63+
`
64+
65+
func generateProfile(out io.Writer) error {
66+
compiled, err := template.New("apparmor_profile").Parse(baseTemplate)
67+
if err != nil {
68+
return err
69+
}
70+
data := &data{
71+
Name: "docker-default",
72+
}
73+
if tunablesExists() {
74+
data.Imports = append(data.Imports, "#include <tunables/global>")
75+
} else {
76+
data.Imports = append(data.Imports, "@{PROC}=/proc/")
77+
}
78+
if abstractionsExists() {
79+
data.InnerImports = append(data.InnerImports, "#include <abstractions/base>")
80+
}
81+
if err := compiled.Execute(out, data); err != nil {
82+
return err
83+
}
84+
return nil
85+
}
86+
87+
// check if the tunables/global exist
88+
func tunablesExists() bool {
89+
_, err := os.Stat("/etc/apparmor.d/tunables/global")
90+
return err == nil
91+
}
92+
93+
// check if abstractions/base exist
94+
func abstractionsExists() bool {
95+
_, err := os.Stat("/etc/apparmor.d/abstractions/base")
96+
return err == nil
97+
}
98+
99+
func installAppArmorProfile() error {
100+
if !apparmor.IsEnabled() {
101+
return nil
102+
}
103+
104+
// Make sure /etc/apparmor.d exists
105+
if err := os.MkdirAll(path.Dir(apparmorProfilePath), 0755); err != nil {
106+
return err
107+
}
108+
109+
f, err := os.OpenFile(apparmorProfilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
110+
if err != nil {
111+
return err
112+
}
113+
if err := generateProfile(f); err != nil {
114+
f.Close()
115+
return err
116+
}
117+
f.Close()
118+
119+
cmd := exec.Command("/sbin/apparmor_parser", "-r", "-W", "docker")
120+
// to use the parser directly we have to make sure we are in the correct
121+
// dir with the profile
122+
cmd.Dir = "/etc/apparmor.d"
123+
124+
output, err := cmd.CombinedOutput()
125+
if err != nil {
126+
return fmt.Errorf("Error loading docker apparmor profile: %s (%s)", err, output)
127+
}
128+
return nil
129+
}
130+
131+
func hasAppArmorProfileLoaded(profile string) error {
132+
file, err := os.Open("/sys/kernel/security/apparmor/profiles")
133+
if err != nil {
134+
return err
135+
}
136+
r := bufio.NewReader(file)
137+
for {
138+
p, err := r.ReadString('\n')
139+
if err != nil {
140+
return err
141+
}
142+
if strings.HasPrefix(p, profile+" ") {
143+
return nil
144+
}
145+
}
146+
}

daemon/execdriver/native/driver.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
sysinfo "github.com/docker/docker/pkg/system"
2222
"github.com/docker/docker/pkg/term"
2323
"github.com/opencontainers/runc/libcontainer"
24+
"github.com/opencontainers/runc/libcontainer/apparmor"
2425
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
2526
"github.com/opencontainers/runc/libcontainer/configs"
2627
"github.com/opencontainers/runc/libcontainer/system"
@@ -51,6 +52,20 @@ func NewDriver(root, initPath string, options []string) (*driver, error) {
5152
return nil, err
5253
}
5354

55+
if apparmor.IsEnabled() {
56+
if err := installAppArmorProfile(); err != nil {
57+
apparmor_profiles := []string{"docker-default", "docker-unconfined"}
58+
59+
// Allow daemon to run if loading failed, but are active
60+
// (possibly through another run, manually, or via system startup)
61+
for _, policy := range apparmor_profiles {
62+
if err := hasAppArmorProfileLoaded(policy); err != nil {
63+
return nil, fmt.Errorf("AppArmor enabled on system but the %s profile could not be loaded.", policy)
64+
}
65+
}
66+
}
67+
}
68+
5469
// choose cgroup manager
5570
// this makes sure there are no breaking changes to people
5671
// who upgrade from versions without native.cgroupdriver opt

docs/security/apparmor.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
AppArmor security profiles for Docker
2+
--------------------------------------
3+
4+
AppArmor (Application Armor) is a security module that allows a system
5+
administrator to associate a security profile with each program. Docker
6+
expects to find an AppArmor policy loaded and enforced.
7+
8+
Container profiles are loaded automatically by Docker. A profile
9+
for the Docker Engine itself also exists and is installed
10+
with the official *.deb* packages. Advanced users and package
11+
managers may find the profile for */usr/bin/docker* underneath
12+
[contrib/apparmor](https://github.com/docker/docker/tree/master/contrib/apparmor)
13+
in the Docker Engine source repository.
14+
15+
16+
Understand the policies
17+
------------------------
18+
19+
The `docker-default` profile the default for running
20+
containers. It is moderately protective while
21+
providing wide application compatability.
22+
23+
The `docker-unconfined` profile is intended for
24+
privileged applications and is the default when runing
25+
a container with the *--privileged* flag.
26+
27+
The system's standard `unconfined` profile inherits all
28+
system-wide policies, applying path-based policies
29+
intended for the host system inside of containers.
30+
This was the default for privileged containers
31+
prior to Docker 1.8.
32+
33+
34+
Overriding the profile for a container
35+
---------------------------------------
36+
37+
Users may override the AppArmor profile using the
38+
`security-opt` option (per-container).
39+
40+
For example, the following explicitly specifies the default policy:
41+
42+
```
43+
$ docker run --rm -it --security-opt apparmor:docker-default hello-world
44+
```
45+

integration-cli/docker_cli_run_test.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,7 +2397,10 @@ func (s *DockerSuite) TestRunWriteToProcAsound(c *check.C) {
23972397
func (s *DockerSuite) TestRunReadProcTimer(c *check.C) {
23982398
testRequires(c, NativeExecDriver)
23992399
out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/timer_stats")
2400-
if err != nil || code != 0 {
2400+
if code != 0 {
2401+
return
2402+
}
2403+
if err != nil {
24012404
c.Fatal(err)
24022405
}
24032406
if strings.Trim(out, "\n ") != "" {
@@ -2414,14 +2417,35 @@ func (s *DockerSuite) TestRunReadProcLatency(c *check.C) {
24142417
return
24152418
}
24162419
out, code, err := dockerCmdWithError("run", "busybox", "cat", "/proc/latency_stats")
2417-
if err != nil || code != 0 {
2420+
if code != 0 {
2421+
return
2422+
}
2423+
if err != nil {
24182424
c.Fatal(err)
24192425
}
24202426
if strings.Trim(out, "\n ") != "" {
24212427
c.Fatalf("expected to receive no output from /proc/latency_stats but received %q", out)
24222428
}
24232429
}
24242430

2431+
func (s *DockerSuite) TestRunReadFilteredProc(c *check.C) {
2432+
testRequires(c, Apparmor)
2433+
2434+
testReadPaths := []string{
2435+
"/proc/latency_stats",
2436+
"/proc/timer_stats",
2437+
"/proc/kcore",
2438+
}
2439+
for i, filePath := range testReadPaths {
2440+
name := fmt.Sprintf("procsieve-%d", i)
2441+
shellCmd := fmt.Sprintf("exec 3<%s", filePath)
2442+
2443+
if out, exitCode, err := dockerCmdWithError("run", "--privileged", "--security-opt", "apparmor:docker-default", "--name", name, "busybox", "sh", "-c", shellCmd); err == nil || exitCode == 0 {
2444+
c.Fatalf("Open FD for read should have failed with permission denied, got: %s, %v", out, err)
2445+
}
2446+
}
2447+
}
2448+
24252449
func (s *DockerSuite) TestMountIntoProc(c *check.C) {
24262450
testRequires(c, NativeExecDriver)
24272451
_, code, err := dockerCmdWithError("run", "-v", "/proc//sys", "busybox", "true")
@@ -2515,6 +2539,7 @@ func (s *DockerSuite) TestRunWriteFilteredProc(c *check.C) {
25152539
"/proc/sys/kernel/modprobe",
25162540
"/proc/sys/kernel/core_pattern",
25172541
"/proc/sysrq-trigger",
2542+
"/proc/kcore",
25182543
}
25192544
for i, filePath := range testWritePaths {
25202545
name := fmt.Sprintf("writeprocsieve-%d", i)

0 commit comments

Comments
 (0)