Add default apparmor profile generation
This adds default apparmor profile generation to the containerd client so that profiles can be generated with a SpecOpt Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
This commit is contained in:
		
							
								
								
									
										18
									
								
								apparmor.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								apparmor.go
									
									
									
									
									
								
							| @@ -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 |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										57
									
								
								contrib/apparmor/apparmor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								contrib/apparmor/apparmor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										193
									
								
								contrib/apparmor/template.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								contrib/apparmor/template.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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/<number>/** 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 <tunables/global>") | ||||||
|  | 	} else { | ||||||
|  | 		p.Imports = append(p.Imports, "@{PROC}=/proc/") | ||||||
|  | 	} | ||||||
|  | 	if macroExists("abstractions/base") { | ||||||
|  | 		p.InnerImports = append(p.InnerImports, "#include <abstractions/base>") | ||||||
|  | 	} | ||||||
|  | 	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 | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Michael Crosby
					Michael Crosby