spiegel_podman/pkg/specgen/generate/security_linux.go
Matt Heon 174631f726 Convert SpecGen values to be nullable where possible
SpecGen is our primary container creation abstraction, and is
used to connect our CLI to the Libpod container creation backend.
Because container creation has a million options (I exaggerate
only slightly), the struct is composed of several other structs,
many of which are quite large.

The core problem is that SpecGen is also an API type - it's used
in remote Podman. There, we have a client and a server, and we
want to respect the server's containers.conf. But how do we tell
what parts of SpecGen were set by the client explicitly, and what
parts were not? If we're not using nullable values, an explicit
empty string and a value never being set are identical - and we
can't tell if it's safe to grab a default from the server's
containers.conf.

Fortunately, we only really need to do this for booleans. An
empty string is sufficient to tell us that a string was unset
(even if the user explicitly gave us an empty string for an
option, filling in a default from the config file is acceptable).
This makes things a lot simpler. My initial attempt at this
changed everything, including strings, and it was far larger and
more painful.

Also, begin the first steps of removing all uses of
containers.conf defaults from client-side. Two are gone entirely,
the rest are marked as remove-when-possible.

[NO NEW TESTS NEEDED] This is just a refactor.

Signed-off-by: Matt Heon <mheon@redhat.com>
2024-01-30 10:42:24 -05:00

273 lines
9.2 KiB
Go

