k8s.io/kubernetes@v1.29.3/test/images/apparmor-loader/loader.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bufio" 21 "bytes" 22 "flag" 23 "fmt" 24 "os" 25 "os/exec" 26 "path" 27 "path/filepath" 28 "strings" 29 "time" 30 31 "k8s.io/klog/v2" 32 ) 33 34 var ( 35 // The directories to load profiles from. 36 dirs []string 37 poll = flag.Duration("poll", -1, "Poll the directories for new profiles with this interval. Values < 0 disable polling, and exit after loading the profiles.") 38 ) 39 40 const ( 41 parser = "apparmor_parser" 42 apparmorfs = "/sys/kernel/security/apparmor" 43 ) 44 45 func main() { 46 klog.InitFlags(nil) 47 flag.Usage = func() { 48 fmt.Fprintf(os.Stderr, "Usage: %s [FLAG]... [PROFILE_DIR]...\n", os.Args[0]) 49 fmt.Fprintf(os.Stderr, "Load the AppArmor profiles specified in the PROFILE_DIR directories.\n") 50 flag.PrintDefaults() 51 } 52 flag.Parse() 53 54 dirs = flag.Args() 55 if len(dirs) == 0 { 56 klog.Errorf("Must specify at least one directory.") 57 flag.Usage() 58 os.Exit(1) 59 } 60 61 // Check that the required parser binary is found. 62 if _, err := exec.LookPath(parser); err != nil { 63 klog.Exitf("Required binary %s not found in PATH", parser) 64 } 65 66 // Check that loaded profiles can be read. 67 if _, err := getLoadedProfiles(); err != nil { 68 klog.Exitf("Unable to access apparmor profiles: %v", err) 69 } 70 71 if *poll < 0 { 72 runOnce() 73 } else { 74 pollForever() 75 } 76 } 77 78 // No polling: run once and exit. 79 func runOnce() { 80 if success, newProfiles := loadNewProfiles(); !success { 81 if len(newProfiles) > 0 { 82 klog.Exitf("Not all profiles were successfully loaded. Loaded: %v", newProfiles) 83 } else { 84 klog.Exit("Error loading profiles.") 85 } 86 } else { 87 if len(newProfiles) > 0 { 88 klog.Infof("Successfully loaded profiles: %v", newProfiles) 89 } else { 90 klog.Warning("No new profiles found.") 91 } 92 } 93 } 94 95 // Poll the directories indefinitely. 96 func pollForever() { 97 klog.V(2).Infof("Polling %s every %s", strings.Join(dirs, ", "), poll.String()) 98 pollFn := func() { 99 _, newProfiles := loadNewProfiles() 100 if len(newProfiles) > 0 { 101 klog.V(2).Infof("Successfully loaded profiles: %v", newProfiles) 102 } 103 } 104 pollFn() // Run immediately. 105 ticker := time.NewTicker(*poll) 106 for range ticker.C { 107 pollFn() 108 } 109 } 110 111 func loadNewProfiles() (success bool, newProfiles []string) { 112 loadedProfiles, err := getLoadedProfiles() 113 if err != nil { 114 klog.Errorf("Error reading loaded profiles: %v", err) 115 return false, nil 116 } 117 118 success = true 119 for _, dir := range dirs { 120 infos, err := os.ReadDir(dir) 121 if err != nil { 122 klog.Warningf("Error reading %s: %v", dir, err) 123 success = false 124 continue 125 } 126 127 for _, info := range infos { 128 path := filepath.Join(dir, info.Name()) 129 // If directory, or symlink to a directory, skip it. 130 resolvedInfo, err := resolveSymlink(dir, info) 131 if err != nil { 132 klog.Warningf("Error resolving symlink: %v", err) 133 continue 134 } 135 if resolvedInfo.IsDir() { 136 // Directory listing is shallow. 137 klog.V(4).Infof("Skipping directory %s", path) 138 continue 139 } 140 141 klog.V(4).Infof("Scanning %s for new profiles", path) 142 profiles, err := getProfileNames(path) 143 if err != nil { 144 klog.Warningf("Error reading %s: %v", path, err) 145 success = false 146 continue 147 } 148 149 if unloadedProfiles(loadedProfiles, profiles) { 150 if err := loadProfiles(path); err != nil { 151 klog.Errorf("Could not load profiles: %v", err) 152 success = false 153 continue 154 } 155 // Add new profiles to list of loaded profiles. 156 newProfiles = append(newProfiles, profiles...) 157 for _, profile := range profiles { 158 loadedProfiles[profile] = true 159 } 160 } 161 } 162 } 163 164 return success, newProfiles 165 } 166 167 func getProfileNames(path string) ([]string, error) { 168 cmd := exec.Command(parser, "--names", path) 169 stderr := &bytes.Buffer{} 170 cmd.Stderr = stderr 171 out, err := cmd.Output() 172 if err != nil { 173 if stderr.Len() > 0 { 174 klog.Warning(stderr.String()) 175 } 176 return nil, fmt.Errorf("error reading profiles from %s: %v", path, err) 177 } 178 179 trimmed := strings.TrimSpace(string(out)) // Remove trailing \n 180 return strings.Split(trimmed, "\n"), nil 181 } 182 183 func unloadedProfiles(loadedProfiles map[string]bool, profiles []string) bool { 184 for _, profile := range profiles { 185 if !loadedProfiles[profile] { 186 return true 187 } 188 } 189 return false 190 } 191 192 func loadProfiles(path string) error { 193 cmd := exec.Command(parser, "--verbose", path) 194 stderr := &bytes.Buffer{} 195 cmd.Stderr = stderr 196 out, err := cmd.Output() 197 klog.V(2).Infof("Loading profiles from %s:\n%s", path, out) 198 if err != nil { 199 if stderr.Len() > 0 { 200 klog.Warning(stderr.String()) 201 } 202 return fmt.Errorf("error loading profiles from %s: %v", path, err) 203 } 204 return nil 205 } 206 207 // If the given fileinfo is a symlink, return the FileInfo of the target. Otherwise, return the 208 // given fileinfo. 209 func resolveSymlink(basePath string, entry os.DirEntry) (os.FileInfo, error) { 210 info, err := entry.Info() 211 if err != nil { 212 return nil, fmt.Errorf("error getting the fileInfo: %v", err) 213 } 214 if info.Mode()&os.ModeSymlink == 0 { 215 // Not a symlink. 216 return info, nil 217 } 218 219 fpath := filepath.Join(basePath, entry.Name()) 220 resolvedName, err := filepath.EvalSymlinks(fpath) 221 if err != nil { 222 return nil, fmt.Errorf("error resolving symlink %s: %v", fpath, err) 223 } 224 resolvedInfo, err := os.Stat(resolvedName) 225 if err != nil { 226 return nil, fmt.Errorf("error calling stat on %s: %v", resolvedName, err) 227 } 228 return resolvedInfo, nil 229 } 230 231 // TODO: This is copied from k8s.io/kubernetes/pkg/security/apparmor.getLoadedProfiles. 232 // 233 // Refactor that method to expose it in a reusable way, and delete this version. 234 func getLoadedProfiles() (map[string]bool, error) { 235 profilesPath := path.Join(apparmorfs, "profiles") 236 profilesFile, err := os.Open(profilesPath) 237 if err != nil { 238 return nil, fmt.Errorf("failed to open %s: %v", profilesPath, err) 239 } 240 defer profilesFile.Close() 241 242 profiles := map[string]bool{} 243 scanner := bufio.NewScanner(profilesFile) 244 for scanner.Scan() { 245 profileName := parseProfileName(scanner.Text()) 246 if profileName == "" { 247 // Unknown line format; skip it. 248 continue 249 } 250 profiles[profileName] = true 251 } 252 return profiles, nil 253 } 254 255 // The profiles file is formatted with one profile per line, matching a form: 256 // 257 // namespace://profile-name (mode) 258 // profile-name (mode) 259 // 260 // Where mode is {enforce, complain, kill}. The "namespace://" is only included for namespaced 261 // profiles. For the purposes of Kubernetes, we consider the namespace part of the profile name. 262 func parseProfileName(profileLine string) string { 263 modeIndex := strings.IndexRune(profileLine, '(') 264 if modeIndex < 0 { 265 return "" 266 } 267 return strings.TrimSpace(profileLine[:modeIndex]) 268 }