diff --git a/daemon/execdriver/native/apparmor.go b/daemon/execdriver/native/apparmor.go deleted file mode 100644 index 3f3d92ac63..0000000000 --- a/daemon/execdriver/native/apparmor.go +++ /dev/null @@ -1,161 +0,0 @@ -// +build linux - -package native - -import ( - "bufio" - "io" - "os" - "os/exec" - "path" - "strings" - "text/template" - - "github.com/docker/docker/pkg/aaparser" - "github.com/opencontainers/runc/libcontainer/apparmor" -) - -const ( - apparmorProfilePath = "/etc/apparmor.d/docker" -) - -type data struct { - Name string - ExecPath string - Imports []string - InnerImports []string - MajorVersion int - MinorVersion int -} - -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}/* w, # deny write for all files directly in /proc (not in a subdir) - # deny write to files not in /proc//** or /proc/sys/** - deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w, - deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel) - deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/ - deny @{PROC}/sysrq-trigger rwklx, - deny @{PROC}/mem rwklx, - deny @{PROC}/kmem rwklx, - deny @{PROC}/kcore rwklx, - - 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, - -{{if ge .MajorVersion 2}}{{if ge .MinorVersion 8}} - # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container - ptrace (trace,read) peer=docker-default, -{{end}}{{end}} -{{if ge .MajorVersion 2}}{{if ge .MinorVersion 9}} - # docker daemon confinement requires explict allow rule for signal - signal (receive) set=(kill,term) peer={{.ExecPath}}, -{{end}}{{end}} -} -` - -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 ") - } - data.MajorVersion, data.MinorVersion, err = aaparser.GetVersion() - if err != nil { - return err - } - data.ExecPath, err = exec.LookPath("docker") - if err != nil { - return err - } - 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() - - if err := aaparser.LoadProfile(apparmorProfilePath); err != nil { - return err - } - - 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/create.go b/daemon/execdriver/native/create.go index 4f97ed93f8..3cb48b41fa 100644 --- a/daemon/execdriver/native/create.go +++ b/daemon/execdriver/native/create.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/daemon/execdriver" derr "github.com/docker/docker/errors" "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/profiles/seccomp" "github.com/docker/docker/volume" "github.com/opencontainers/runc/libcontainer/apparmor" @@ -71,7 +72,7 @@ func (d *Driver) createContainer(c *execdriver.Command, hooks execdriver.Hooks) } if c.SeccompProfile == "" { - container.Seccomp = getDefaultSeccompProfile() + container.Seccomp = seccomp.GetDefaultProfile() } } // add CAP_ prefix to all caps for new libcontainer update to match @@ -88,7 +89,7 @@ func (d *Driver) createContainer(c *execdriver.Command, hooks execdriver.Hooks) } if c.SeccompProfile != "" && c.SeccompProfile != "unconfined" { - container.Seccomp, err = loadSeccompProfile(c.SeccompProfile) + container.Seccomp, err = seccomp.LoadProfile(c.SeccompProfile) if err != nil { return nil, err } diff --git a/daemon/execdriver/native/driver.go b/daemon/execdriver/native/driver.go index 050745e80c..cfece7b433 100644 --- a/daemon/execdriver/native/driver.go +++ b/daemon/execdriver/native/driver.go @@ -21,6 +21,7 @@ import ( "github.com/docker/docker/pkg/reexec" sysinfo "github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/term" + aaprofile "github.com/docker/docker/profiles/apparmor" "github.com/opencontainers/runc/libcontainer" "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runc/libcontainer/cgroups/systemd" @@ -33,6 +34,8 @@ import ( const ( DriverName = "native" Version = "0.2" + + defaultApparmorProfile = "docker-default" ) // Driver contains all information for native driver, @@ -57,13 +60,13 @@ func NewDriver(root string, options []string) (*Driver, error) { } if apparmor.IsEnabled() { - if err := installAppArmorProfile(); err != nil { - apparmorProfiles := []string{"docker-default"} + if err := aaprofile.InstallDefault(defaultApparmorProfile); err != nil { + apparmorProfiles := []string{defaultApparmorProfile} // Allow daemon to run if loading failed, but are active // (possibly through another run, manually, or via system startup) for _, policy := range apparmorProfiles { - if err := hasAppArmorProfileLoaded(policy); err != nil { + if err := aaprofile.IsLoaded(policy); err != nil { return nil, fmt.Errorf("AppArmor enabled on system but the %s profile could not be loaded.", policy) } } diff --git a/profiles/apparmor/apparmor.go b/profiles/apparmor/apparmor.go new file mode 100644 index 0000000000..46178886e6 --- /dev/null +++ b/profiles/apparmor/apparmor.go @@ -0,0 +1,110 @@ +// +build linux + +package apparmor + +import ( + "bufio" + "io" + "os" + "path" + "strings" + "text/template" + + "github.com/docker/docker/pkg/aaparser" +) + +var ( + // profileDirectory is the file store for apparmor profiles and macros. + profileDirectory = "/etc/apparmor.d" + // defaultProfilePath is the default path for the apparmor profile to be saved. + defaultProfilePath = path.Join(profileDirectory, "docker") +) + +// profileData holds information about the given profile for generation. +type profileData struct { + // Name is profile name. + Name string + // ExecPath is the path to the docker binary. + ExecPath string + // Imports defines the apparmor functions to import, before defining the profile. + Imports []string + // InnerImports defines the apparmor functions to import in the profile. + InnerImports []string + // MajorVersion is the apparmor_parser major version. + MajorVersion int + // MinorVersion is the apparmor_parser minor version. + MinorVersion int +} + +// generateDefault creates an apparmor profile from ProfileData. +func (p *profileData) generateDefault(out io.Writer) error { + compiled, err := template.New("apparmor_profile").Parse(baseTemplate) + if err != nil { + return err + } + if macroExists("tunables/global") { + p.Imports = append(p.Imports, "#include ") + } else { + p.Imports = append(p.Imports, "@{PROC}=/proc/") + } + if macroExists("abstractions/base") { + p.InnerImports = append(p.InnerImports, "#include ") + } + if err := compiled.Execute(out, p); err != nil { + return err + } + return nil +} + +// macrosExists checks if the passed macro exists. +func macroExists(m string) bool { + _, err := os.Stat(path.Join(profileDirectory, m)) + return err == nil +} + +// InstallDefault generates a default profile and installs it in the +// ProfileDirectory with `apparmor_parser`. +func InstallDefault(name string) error { + // Make sure the path where they want to save the profile exists + if err := os.MkdirAll(profileDirectory, 0755); err != nil { + return err + } + + p := profileData{ + Name: name, + } + + f, err := os.OpenFile(defaultProfilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + if err := p.generateDefault(f); err != nil { + f.Close() + return err + } + f.Close() + + if err := aaparser.LoadProfile(defaultProfilePath); err != nil { + return err + } + + return nil +} + +// IsLoaded checks if a passed profile as been loaded into the kernel. +func IsLoaded(name 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, name+" ") { + return nil + } + } +} diff --git a/profiles/apparmor/template.go b/profiles/apparmor/template.go new file mode 100644 index 0000000000..d52748c2bf --- /dev/null +++ b/profiles/apparmor/template.go @@ -0,0 +1,50 @@ +// +build linux + +package apparmor + +// baseTemplate defines the default apparmor profile for containers. +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}/* w, # deny write for all files directly in /proc (not in a subdir) + # deny write to files not in /proc//** or /proc/sys/** + deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w, + deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel) + deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/ + deny @{PROC}/sysrq-trigger rwklx, + deny @{PROC}/mem rwklx, + deny @{PROC}/kmem rwklx, + deny @{PROC}/kcore rwklx, + + 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, + +{{if ge .MajorVersion 2}}{{if ge .MinorVersion 8}} + # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container + ptrace (trace,read) peer=docker-default, +{{end}}{{end}} +{{if ge .MajorVersion 2}}{{if ge .MinorVersion 9}} + # docker daemon confinement requires explict allow rule for signal + signal (receive) set=(kill,term) peer={{.ExecPath}}, +{{end}}{{end}} +} +` diff --git a/profiles/seccomp/fixtures/example.json b/profiles/seccomp/fixtures/example.json new file mode 100755 index 0000000000..674ca50fd9 --- /dev/null +++ b/profiles/seccomp/fixtures/example.json @@ -0,0 +1,27 @@ +{ + "defaultAction": "SCMP_ACT_ERRNO", + "syscalls": [ + { + "name": "clone", + "action": "SCMP_ACT_ALLOW", + "args": [ + { + "index": 0, + "value": 2080505856, + "valueTwo": 0, + "op": "SCMP_CMP_MASKED_EQ" + } + ] + }, + { + "name": "open", + "action": "SCMP_ACT_ALLOW", + "args": [] + }, + { + "name": "close", + "action": "SCMP_ACT_ALLOW", + "args": [] + } + ] +} diff --git a/daemon/execdriver/native/seccomp.go b/profiles/seccomp/seccomp.go similarity index 90% rename from daemon/execdriver/native/seccomp.go rename to profiles/seccomp/seccomp.go index 8263012341..fbc0307bc3 100644 --- a/daemon/execdriver/native/seccomp.go +++ b/profiles/seccomp/seccomp.go @@ -1,6 +1,6 @@ // +build linux -package native +package seccomp import ( "encoding/json" @@ -11,11 +11,13 @@ import ( "github.com/opencontainers/runc/libcontainer/seccomp" ) -func getDefaultSeccompProfile() *configs.Seccomp { +// GetDefaultProfile returns the default seccomp profile. +func GetDefaultProfile() *configs.Seccomp { return defaultSeccompProfile } -func loadSeccompProfile(body string) (*configs.Seccomp, error) { +// LoadProfile takes a file path a decodes the seccomp profile. +func LoadProfile(body string) (*configs.Seccomp, error) { var config types.Seccomp if err := json.Unmarshal([]byte(body), &config); err != nil { return nil, fmt.Errorf("Decoding seccomp profile failed: %v", err) diff --git a/daemon/execdriver/native/seccomp_default.go b/profiles/seccomp/seccomp_default.go similarity index 99% rename from daemon/execdriver/native/seccomp_default.go rename to profiles/seccomp/seccomp_default.go index a3b4028359..1150ee8feb 100644 --- a/daemon/execdriver/native/seccomp_default.go +++ b/profiles/seccomp/seccomp_default.go @@ -1,6 +1,6 @@ // +build linux,seccomp -package native +package seccomp import ( "syscall" diff --git a/profiles/seccomp/seccomp_test.go b/profiles/seccomp/seccomp_test.go new file mode 100644 index 0000000000..11df61e94d --- /dev/null +++ b/profiles/seccomp/seccomp_test.go @@ -0,0 +1,19 @@ +// +build linux + +package seccomp + +import ( + "io/ioutil" + "testing" +) + +func TestLoadProfile(t *testing.T) { + f, err := ioutil.ReadFile("fixtures/example.json") + if err != nil { + t.Fatal(err) + } + + if _, err := LoadProfile(string(f)); err != nil { + t.Fatal(err) + } +} diff --git a/daemon/execdriver/native/seccomp_unsupported.go b/profiles/seccomp/seccomp_unsupported.go similarity index 89% rename from daemon/execdriver/native/seccomp_unsupported.go rename to profiles/seccomp/seccomp_unsupported.go index b0173ecfae..47e386a7d6 100644 --- a/daemon/execdriver/native/seccomp_unsupported.go +++ b/profiles/seccomp/seccomp_unsupported.go @@ -1,6 +1,6 @@ // +build linux,!seccomp -package native +package seccomp import "github.com/opencontainers/runc/libcontainer/configs"