//go:build !remote
package generate
import (
"fmt"
"strings"
"github.com/containers/common/libimage"
"github.com/containers/common/pkg/apparmor"
"github.com/containers/common/pkg/capabilities"
"github.com/containers/common/pkg/config"
"github.com/containers/podman/v4/libpod"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/specgen"
"github.com/containers/podman/v4/pkg/util"
"github.com/opencontainers/runtime-tools/generate"
"github.com/opencontainers/selinux/go-selinux/label"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
// setLabelOpts sets the label options of the SecurityConfig according to the
// input.
func setLabelOpts(s *specgen.SpecGenerator, runtime *libpod.Runtime, pidConfig specgen.Namespace, ipcConfig specgen.Namespace) error {
if !runtime.EnableLabeling() || s.IsPrivileged() {
s.SelinuxOpts = label.DisableSecOpt()
return nil
}
var labelOpts []string
if pidConfig.IsHost() {
labelOpts = append(labelOpts, label.DisableSecOpt()...)
} else if pidConfig.IsContainer() {
ctr, err := runtime.LookupContainer(pidConfig.Value)
if err != nil {
return fmt.Errorf("container %q not found: %w", pidConfig.Value, err)
}
secopts, err := label.DupSecOpt(ctr.ProcessLabel())
if err != nil {
return fmt.Errorf("failed to duplicate label %q : %w", ctr.ProcessLabel(), err)
}
labelOpts = append(labelOpts, secopts...)
}
if ipcConfig.IsHost() {
labelOpts = append(labelOpts, label.DisableSecOpt()...)
} else if ipcConfig.IsContainer() {
ctr, err := runtime.LookupContainer(ipcConfig.Value)
if err != nil {
return fmt.Errorf("container %q not found: %w", ipcConfig.Value, err)
}
secopts, err := label.DupSecOpt(ctr.ProcessLabel())
if err != nil {
return fmt.Errorf("failed to duplicate label %q : %w", ctr.ProcessLabel(), err)
}
labelOpts = append(labelOpts, secopts...)
}
s.SelinuxOpts = append(s.SelinuxOpts, labelOpts...)
return nil
}
func setupApparmor(s *specgen.SpecGenerator, rtc *config.Config, g *generate.Generator) error {
hasProfile := len(s.ApparmorProfile) > 0
if !apparmor.IsEnabled() {
if hasProfile && s.ApparmorProfile != "unconfined" {
return fmt.Errorf("apparmor profile %q specified, but Apparmor is not enabled on this system", s.ApparmorProfile)
}
return nil
}
// If privileged and caller did not specify apparmor profiles return
if s.IsPrivileged() && !hasProfile {
return nil
}
if !hasProfile {
s.ApparmorProfile = rtc.Containers.ApparmorProfile
}
if len(s.ApparmorProfile) > 0 {
g.SetProcessApparmorProfile(s.ApparmorProfile)
}
return nil
}
func securityConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, newImage *libimage.Image, rtc *config.Config) error {
var (
caplist []string
err error
)
// HANDLE CAPABILITIES
// NOTE: Must happen before SECCOMP
if s.IsPrivileged() {
g.SetupPrivileged(true)
caplist, err = capabilities.BoundingSet()
if err != nil {
return err
}
} else {
mergedCaps, err := capabilities.MergeCapabilities(rtc.Containers.DefaultCapabilities.Get(), s.CapAdd, s.CapDrop)
if err != nil {
return err
}
boundingSet, err := capabilities.BoundingSet()
if err != nil {
return err
}
boundingCaps := make(map[string]interface{})
for _, b := range boundingSet {
boundingCaps[b] = b
}
for _, c := range mergedCaps {
if _, ok := boundingCaps[c]; ok {
caplist = append(caplist, c)
}
}
privCapsRequired := []string{}
// If the container image specifies a label with a
// capabilities.ContainerImageLabel then split the comma separated list
// of capabilities and record them. This list indicates the only
// capabilities, required to run the container.
var capsRequiredRequested []string
for key, val := range s.Labels {
if slices.Contains(capabilities.ContainerImageLabels, key) {
capsRequiredRequested = strings.Split(val, ",")
}
}
if !s.IsPrivileged() && len(capsRequiredRequested) == 1 && capsRequiredRequested[0] == "" {
caplist = []string{}
} else if !s.IsPrivileged() && len(capsRequiredRequested) > 0 {
// Pass capRequiredRequested in CapAdd field to normalize capabilities names
capsRequired, err := capabilities.MergeCapabilities(nil, capsRequiredRequested, nil)
if err != nil {
return fmt.Errorf("capabilities requested by user or image are not valid: %q: %w", strings.Join(capsRequired, ","), err)
}
// Verify all capRequired are in the capList
for _, cap := range capsRequired {
if !slices.Contains(caplist, cap) {
privCapsRequired = append(privCapsRequired, cap)
}
}
if len(privCapsRequired) == 0 {
caplist = capsRequired
} else {
logrus.Errorf("Capabilities requested by user or image are not allowed by default: %q", strings.Join(privCapsRequired, ","))
}
}
}
configSpec := g.Config
configSpec.Process.Capabilities.Ambient = []string{}
// Always unset the inheritable capabilities similarly to what the Linux kernel does
// They are used only when using capabilities with uid != 0.
configSpec.Process.Capabilities.Inheritable = []string{}
configSpec.Process.Capabilities.Bounding = caplist
user := strings.Split(s.User, ":")[0]
if (user == "" && s.UserNS.NSMode != specgen.KeepID) || user == "root" || user == "0" {
configSpec.Process.Capabilities.Effective = caplist
configSpec.Process.Capabilities.Permitted = caplist
} else {
mergedCaps, err := capabilities.MergeCapabilities(nil, s.CapAdd, nil)
if err != nil {
return fmt.Errorf("capabilities requested by user are not valid: %q: %w", strings.Join(s.CapAdd, ","), err)
}
boundingSet, err := capabilities.BoundingSet()
if err != nil {
return err
}
boundingCaps := make(map[string]interface{})
for _, b := range boundingSet {
boundingCaps[b] = b
}
var userCaps []string
for _, c := range mergedCaps {
if _, ok := boundingCaps[c]; ok {
userCaps = append(userCaps, c)
}
}
configSpec.Process.Capabilities.Effective = userCaps
configSpec.Process.Capabilities.Permitted = userCaps
// Ambient capabilities were added to Linux 4.3. Set ambient
// capabilities only when the kernel supports them.
if supportAmbientCapabilities() {
configSpec.Process.Capabilities.Ambient = userCaps
configSpec.Process.Capabilities.Inheritable = userCaps
}
}
if s.NoNewPrivileges != nil {
g.SetProcessNoNewPrivileges(*s.NoNewPrivileges)
}
if err := setupApparmor(s, rtc, g); err != nil {
return err
}
// HANDLE SECCOMP
if s.SeccompProfilePath != "unconfined" {
seccompConfig, err := getSeccompConfig(s, configSpec, newImage)
if err != nil {
return err
}
configSpec.Linux.Seccomp = seccompConfig
}
// Clear default Seccomp profile from Generator for unconfined containers
// and privileged containers which do not specify a seccomp profile.
if s.SeccompProfilePath == "unconfined" || (s.IsPrivileged() && (s.SeccompProfilePath == "" || s.SeccompProfilePath == config.SeccompOverridePath || s.SeccompProfilePath == config.SeccompDefaultPath)) {
configSpec.Linux.Seccomp = nil
}
if s.ReadOnlyFilesystem != nil {
g.SetRootReadonly(*s.ReadOnlyFilesystem)
}
noUseIPC := s.IpcNS.NSMode == specgen.FromContainer || s.IpcNS.NSMode == specgen.FromPod || s.IpcNS.NSMode == specgen.Host
noUseNet := s.NetNS.NSMode == specgen.FromContainer || s.NetNS.NSMode == specgen.FromPod || s.NetNS.NSMode == specgen.Host
noUseUTS := s.UtsNS.NSMode == specgen.FromContainer || s.UtsNS.NSMode == specgen.FromPod || s.UtsNS.NSMode == specgen.Host
// Add default sysctls
defaultSysctls, err := util.ValidateSysctls(rtc.Sysctls())
if err != nil {
return err
}
for sysctlKey, sysctlVal := range defaultSysctls {
// Ignore mqueue sysctls if --ipc=host
if noUseIPC && strings.HasPrefix(sysctlKey, "fs.mqueue.") {
logrus.Infof("Sysctl %s=%s ignored in containers.conf, since IPC Namespace set to %q", sysctlKey, sysctlVal, s.IpcNS.NSMode)
continue
}
// Ignore net sysctls if --net=host
if noUseNet && strings.HasPrefix(sysctlKey, "net.") {
logrus.Infof("Sysctl %s=%s ignored in containers.conf, since Network Namespace set to host", sysctlKey, sysctlVal)
continue
}
// Ignore uts sysctls if --uts=host
if noUseUTS && (strings.HasPrefix(sysctlKey, "kernel.domainname") || strings.HasPrefix(sysctlKey, "kernel.hostname")) {
logrus.Infof("Sysctl %s=%s ignored in containers.conf, since UTS Namespace set to host", sysctlKey, sysctlVal)
continue
}
g.AddLinuxSysctl(sysctlKey, sysctlVal)
}
for sysctlKey, sysctlVal := range s.Sysctl {
if s.IpcNS.IsHost() && strings.HasPrefix(sysctlKey, "fs.mqueue.") {
return fmt.Errorf("sysctl %s=%s can't be set since IPC Namespace set to host: %w", sysctlKey, sysctlVal, define.ErrInvalidArg)
}
// Ignore net sysctls if --net=host
if s.NetNS.IsHost() && strings.HasPrefix(sysctlKey, "net.") {
return fmt.Errorf("sysctl %s=%s can't be set since Network Namespace set to host: %w", sysctlKey, sysctlVal, define.ErrInvalidArg)
}
// Ignore uts sysctls if --uts=host
if s.UtsNS.IsHost() && (strings.HasPrefix(sysctlKey, "kernel.domainname") || strings.HasPrefix(sysctlKey, "kernel.hostname")) {
return fmt.Errorf("sysctl %s=%s can't be set since UTS Namespace set to host: %w", sysctlKey, sysctlVal, define.ErrInvalidArg)
}
g.AddLinuxSysctl(sysctlKey, sysctlVal)
}
return nil
}