diff --git a/apparmor.go b/apparmor.go deleted file mode 100644 index e8485c3f5..000000000 --- a/apparmor.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build linux - -package containerd - -import ( - "context" - - "github.com/containerd/containerd/containers" - specs "github.com/opencontainers/runtime-spec/specs-go" -) - -// WithApparmor sets the provided apparmor profile to the spec -func WithApparmorProfile(profile string) SpecOpts { - return func(_ context.Context, _ *Client, _ *containers.Container, s *specs.Spec) error { - s.Process.ApparmorProfile = profile - return nil - } -} diff --git a/contrib/apparmor/apparmor.go b/contrib/apparmor/apparmor.go new file mode 100644 index 000000000..1a8f002ee --- /dev/null +++ b/contrib/apparmor/apparmor.go @@ -0,0 +1,57 @@ +// +build linux + +package apparmor + +import ( + "context" + "io/ioutil" + "os" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/containers" + specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" +) + +// WithProfile sets the provided apparmor profile to the spec +func WithProfile(profile string) containerd.SpecOpts { + return func(_ context.Context, _ *containerd.Client, _ *containers.Container, s *specs.Spec) error { + s.Process.ApparmorProfile = profile + return nil + } +} + +// WithDefaultProfile will generate a default apparmor profile under the provided name +// for the container. It is only generated if a profile under that name does not exist. +func WithDefaultProfile(name string) containerd.SpecOpts { + return func(_ context.Context, _ *containerd.Client, _ *containers.Container, s *specs.Spec) error { + yes, err := isLoaded(name) + if err != nil { + return err + } + if yes { + s.Process.ApparmorProfile = name + return nil + } + p, err := loadData(name) + if err != nil { + return err + } + f, err := ioutil.TempFile("", p.Name) + if err != nil { + return err + } + defer f.Close() + path := f.Name() + defer os.Remove(path) + + if err := generate(p, f); err != nil { + return err + } + if err := load(path); err != nil { + return errors.Wrapf(err, "load apparmor profile %s", path) + } + s.Process.ApparmorProfile = name + return nil + } +} diff --git a/contrib/apparmor/template.go b/contrib/apparmor/template.go new file mode 100644 index 000000000..29c7af9ea --- /dev/null +++ b/contrib/apparmor/template.go @@ -0,0 +1,193 @@ +// +build linux + +package apparmor + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "path" + "strconv" + "strings" + "text/template" + + "github.com/pkg/errors" +) + +const dir = "/etc/apparmor.d" + +const defaultTemplate = ` +{{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/** rwklx, + deny /sys/kernel/security/** rwklx, + +{{if ge .Version 208095}} + ptrace (trace,read) peer={{.Name}}, +{{end}} +} +` + +type data struct { + Name string + Imports []string + InnerImports []string + Version int +} + +func loadData(name string) (*data, error) { + p := data{ + Name: name, + } + + 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 ") + } + ver, err := getVersion() + if err != nil { + return nil, errors.Wrap(err, "get apparmor_parser version") + } + p.Version = ver + return &p, nil +} + +func generate(p *data, o io.Writer) error { + t, err := template.New("apparmor_profile").Parse(defaultTemplate) + if err != nil { + return err + } + return t.Execute(o, p) +} + +func load(path string) error { + out, err := aaParser("-Kr", path) + if err != nil { + return errors.Errorf("%s: %s", err, out) + } + return nil +} + +// macrosExists checks if the passed macro exists. +func macroExists(m string) bool { + _, err := os.Stat(path.Join(dir, m)) + return err == nil +} + +func aaParser(args ...string) (string, error) { + out, err := exec.Command("apparmor_parser", args...).CombinedOutput() + if err != nil { + return "", err + } + return string(out), nil +} + +func getVersion() (int, error) { + out, err := aaParser("--version") + if err != nil { + return -1, err + } + return parseVersion(out) +} + +// parseVersion takes the output from `apparmor_parser --version` and returns +// a representation of the {major, minor, patch} version as a single number of +// the form MMmmPPP {major, minor, patch}. +func parseVersion(output string) (int, error) { + // output is in the form of the following: + // AppArmor parser version 2.9.1 + // Copyright (C) 1999-2008 Novell Inc. + // Copyright 2009-2012 Canonical Ltd. + + lines := strings.SplitN(output, "\n", 2) + words := strings.Split(lines[0], " ") + version := words[len(words)-1] + + // split by major minor version + v := strings.Split(version, ".") + if len(v) == 0 || len(v) > 3 { + return -1, fmt.Errorf("parsing version failed for output: `%s`", output) + } + + // Default the versions to 0. + var majorVersion, minorVersion, patchLevel int + + majorVersion, err := strconv.Atoi(v[0]) + if err != nil { + return -1, err + } + + if len(v) > 1 { + minorVersion, err = strconv.Atoi(v[1]) + if err != nil { + return -1, err + } + } + if len(v) > 2 { + patchLevel, err = strconv.Atoi(v[2]) + if err != nil { + return -1, err + } + } + + // major*10^5 + minor*10^3 + patch*10^0 + numericVersion := majorVersion*1e5 + minorVersion*1e3 + patchLevel + return numericVersion, nil +} + +func isLoaded(name string) (bool, error) { + f, err := os.Open("/sys/kernel/security/apparmor/profiles") + if err != nil { + return false, err + } + defer f.Close() + r := bufio.NewReader(f) + for { + p, err := r.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + return false, err + } + if strings.HasPrefix(p, name+" ") { + return true, nil + } + } + return false, nil +}