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  }