github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/profiles/apparmor/apparmor.go (about) 1 //go:build linux 2 3 package apparmor // import "github.com/docker/docker/profiles/apparmor" 4 5 import ( 6 "bufio" 7 "fmt" 8 "io" 9 "os" 10 "os/exec" 11 "path" 12 "strings" 13 "text/template" 14 ) 15 16 // profileDirectory is the file store for apparmor profiles and macros. 17 const profileDirectory = "/etc/apparmor.d" 18 19 // profileData holds information about the given profile for generation. 20 type profileData struct { 21 // Name is profile name. 22 Name string 23 // DaemonProfile is the profile name of our daemon. 24 DaemonProfile string 25 // Imports defines the apparmor functions to import, before defining the profile. 26 Imports []string 27 // InnerImports defines the apparmor functions to import in the profile. 28 InnerImports []string 29 } 30 31 // generateDefault creates an apparmor profile from ProfileData. 32 func (p *profileData) generateDefault(out io.Writer) error { 33 compiled, err := template.New("apparmor_profile").Parse(baseTemplate) 34 if err != nil { 35 return err 36 } 37 38 if macroExists("tunables/global") { 39 p.Imports = append(p.Imports, "#include <tunables/global>") 40 } else { 41 p.Imports = append(p.Imports, "@{PROC}=/proc/") 42 } 43 44 if macroExists("abstractions/base") { 45 p.InnerImports = append(p.InnerImports, "#include <abstractions/base>") 46 } 47 48 return compiled.Execute(out, p) 49 } 50 51 // macrosExists checks if the passed macro exists. 52 func macroExists(m string) bool { 53 _, err := os.Stat(path.Join(profileDirectory, m)) 54 return err == nil 55 } 56 57 // InstallDefault generates a default profile in a temp directory determined by 58 // os.TempDir(), then loads the profile into the kernel using 'apparmor_parser'. 59 func InstallDefault(name string) error { 60 p := profileData{ 61 Name: name, 62 } 63 64 // Figure out the daemon profile. 65 currentProfile, err := os.ReadFile("/proc/self/attr/current") 66 if err != nil { 67 // If we couldn't get the daemon profile, assume we are running 68 // unconfined which is generally the default. 69 currentProfile = nil 70 } 71 daemonProfile := string(currentProfile) 72 // Normally profiles are suffixed by " (enforcing)" or similar. AppArmor 73 // profiles cannot contain spaces so this doesn't restrict daemon profile 74 // names. 75 if parts := strings.SplitN(daemonProfile, " ", 2); len(parts) >= 1 { 76 daemonProfile = parts[0] 77 } 78 if daemonProfile == "" { 79 daemonProfile = "unconfined" 80 } 81 p.DaemonProfile = daemonProfile 82 83 // Install to a temporary directory. 84 f, err := os.CreateTemp("", name) 85 if err != nil { 86 return err 87 } 88 profilePath := f.Name() 89 90 defer f.Close() 91 defer os.Remove(profilePath) 92 93 if err := p.generateDefault(f); err != nil { 94 return err 95 } 96 97 return loadProfile(profilePath) 98 } 99 100 // IsLoaded checks if a profile with the given name has been loaded into the 101 // kernel. 102 func IsLoaded(name string) (bool, error) { 103 file, err := os.Open("/sys/kernel/security/apparmor/profiles") 104 if err != nil { 105 return false, err 106 } 107 defer file.Close() 108 109 r := bufio.NewReader(file) 110 for { 111 p, err := r.ReadString('\n') 112 if err == io.EOF { 113 break 114 } 115 if err != nil { 116 return false, err 117 } 118 if strings.HasPrefix(p, name+" ") { 119 return true, nil 120 } 121 } 122 123 return false, nil 124 } 125 126 // loadProfile runs `apparmor_parser -Kr` on a specified apparmor profile to 127 // replace the profile. The `-K` is necessary to make sure that apparmor_parser 128 // doesn't try to write to a read-only filesystem. 129 func loadProfile(profilePath string) error { 130 c := exec.Command("apparmor_parser", "-Kr", profilePath) 131 c.Dir = "" 132 133 output, err := c.CombinedOutput() 134 if err != nil { 135 return fmt.Errorf("running `%s %s` failed with output: %s\nerror: %v", c.Path, strings.Join(c.Args, " "), output, err) 136 } 137 138 return nil 139 }