github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/pkg/apparmor/apparmor_linux.go (about)

     1  // +build linux,apparmor
     2  
     3  package apparmor
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/exec"
    12  	"path"
    13  	"strconv"
    14  	"strings"
    15  	"text/template"
    16  
    17  	"github.com/containers/libpod/pkg/rootless"
    18  	runcaa "github.com/opencontainers/runc/libcontainer/apparmor"
    19  	"github.com/pkg/errors"
    20  	"github.com/sirupsen/logrus"
    21  )
    22  
    23  // profileDirectory is the file store for apparmor profiles and macros.
    24  var profileDirectory = "/etc/apparmor.d"
    25  
    26  // IsEnabled returns true if AppArmor is enabled on the host.
    27  func IsEnabled() bool {
    28  	if rootless.IsRootless() {
    29  		return false
    30  	}
    31  	return runcaa.IsEnabled()
    32  }
    33  
    34  // profileData holds information about the given profile for generation.
    35  type profileData struct {
    36  	// Name is profile name.
    37  	Name string
    38  	// Imports defines the apparmor functions to import, before defining the profile.
    39  	Imports []string
    40  	// InnerImports defines the apparmor functions to import in the profile.
    41  	InnerImports []string
    42  	// Version is the {major, minor, patch} version of apparmor_parser as a single number.
    43  	Version int
    44  }
    45  
    46  // generateDefault creates an apparmor profile from ProfileData.
    47  func (p *profileData) generateDefault(out io.Writer) error {
    48  	compiled, err := template.New("apparmor_profile").Parse(libpodProfileTemplate)
    49  	if err != nil {
    50  		return err
    51  	}
    52  
    53  	if macroExists("tunables/global") {
    54  		p.Imports = append(p.Imports, "#include <tunables/global>")
    55  	} else {
    56  		p.Imports = append(p.Imports, "@{PROC}=/proc/")
    57  	}
    58  
    59  	if macroExists("abstractions/base") {
    60  		p.InnerImports = append(p.InnerImports, "#include <abstractions/base>")
    61  	}
    62  
    63  	ver, err := getAAParserVersion()
    64  	if err != nil {
    65  		return err
    66  	}
    67  	p.Version = ver
    68  
    69  	return compiled.Execute(out, p)
    70  }
    71  
    72  // macrosExists checks if the passed macro exists.
    73  func macroExists(m string) bool {
    74  	_, err := os.Stat(path.Join(profileDirectory, m))
    75  	return err == nil
    76  }
    77  
    78  // InstallDefault generates a default profile and loads it into the kernel
    79  // using 'apparmor_parser'.
    80  func InstallDefault(name string) error {
    81  	if rootless.IsRootless() {
    82  		return ErrApparmorRootless
    83  	}
    84  
    85  	p := profileData{
    86  		Name: name,
    87  	}
    88  
    89  	cmd := exec.Command("apparmor_parser", "-Kr")
    90  	pipe, err := cmd.StdinPipe()
    91  	if err != nil {
    92  		return err
    93  	}
    94  	if err := cmd.Start(); err != nil {
    95  		if pipeErr := pipe.Close(); pipeErr != nil {
    96  			logrus.Errorf("unable to close apparmor pipe: %q", pipeErr)
    97  		}
    98  		return err
    99  	}
   100  	if err := p.generateDefault(pipe); err != nil {
   101  		if pipeErr := pipe.Close(); pipeErr != nil {
   102  			logrus.Errorf("unable to close apparmor pipe: %q", pipeErr)
   103  		}
   104  		if cmdErr := cmd.Wait(); cmdErr != nil {
   105  			logrus.Errorf("unable to wait for apparmor command: %q", cmdErr)
   106  		}
   107  		return err
   108  	}
   109  
   110  	if pipeErr := pipe.Close(); pipeErr != nil {
   111  		logrus.Errorf("unable to close apparmor pipe: %q", pipeErr)
   112  	}
   113  	return cmd.Wait()
   114  }
   115  
   116  // DefaultContent returns the default profile content as byte slice. The
   117  // profile is named as the provided `name`. The function errors if the profile
   118  // generation fails.
   119  func DefaultContent(name string) ([]byte, error) {
   120  	p := profileData{Name: name}
   121  	var bytes bytes.Buffer
   122  	if err := p.generateDefault(&bytes); err != nil {
   123  		return nil, err
   124  	}
   125  	return bytes.Bytes(), nil
   126  }
   127  
   128  // IsLoaded checks if a profile with the given name has been loaded into the
   129  // kernel.
   130  func IsLoaded(name string) (bool, error) {
   131  	if name != "" && rootless.IsRootless() {
   132  		return false, errors.Wrapf(ErrApparmorRootless, "cannot load AppArmor profile %q", name)
   133  	}
   134  
   135  	file, err := os.Open("/sys/kernel/security/apparmor/profiles")
   136  	if err != nil {
   137  		if os.IsNotExist(err) {
   138  			return false, nil
   139  		}
   140  		return false, err
   141  	}
   142  	defer file.Close()
   143  
   144  	r := bufio.NewReader(file)
   145  	for {
   146  		p, err := r.ReadString('\n')
   147  		if err == io.EOF {
   148  			break
   149  		}
   150  		if err != nil {
   151  			return false, err
   152  		}
   153  		if strings.HasPrefix(p, name+" ") {
   154  			return true, nil
   155  		}
   156  	}
   157  
   158  	return false, nil
   159  }
   160  
   161  // execAAParser runs `apparmor_parser` with the passed arguments.
   162  func execAAParser(dir string, args ...string) (string, error) {
   163  	c := exec.Command("apparmor_parser", args...)
   164  	c.Dir = dir
   165  
   166  	output, err := c.CombinedOutput()
   167  	if err != nil {
   168  		return "", fmt.Errorf("running `%s %s` failed with output: %s\nerror: %v", c.Path, strings.Join(c.Args, " "), output, err)
   169  	}
   170  
   171  	return string(output), nil
   172  }
   173  
   174  // getAAParserVersion returns the major and minor version of apparmor_parser.
   175  func getAAParserVersion() (int, error) {
   176  	output, err := execAAParser("", "--version")
   177  	if err != nil {
   178  		return -1, err
   179  	}
   180  	return parseAAParserVersion(output)
   181  }
   182  
   183  // parseAAParserVersion parses the given `apparmor_parser --version` output and
   184  // returns the major and minor version number as an integer.
   185  func parseAAParserVersion(output string) (int, error) {
   186  	// output is in the form of the following:
   187  	// AppArmor parser version 2.9.1
   188  	// Copyright (C) 1999-2008 Novell Inc.
   189  	// Copyright 2009-2012 Canonical Ltd.
   190  	lines := strings.SplitN(output, "\n", 2)
   191  	words := strings.Split(lines[0], " ")
   192  	version := words[len(words)-1]
   193  
   194  	// split by major minor version
   195  	v := strings.Split(version, ".")
   196  	if len(v) == 0 || len(v) > 3 {
   197  		return -1, fmt.Errorf("parsing version failed for output: `%s`", output)
   198  	}
   199  
   200  	// Default the versions to 0.
   201  	var majorVersion, minorVersion, patchLevel int
   202  
   203  	majorVersion, err := strconv.Atoi(v[0])
   204  	if err != nil {
   205  		return -1, err
   206  	}
   207  
   208  	if len(v) > 1 {
   209  		minorVersion, err = strconv.Atoi(v[1])
   210  		if err != nil {
   211  			return -1, err
   212  		}
   213  	}
   214  	if len(v) > 2 {
   215  		patchLevel, err = strconv.Atoi(v[2])
   216  		if err != nil {
   217  			return -1, err
   218  		}
   219  	}
   220  
   221  	// major*10^5 + minor*10^3 + patch*10^0
   222  	numericVersion := majorVersion*1e5 + minorVersion*1e3 + patchLevel
   223  	return numericVersion, nil
   224  
   225  }
   226  
   227  // CheckProfileAndLoadDefault checks if the specified profile is loaded and
   228  // loads the DefaultLibpodProfile if the specified on is prefixed by
   229  // DefaultLipodProfilePrefix.  This allows to always load and apply the latest
   230  // default AppArmor profile.  Note that AppArmor requires root.  If it's a
   231  // default profile, return DefaultLipodProfilePrefix, otherwise the specified
   232  // one.
   233  func CheckProfileAndLoadDefault(name string) (string, error) {
   234  	if name == "unconfined" {
   235  		return name, nil
   236  	}
   237  
   238  	// AppArmor is not supported in rootless mode as it requires root
   239  	// privileges.  Return an error in case a specific profile is specified.
   240  	if rootless.IsRootless() {
   241  		if name != "" {
   242  			return "", errors.Wrapf(ErrApparmorRootless, "cannot load AppArmor profile %q", name)
   243  		} else {
   244  			logrus.Debug("skipping loading default AppArmor profile (rootless mode)")
   245  			return "", nil
   246  		}
   247  	}
   248  
   249  	// Check if AppArmor is disabled and error out if a profile is to be set.
   250  	if !runcaa.IsEnabled() {
   251  		if name == "" {
   252  			return "", nil
   253  		} else {
   254  			return "", fmt.Errorf("profile %q specified but AppArmor is disabled on the host", name)
   255  		}
   256  	}
   257  
   258  	// If the specified name is not empty or is not a default libpod one,
   259  	// ignore it and return the name.
   260  	if name != "" && !strings.HasPrefix(name, DefaultLipodProfilePrefix) {
   261  		isLoaded, err := IsLoaded(name)
   262  		if err != nil {
   263  			return "", err
   264  		}
   265  		if !isLoaded {
   266  			return "", fmt.Errorf("AppArmor profile %q specified but not loaded", name)
   267  		}
   268  		return name, nil
   269  	}
   270  
   271  	name = DefaultLibpodProfile
   272  	// To avoid expensive redundant loads on each invocation, check
   273  	// if it's loaded before installing it.
   274  	isLoaded, err := IsLoaded(name)
   275  	if err != nil {
   276  		return "", err
   277  	}
   278  	if !isLoaded {
   279  		err = InstallDefault(name)
   280  		if err != nil {
   281  			return "", err
   282  		}
   283  		logrus.Infof("successfully loaded AppAmor profile %q", name)
   284  	} else {
   285  		logrus.Infof("AppAmor profile %q is already loaded", name)
   286  	}
   287  
   288  	return name, nil
   289  }