123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- /*
- Copyright 2015 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package main
- import (
- "bufio"
- "bytes"
- "flag"
- "fmt"
- "io/ioutil"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "strings"
- "time"
- "k8s.io/klog"
- )
- var (
- // The directories to load profiles from.
- dirs []string
- poll = flag.Duration("poll", -1, "Poll the directories for new profiles with this interval. Values < 0 disable polling, and exit after loading the profiles.")
- )
- const (
- parser = "apparmor_parser"
- apparmorfs = "/sys/kernel/security/apparmor"
- )
- func main() {
- flag.Usage = func() {
- fmt.Fprintf(os.Stderr, "Usage: %s [FLAG]... [PROFILE_DIR]...\n", os.Args[0])
- fmt.Fprintf(os.Stderr, "Load the AppArmor profiles specified in the PROFILE_DIR directories.\n")
- flag.PrintDefaults()
- }
- flag.Parse()
- dirs = flag.Args()
- if len(dirs) == 0 {
- klog.Errorf("Must specify at least one directory.")
- flag.Usage()
- os.Exit(1)
- }
- // Check that the required parser binary is found.
- if _, err := exec.LookPath(parser); err != nil {
- klog.Exitf("Required binary %s not found in PATH", parser)
- }
- // Check that loaded profiles can be read.
- if _, err := getLoadedProfiles(); err != nil {
- klog.Exitf("Unable to access apparmor profiles: %v", err)
- }
- if *poll < 0 {
- runOnce()
- } else {
- pollForever()
- }
- }
- // No polling: run once and exit.
- func runOnce() {
- if success, newProfiles := loadNewProfiles(); !success {
- if len(newProfiles) > 0 {
- klog.Exitf("Not all profiles were successfully loaded. Loaded: %v", newProfiles)
- } else {
- klog.Exit("Error loading profiles.")
- }
- } else {
- if len(newProfiles) > 0 {
- klog.Infof("Successfully loaded profiles: %v", newProfiles)
- } else {
- klog.Warning("No new profiles found.")
- }
- }
- }
- // Poll the directories indefinitely.
- func pollForever() {
- klog.V(2).Infof("Polling %s every %s", strings.Join(dirs, ", "), poll.String())
- pollFn := func() {
- _, newProfiles := loadNewProfiles()
- if len(newProfiles) > 0 {
- klog.V(2).Infof("Successfully loaded profiles: %v", newProfiles)
- }
- }
- pollFn() // Run immediately.
- ticker := time.NewTicker(*poll)
- for range ticker.C {
- pollFn()
- }
- }
- func loadNewProfiles() (success bool, newProfiles []string) {
- loadedProfiles, err := getLoadedProfiles()
- if err != nil {
- klog.Errorf("Error reading loaded profiles: %v", err)
- return false, nil
- }
- success = true
- for _, dir := range dirs {
- infos, err := ioutil.ReadDir(dir)
- if err != nil {
- klog.Warningf("Error reading %s: %v", dir, err)
- success = false
- continue
- }
- for _, info := range infos {
- path := filepath.Join(dir, info.Name())
- // If directory, or symlink to a directory, skip it.
- resolvedInfo, err := resolveSymlink(dir, info)
- if err != nil {
- klog.Warningf("Error resolving symlink: %v", err)
- continue
- }
- if resolvedInfo.IsDir() {
- // Directory listing is shallow.
- klog.V(4).Infof("Skipping directory %s", path)
- continue
- }
- klog.V(4).Infof("Scanning %s for new profiles", path)
- profiles, err := getProfileNames(path)
- if err != nil {
- klog.Warningf("Error reading %s: %v", path, err)
- success = false
- continue
- }
- if unloadedProfiles(loadedProfiles, profiles) {
- if err := loadProfiles(path); err != nil {
- klog.Errorf("Could not load profiles: %v", err)
- success = false
- continue
- }
- // Add new profiles to list of loaded profiles.
- newProfiles = append(newProfiles, profiles...)
- for _, profile := range profiles {
- loadedProfiles[profile] = true
- }
- }
- }
- }
- return success, newProfiles
- }
- func getProfileNames(path string) ([]string, error) {
- cmd := exec.Command(parser, "--names", path)
- stderr := &bytes.Buffer{}
- cmd.Stderr = stderr
- out, err := cmd.Output()
- if err != nil {
- if stderr.Len() > 0 {
- klog.Warning(stderr.String())
- }
- return nil, fmt.Errorf("error reading profiles from %s: %v", path, err)
- }
- trimmed := strings.TrimSpace(string(out)) // Remove trailing \n
- return strings.Split(trimmed, "\n"), nil
- }
- func unloadedProfiles(loadedProfiles map[string]bool, profiles []string) bool {
- for _, profile := range profiles {
- if !loadedProfiles[profile] {
- return true
- }
- }
- return false
- }
- func loadProfiles(path string) error {
- cmd := exec.Command(parser, "--verbose", path)
- stderr := &bytes.Buffer{}
- cmd.Stderr = stderr
- out, err := cmd.Output()
- klog.V(2).Infof("Loading profiles from %s:\n%s", path, out)
- if err != nil {
- if stderr.Len() > 0 {
- klog.Warning(stderr.String())
- }
- return fmt.Errorf("error loading profiles from %s: %v", path, err)
- }
- return nil
- }
- // If the given fileinfo is a symlink, return the FileInfo of the target. Otherwise, return the
- // given fileinfo.
- func resolveSymlink(basePath string, info os.FileInfo) (os.FileInfo, error) {
- if info.Mode()&os.ModeSymlink == 0 {
- // Not a symlink.
- return info, nil
- }
- fpath := filepath.Join(basePath, info.Name())
- resolvedName, err := filepath.EvalSymlinks(fpath)
- if err != nil {
- return nil, fmt.Errorf("error resolving symlink %s: %v", fpath, err)
- }
- resolvedInfo, err := os.Stat(resolvedName)
- if err != nil {
- return nil, fmt.Errorf("error calling stat on %s: %v", resolvedName, err)
- }
- return resolvedInfo, nil
- }
- // TODO: This is copied from k8s.io/kubernetes/pkg/security/apparmor.getLoadedProfiles.
- // Refactor that method to expose it in a reusable way, and delete this version.
- func getLoadedProfiles() (map[string]bool, error) {
- profilesPath := path.Join(apparmorfs, "profiles")
- profilesFile, err := os.Open(profilesPath)
- if err != nil {
- return nil, fmt.Errorf("failed to open %s: %v", profilesPath, err)
- }
- defer profilesFile.Close()
- profiles := map[string]bool{}
- scanner := bufio.NewScanner(profilesFile)
- for scanner.Scan() {
- profileName := parseProfileName(scanner.Text())
- if profileName == "" {
- // Unknown line format; skip it.
- continue
- }
- profiles[profileName] = true
- }
- return profiles, nil
- }
- // The profiles file is formatted with one profile per line, matching a form:
- // namespace://profile-name (mode)
- // profile-name (mode)
- // Where mode is {enforce, complain, kill}. The "namespace://" is only included for namespaced
- // profiles. For the purposes of Kubernetes, we consider the namespace part of the profile name.
- func parseProfileName(profileLine string) string {
- modeIndex := strings.IndexRune(profileLine, '(')
- if modeIndex < 0 {
- return ""
- }
- return strings.TrimSpace(profileLine[:modeIndex])
- }
|