apparmor: apply default profile at container initialization

Apply the default AppArmor profile at container initialization to cover
all possible code paths (i.e., podman-{start,run}) before executing the
runtime.  This allows moving most of the logic into pkg/apparmor.

Also make the loading and application of the default AppArmor profile
versio-indepenent by checking for the `libpod-default-` prefix and
over-writing the profile in the run-time spec if needed.

The intitial run-time spec of the container differs a bit from the
applied one when having started the container, which results in
displaying a potentially outdated AppArmor profile when inspecting
a container.  To fix that, load the container config from the file
system if present and use it to display the data.

Fixes: #2107
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg 2019-01-09 14:54:58 +01:00
parent c37f731596
commit edb285d176
9 changed files with 123 additions and 342 deletions

View File

@ -15,13 +15,11 @@ import (
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
ann "github.com/containers/libpod/pkg/annotations" ann "github.com/containers/libpod/pkg/annotations"
"github.com/containers/libpod/pkg/apparmor"
"github.com/containers/libpod/pkg/inspect" "github.com/containers/libpod/pkg/inspect"
ns "github.com/containers/libpod/pkg/namespaces" ns "github.com/containers/libpod/pkg/namespaces"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
cc "github.com/containers/libpod/pkg/spec" cc "github.com/containers/libpod/pkg/spec"
"github.com/containers/libpod/pkg/util" "github.com/containers/libpod/pkg/util"
libpodVersion "github.com/containers/libpod/version"
"github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/signal"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/docker/go-units" "github.com/docker/go-units"
@ -162,83 +160,9 @@ func createContainer(c *cli.Context, runtime *libpod.Runtime) (*libpod.Container
return ctr, createConfig, nil return ctr, createConfig, nil
} }
// Checks if a user-specified AppArmor profile is loaded, or loads the default profile if
// AppArmor is enabled.
// Any interaction with AppArmor requires root permissions.
func loadAppArmor(config *cc.CreateConfig) error {
if rootless.IsRootless() {
noAAMsg := "AppArmor security is not available in rootless mode"
switch config.ApparmorProfile {
case "":
logrus.Warn(noAAMsg)
case "unconfined":
default:
return fmt.Errorf(noAAMsg)
}
return nil
}
if config.ApparmorProfile == "" && apparmor.IsEnabled() {
// Unless specified otherwise, make sure that the default AppArmor
// profile is installed. To avoid redundantly loading the profile
// on each invocation, check if it's loaded before installing it.
// Suffix the profile with the current libpod version to allow
// loading the new, potentially updated profile after an update.
profile := fmt.Sprintf("%s-%s", apparmor.DefaultLibpodProfile, libpodVersion.Version)
loadProfile := func() error {
isLoaded, err := apparmor.IsLoaded(profile)
if err != nil {
return err
}
if !isLoaded {
err = apparmor.InstallDefault(profile)
if err != nil {
return err
}
}
return nil
}
if err := loadProfile(); err != nil {
switch err {
case apparmor.ErrApparmorUnsupported:
// do not set the profile when AppArmor isn't supported
logrus.Debugf("AppArmor is not supported: setting empty profile")
default:
return err
}
} else {
logrus.Infof("Sucessfully loaded AppAmor profile '%s'", profile)
config.ApparmorProfile = profile
}
} else if config.ApparmorProfile != "" && config.ApparmorProfile != "unconfined" {
if !apparmor.IsEnabled() {
return fmt.Errorf("Profile specified but AppArmor is disabled on the host")
}
isLoaded, err := apparmor.IsLoaded(config.ApparmorProfile)
if err != nil {
switch err {
case apparmor.ErrApparmorUnsupported:
return fmt.Errorf("Profile specified but AppArmor is not supported")
default:
return fmt.Errorf("Error checking if AppArmor profile is loaded: %v", err)
}
}
if !isLoaded {
return fmt.Errorf("The specified AppArmor profile '%s' is not loaded", config.ApparmorProfile)
}
}
return nil
}
func parseSecurityOpt(config *cc.CreateConfig, securityOpts []string) error { func parseSecurityOpt(config *cc.CreateConfig, securityOpts []string) error {
var ( var (
labelOpts []string labelOpts []string
err error
) )
if config.PidMode.IsHost() { if config.PidMode.IsHost() {
@ -283,10 +207,6 @@ func parseSecurityOpt(config *cc.CreateConfig, securityOpts []string) error {
} }
} }
if err := loadAppArmor(config); err != nil {
return err
}
if config.SeccompProfilePath == "" { if config.SeccompProfilePath == "" {
if _, err := os.Stat(libpod.SeccompOverridePath); err == nil { if _, err := os.Stat(libpod.SeccompOverridePath); err == nil {
config.SeccompProfilePath = libpod.SeccompOverridePath config.SeccompProfilePath = libpod.SeccompOverridePath
@ -304,7 +224,7 @@ func parseSecurityOpt(config *cc.CreateConfig, securityOpts []string) error {
} }
} }
config.LabelOpts = labelOpts config.LabelOpts = labelOpts
return err return nil
} }
// isPortInPortBindings determines if an exposed host port is in user // isPortInPortBindings determines if an exposed host port is in user

View File

@ -1,7 +1,9 @@
package libpod package libpod
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
@ -407,6 +409,30 @@ func (c *Container) Spec() *spec.Spec {
return returnSpec return returnSpec
} }
// specFromState returns the unmarshalled json config of the container. If the
// config does not exist (e.g., because the container was never started) return
// the spec from the config.
func (c *Container) specFromState() (*spec.Spec, error) {
spec := c.config.Spec
if f, err := os.Open(c.state.ConfigPath); err == nil {
content, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrapf(err, "error reading container config")
}
if err := json.Unmarshal([]byte(content), &spec); err != nil {
return nil, errors.Wrapf(err, "error unmarshalling container config")
}
} else {
// ignore when the file does not exist
if !os.IsNotExist(err) {
return nil, errors.Wrapf(err, "error opening container config")
}
}
return spec, nil
}
// ID returns the container's ID // ID returns the container's ID
func (c *Container) ID() string { func (c *Container) ID() string {
return c.config.ID return c.config.ID

View File

@ -12,7 +12,10 @@ import (
func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data) (*inspect.ContainerInspectData, error) { func (c *Container) getContainerInspectData(size bool, driverData *inspect.Data) (*inspect.ContainerInspectData, error) {
config := c.config config := c.config
runtimeInfo := c.state runtimeInfo := c.state
spec := c.config.Spec spec, err := c.specFromState()
if err != nil {
return nil, err
}
// Process is allowed to be nil in the spec // Process is allowed to be nil in the spec
args := []string{} args := []string{}

View File

@ -20,6 +20,7 @@ import (
cnitypes "github.com/containernetworking/cni/pkg/types/current" cnitypes "github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/ns"
crioAnnotations "github.com/containers/libpod/pkg/annotations" crioAnnotations "github.com/containers/libpod/pkg/annotations"
"github.com/containers/libpod/pkg/apparmor"
"github.com/containers/libpod/pkg/criu" "github.com/containers/libpod/pkg/criu"
"github.com/containers/libpod/pkg/lookup" "github.com/containers/libpod/pkg/lookup"
"github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/resolvconf"
@ -185,6 +186,13 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
} }
} }
// Apply AppArmor checks and load the default profile if needed.
updatedProfile, err := apparmor.CheckProfileAndLoadDefault(c.config.Spec.Process.ApparmorProfile)
if err != nil {
return nil, err
}
g.SetProcessApparmorProfile(updatedProfile)
if err := c.makeBindMounts(); err != nil { if err := c.makeBindMounts(); err != nil {
return nil, err return nil, err
} }

View File

@ -2,11 +2,16 @@ package apparmor
import ( import (
"errors" "errors"
libpodVersion "github.com/containers/libpod/version"
) )
var ( var (
// DefaultLipodProfilePrefix is used for version-independent presence checks.
DefaultLipodProfilePrefix = "libpod-default" + "-"
// DefaultLibpodProfile is the name of default libpod AppArmor profile. // DefaultLibpodProfile is the name of default libpod AppArmor profile.
DefaultLibpodProfile = "libpod-default" DefaultLibpodProfile = DefaultLipodProfilePrefix + libpodVersion.Version
// ErrApparmorUnsupported indicates that AppArmor support is not supported. // ErrApparmorUnsupported indicates that AppArmor support is not supported.
ErrApparmorUnsupported = errors.New("AppArmor is not supported") ErrApparmorUnsupported = errors.New("AppArmor is not supported")
// ErrApparmorRootless indicates that AppArmor support is not supported in rootless mode.
ErrApparmorRootless = errors.New("AppArmor is not supported in rootless mode")
) )

View File

@ -13,7 +13,10 @@ import (
"strings" "strings"
"text/template" "text/template"
"github.com/containers/libpod/pkg/rootless"
runcaa "github.com/opencontainers/runc/libcontainer/apparmor" runcaa "github.com/opencontainers/runc/libcontainer/apparmor"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
) )
// profileDirectory is the file store for apparmor profiles and macros. // profileDirectory is the file store for apparmor profiles and macros.
@ -21,6 +24,9 @@ var profileDirectory = "/etc/apparmor.d"
// IsEnabled returns true if AppArmor is enabled on the host. // IsEnabled returns true if AppArmor is enabled on the host.
func IsEnabled() bool { func IsEnabled() bool {
if rootless.IsRootless() {
return false
}
return runcaa.IsEnabled() return runcaa.IsEnabled()
} }
@ -71,6 +77,10 @@ func macroExists(m string) bool {
// InstallDefault generates a default profile and loads it into the kernel // InstallDefault generates a default profile and loads it into the kernel
// using 'apparmor_parser'. // using 'apparmor_parser'.
func InstallDefault(name string) error { func InstallDefault(name string) error {
if rootless.IsRootless() {
return ErrApparmorRootless
}
p := profileData{ p := profileData{
Name: name, Name: name,
} }
@ -97,6 +107,10 @@ func InstallDefault(name string) error {
// IsLoaded checks if a profile with the given name has been loaded into the // IsLoaded checks if a profile with the given name has been loaded into the
// kernel. // kernel.
func IsLoaded(name string) (bool, error) { func IsLoaded(name string) (bool, error) {
if name != "" && rootless.IsRootless() {
return false, errors.Wrapf(ErrApparmorRootless, "cannot load AppArmor profile %q", name)
}
file, err := os.Open("/sys/kernel/security/apparmor/profiles") file, err := os.Open("/sys/kernel/security/apparmor/profiles")
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -188,3 +202,55 @@ func parseAAParserVersion(output string) (int, error) {
return numericVersion, nil return numericVersion, nil
} }
// CheckProfileAndLoadDefault checks if the specified profile is loaded and
// loads the DefaultLibpodProfile if the specified on is prefixed by
// DefaultLipodProfilePrefix. This allows to always load and apply the latest
// default AppArmor profile. Note that AppArmor requires root. If it's a
// default profile, return DefaultLipodProfilePrefix, otherwise the specified
// one.
func CheckProfileAndLoadDefault(name string) (string, error) {
if name == "unconfined" {
return name, nil
}
if name != "" && rootless.IsRootless() {
return "", errors.Wrapf(ErrApparmorRootless, "cannot load AppArmor profile %q", name)
}
if name != "" && !runcaa.IsEnabled() {
return "", fmt.Errorf("profile %q specified but AppArmor is disabled on the host", name)
}
// If the specified name is not empty or is not a default libpod one,
// ignore it and return the name.
if name != "" && !strings.HasPrefix(name, DefaultLipodProfilePrefix) {
isLoaded, err := IsLoaded(name)
if err != nil {
return "", err
}
if !isLoaded {
return "", fmt.Errorf("AppArmor profile %q specified but not loaded")
}
return name, nil
}
name = DefaultLibpodProfile
// To avoid expensive redundant loads on each invocation, check
// if it's loaded before installing it.
isLoaded, err := IsLoaded(name)
if err != nil {
return "", err
}
if !isLoaded {
err = InstallDefault(name)
if err != nil {
return "", err
}
logrus.Infof("successfully loaded AppAmor profile %q", name)
} else {
logrus.Infof("AppAmor profile %q is already loaded", name)
}
return name, nil
}

View File

@ -2,19 +2,25 @@
package apparmor package apparmor
// IsEnabled returns true if AppArmor is enabled on the host. // IsEnabled dummy.
func IsEnabled() bool { func IsEnabled() bool {
return false return false
} }
// InstallDefault generates a default profile in a temp directory determined by // InstallDefault dummy.
// os.TempDir(), then loads the profile into the kernel using 'apparmor_parser'.
func InstallDefault(name string) error { func InstallDefault(name string) error {
return ErrApparmorUnsupported return ErrApparmorUnsupported
} }
// IsLoaded checks if a profile with the given name has been loaded into the // IsLoaded dummy.
// kernel.
func IsLoaded(name string) (bool, error) { func IsLoaded(name string) (bool, error) {
return false, ErrApparmorUnsupported return false, ErrApparmorUnsupported
} }
// CheckProfileAndLoadDefault dummy.
func CheckProfileAndLoadDefault(name string) (string, error) {
if name == "" {
return "", nil
}
return "", ErrApparmorUnsupported
}

View File

@ -252,6 +252,7 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint
} }
// SECURITY OPTS // SECURITY OPTS
g.SetProcessNoNewPrivileges(config.NoNewPrivs) g.SetProcessNoNewPrivileges(config.NoNewPrivs)
g.SetProcessApparmorProfile(config.ApparmorProfile) g.SetProcessApparmorProfile(config.ApparmorProfile)
blockAccessToKernelFilesystems(config, &g) blockAccessToKernelFilesystems(config, &g)

View File

@ -1,254 +0,0 @@
package sysinfo
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
func findCgroupMountpoints() (map[string]string, error) {
cgMounts, err := cgroups.GetCgroupMounts(false)
if err != nil {
return nil, fmt.Errorf("Failed to parse cgroup information: %v", err)
}
mps := make(map[string]string)
for _, m := range cgMounts {
for _, ss := range m.Subsystems {
mps[ss] = m.Mountpoint
}
}
return mps, nil
}
// New returns a new SysInfo, using the filesystem to detect which features
// the kernel supports. If `quiet` is `false` warnings are printed in logs
// whenever an error occurs or misconfigurations are present.
func New(quiet bool) *SysInfo {
sysInfo := &SysInfo{}
cgMounts, err := findCgroupMountpoints()
if err != nil {
logrus.Warnf("Failed to parse cgroup information: %v", err)
} else {
sysInfo.cgroupMemInfo = checkCgroupMem(cgMounts, quiet)
sysInfo.cgroupCPUInfo = checkCgroupCPU(cgMounts, quiet)
sysInfo.cgroupBlkioInfo = checkCgroupBlkioInfo(cgMounts, quiet)
sysInfo.cgroupCpusetInfo = checkCgroupCpusetInfo(cgMounts, quiet)
sysInfo.cgroupPids = checkCgroupPids(quiet)
}
_, ok := cgMounts["devices"]
sysInfo.CgroupDevicesEnabled = ok
sysInfo.IPv4ForwardingDisabled = !readProcBool("/proc/sys/net/ipv4/ip_forward")
sysInfo.BridgeNFCallIPTablesDisabled = !readProcBool("/proc/sys/net/bridge/bridge-nf-call-iptables")
sysInfo.BridgeNFCallIP6TablesDisabled = !readProcBool("/proc/sys/net/bridge/bridge-nf-call-ip6tables")
// Check if AppArmor is supported.
if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) {
sysInfo.AppArmor = true
}
// Check if Seccomp is supported, via CONFIG_SECCOMP.
if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL {
// Make sure the kernel has CONFIG_SECCOMP_FILTER.
if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL {
sysInfo.Seccomp = true
}
}
return sysInfo
}
// checkCgroupMem reads the memory information from the memory cgroup mount point.
func checkCgroupMem(cgMounts map[string]string, quiet bool) cgroupMemInfo {
mountPoint, ok := cgMounts["memory"]
if !ok {
if !quiet {
logrus.Warn("Your kernel does not support cgroup memory limit")
}
return cgroupMemInfo{}
}
swapLimit := cgroupEnabled(mountPoint, "memory.memsw.limit_in_bytes")
if !quiet && !swapLimit {
logrus.Warn("Your kernel does not support swap memory limit")
}
memoryReservation := cgroupEnabled(mountPoint, "memory.soft_limit_in_bytes")
if !quiet && !memoryReservation {
logrus.Warn("Your kernel does not support memory reservation")
}
oomKillDisable := cgroupEnabled(mountPoint, "memory.oom_control")
if !quiet && !oomKillDisable {
logrus.Warn("Your kernel does not support oom control")
}
memorySwappiness := cgroupEnabled(mountPoint, "memory.swappiness")
if !quiet && !memorySwappiness {
logrus.Warn("Your kernel does not support memory swappiness")
}
kernelMemory := cgroupEnabled(mountPoint, "memory.kmem.limit_in_bytes")
if !quiet && !kernelMemory {
logrus.Warn("Your kernel does not support kernel memory limit")
}
return cgroupMemInfo{
MemoryLimit: true,
SwapLimit: swapLimit,
MemoryReservation: memoryReservation,
OomKillDisable: oomKillDisable,
MemorySwappiness: memorySwappiness,
KernelMemory: kernelMemory,
}
}
// checkCgroupCPU reads the cpu information from the cpu cgroup mount point.
func checkCgroupCPU(cgMounts map[string]string, quiet bool) cgroupCPUInfo {
mountPoint, ok := cgMounts["cpu"]
if !ok {
if !quiet {
logrus.Warn("Unable to find cpu cgroup in mounts")
}
return cgroupCPUInfo{}
}
cpuShares := cgroupEnabled(mountPoint, "cpu.shares")
if !quiet && !cpuShares {
logrus.Warn("Your kernel does not support cgroup cpu shares")
}
cpuCfsPeriod := cgroupEnabled(mountPoint, "cpu.cfs_period_us")
if !quiet && !cpuCfsPeriod {
logrus.Warn("Your kernel does not support cgroup cfs period")
}
cpuCfsQuota := cgroupEnabled(mountPoint, "cpu.cfs_quota_us")
if !quiet && !cpuCfsQuota {
logrus.Warn("Your kernel does not support cgroup cfs quotas")
}
cpuRealtimePeriod := cgroupEnabled(mountPoint, "cpu.rt_period_us")
if !quiet && !cpuRealtimePeriod {
logrus.Warn("Your kernel does not support cgroup rt period")
}
cpuRealtimeRuntime := cgroupEnabled(mountPoint, "cpu.rt_runtime_us")
if !quiet && !cpuRealtimeRuntime {
logrus.Warn("Your kernel does not support cgroup rt runtime")
}
return cgroupCPUInfo{
CPUShares: cpuShares,
CPUCfsPeriod: cpuCfsPeriod,
CPUCfsQuota: cpuCfsQuota,
CPURealtimePeriod: cpuRealtimePeriod,
CPURealtimeRuntime: cpuRealtimeRuntime,
}
}
// checkCgroupBlkioInfo reads the blkio information from the blkio cgroup mount point.
func checkCgroupBlkioInfo(cgMounts map[string]string, quiet bool) cgroupBlkioInfo {
mountPoint, ok := cgMounts["blkio"]
if !ok {
if !quiet {
logrus.Warn("Unable to find blkio cgroup in mounts")
}
return cgroupBlkioInfo{}
}
weight := cgroupEnabled(mountPoint, "blkio.weight")
if !quiet && !weight {
logrus.Warn("Your kernel does not support cgroup blkio weight")
}
weightDevice := cgroupEnabled(mountPoint, "blkio.weight_device")
if !quiet && !weightDevice {
logrus.Warn("Your kernel does not support cgroup blkio weight_device")
}
readBpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.read_bps_device")
if !quiet && !readBpsDevice {
logrus.Warn("Your kernel does not support cgroup blkio throttle.read_bps_device")
}
writeBpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.write_bps_device")
if !quiet && !writeBpsDevice {
logrus.Warn("Your kernel does not support cgroup blkio throttle.write_bps_device")
}
readIOpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.read_iops_device")
if !quiet && !readIOpsDevice {
logrus.Warn("Your kernel does not support cgroup blkio throttle.read_iops_device")
}
writeIOpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.write_iops_device")
if !quiet && !writeIOpsDevice {
logrus.Warn("Your kernel does not support cgroup blkio throttle.write_iops_device")
}
return cgroupBlkioInfo{
BlkioWeight: weight,
BlkioWeightDevice: weightDevice,
BlkioReadBpsDevice: readBpsDevice,
BlkioWriteBpsDevice: writeBpsDevice,
BlkioReadIOpsDevice: readIOpsDevice,
BlkioWriteIOpsDevice: writeIOpsDevice,
}
}
// checkCgroupCpusetInfo reads the cpuset information from the cpuset cgroup mount point.
func checkCgroupCpusetInfo(cgMounts map[string]string, quiet bool) cgroupCpusetInfo {
mountPoint, ok := cgMounts["cpuset"]
if !ok {
if !quiet {
logrus.Warn("Unable to find cpuset cgroup in mounts")
}
return cgroupCpusetInfo{}
}
cpus, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.cpus"))
if err != nil {
return cgroupCpusetInfo{}
}
mems, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.mems"))
if err != nil {
return cgroupCpusetInfo{}
}
return cgroupCpusetInfo{
Cpuset: true,
Cpus: strings.TrimSpace(string(cpus)),
Mems: strings.TrimSpace(string(mems)),
}
}
// checkCgroupPids reads the pids information from the pids cgroup mount point.
func checkCgroupPids(quiet bool) cgroupPids {
_, err := cgroups.FindCgroupMountpoint("pids")
if err != nil {
if !quiet {
logrus.Warn(err)
}
return cgroupPids{}
}
return cgroupPids{
PidsLimit: true,
}
}
func cgroupEnabled(mountPoint, name string) bool {
_, err := os.Stat(path.Join(mountPoint, name))
return err == nil
}
func readProcBool(path string) bool {
val, err := ioutil.ReadFile(path)
if err != nil {
return false
}
return strings.TrimSpace(string(val)) == "1"
}