container: split security options to a SecurityOptions struct

- Split these options to a separate struct, so that we can handle them in isolation.
- Change some tests to use subtests, and improve coverage

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2023-04-14 09:27:20 +02:00
parent e22758bfb2
commit 3eebf4d162
No known key found for this signature in database
GPG key ID: 76698F39D527CE8C
8 changed files with 118 additions and 91 deletions

View file

@ -79,9 +79,7 @@ type Container struct {
Name string Name string
Driver string Driver string
OS string OS string
// MountLabel contains the options for the 'mount' command
MountLabel string
ProcessLabel string
RestartCount int RestartCount int
HasBeenStartedBefore bool HasBeenStartedBefore bool
HasBeenManuallyStopped bool // used for unless-stopped restart policy HasBeenManuallyStopped bool // used for unless-stopped restart policy
@ -99,13 +97,11 @@ type Container struct {
attachContext *attachContext attachContext *attachContext
// Fields here are specific to Unix platforms // Fields here are specific to Unix platforms
AppArmorProfile string SecurityOptions
HostnamePath string HostnamePath string
HostsPath string HostsPath string
ShmPath string ShmPath string
ResolvConfPath string ResolvConfPath string
SeccompProfile string
NoNewPrivileges bool
// Fields here are specific to Windows // Fields here are specific to Windows
NetworkSharedContainerID string `json:"-"` NetworkSharedContainerID string `json:"-"`
@ -113,6 +109,15 @@ type Container struct {
LocalLogCacheMeta localLogCacheMeta `json:",omitempty"` LocalLogCacheMeta localLogCacheMeta `json:",omitempty"`
} }
type SecurityOptions struct {
// MountLabel contains the options for the "mount" command.
MountLabel string
ProcessLabel string
AppArmorProfile string
SeccompProfile string
NoNewPrivileges bool
}
type localLogCacheMeta struct { type localLogCacheMeta struct {
HaveNotifyEnabled bool HaveNotifyEnabled bool
} }

View file

@ -209,7 +209,7 @@ func (daemon *Daemon) generateHostname(id string, config *containertypes.Config)
func (daemon *Daemon) setSecurityOptions(container *container.Container, hostConfig *containertypes.HostConfig) error { func (daemon *Daemon) setSecurityOptions(container *container.Container, hostConfig *containertypes.HostConfig) error {
container.Lock() container.Lock()
defer container.Unlock() defer container.Unlock()
return daemon.parseSecurityOpt(container, hostConfig) return daemon.parseSecurityOpt(&container.SecurityOptions, hostConfig)
} }
func (daemon *Daemon) setHostConfig(container *container.Container, hostConfig *containertypes.HostConfig) error { func (daemon *Daemon) setHostConfig(container *container.Container, hostConfig *containertypes.HostConfig) error {

View file

@ -15,7 +15,7 @@ func (daemon *Daemon) saveAppArmorConfig(container *container.Container) error {
return nil // if apparmor is disabled there is nothing to do here. return nil // if apparmor is disabled there is nothing to do here.
} }
if err := parseSecurityOpt(container, container.HostConfig); err != nil { if err := parseSecurityOpt(&container.SecurityOptions, container.HostConfig); err != nil {
return errdefs.InvalidParameter(err) return errdefs.InvalidParameter(err)
} }

View file

@ -190,12 +190,12 @@ func getBlkioWeightDevices(config containertypes.Resources) ([]specs.LinuxWeight
return blkioWeightDevices, nil return blkioWeightDevices, nil
} }
func (daemon *Daemon) parseSecurityOpt(container *container.Container, hostConfig *containertypes.HostConfig) error { func (daemon *Daemon) parseSecurityOpt(securityOptions *container.SecurityOptions, hostConfig *containertypes.HostConfig) error {
container.NoNewPrivileges = daemon.configStore.NoNewPrivileges securityOptions.NoNewPrivileges = daemon.configStore.NoNewPrivileges
return parseSecurityOpt(container, hostConfig) return parseSecurityOpt(securityOptions, hostConfig)
} }
func parseSecurityOpt(container *container.Container, config *containertypes.HostConfig) error { func parseSecurityOpt(securityOptions *container.SecurityOptions, config *containertypes.HostConfig) error {
var ( var (
labelOpts []string labelOpts []string
err error err error
@ -203,7 +203,7 @@ func parseSecurityOpt(container *container.Container, config *containertypes.Hos
for _, opt := range config.SecurityOpt { for _, opt := range config.SecurityOpt {
if opt == "no-new-privileges" { if opt == "no-new-privileges" {
container.NoNewPrivileges = true securityOptions.NoNewPrivileges = true
continue continue
} }
if opt == "disable" { if opt == "disable" {
@ -227,21 +227,21 @@ func parseSecurityOpt(container *container.Container, config *containertypes.Hos
case "label": case "label":
labelOpts = append(labelOpts, v) labelOpts = append(labelOpts, v)
case "apparmor": case "apparmor":
container.AppArmorProfile = v securityOptions.AppArmorProfile = v
case "seccomp": case "seccomp":
container.SeccompProfile = v securityOptions.SeccompProfile = v
case "no-new-privileges": case "no-new-privileges":
noNewPrivileges, err := strconv.ParseBool(v) noNewPrivileges, err := strconv.ParseBool(v)
if err != nil { if err != nil {
return fmt.Errorf("invalid --security-opt 2: %q", opt) return fmt.Errorf("invalid --security-opt 2: %q", opt)
} }
container.NoNewPrivileges = noNewPrivileges securityOptions.NoNewPrivileges = noNewPrivileges
default: default:
return fmt.Errorf("invalid --security-opt 2: %q", opt) return fmt.Errorf("invalid --security-opt 2: %q", opt)
} }
} }
container.ProcessLabel, container.MountLabel, err = label.InitLabels(labelOpts) securityOptions.ProcessLabel, securityOptions.MountLabel, err = label.InitLabels(labelOpts)
return err return err
} }

View file

@ -14,6 +14,7 @@ import (
"github.com/docker/docker/container" "github.com/docker/docker/container"
"github.com/docker/docker/daemon/config" "github.com/docker/docker/daemon/config"
"github.com/docker/docker/pkg/sysinfo" "github.com/docker/docker/pkg/sysinfo"
"github.com/opencontainers/selinux/go-selinux"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -138,115 +139,136 @@ func TestAdjustCPUSharesNoAdjustment(t *testing.T) {
// Unix test as uses settings which are not available on Windows // Unix test as uses settings which are not available on Windows
func TestParseSecurityOptWithDeprecatedColon(t *testing.T) { func TestParseSecurityOptWithDeprecatedColon(t *testing.T) {
ctr := &container.Container{} opts := &container.SecurityOptions{}
cfg := &containertypes.HostConfig{} cfg := &containertypes.HostConfig{}
// test apparmor // test apparmor
cfg.SecurityOpt = []string{"apparmor=test_profile"} cfg.SecurityOpt = []string{"apparmor=test_profile"}
if err := parseSecurityOpt(ctr, cfg); err != nil { if err := parseSecurityOpt(opts, cfg); err != nil {
t.Fatalf("Unexpected parseSecurityOpt error: %v", err) t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
} }
if ctr.AppArmorProfile != "test_profile" { if opts.AppArmorProfile != "test_profile" {
t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", ctr.AppArmorProfile) t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", opts.AppArmorProfile)
} }
// test seccomp // test seccomp
sp := "/path/to/seccomp_test.json" sp := "/path/to/seccomp_test.json"
cfg.SecurityOpt = []string{"seccomp=" + sp} cfg.SecurityOpt = []string{"seccomp=" + sp}
if err := parseSecurityOpt(ctr, cfg); err != nil { if err := parseSecurityOpt(opts, cfg); err != nil {
t.Fatalf("Unexpected parseSecurityOpt error: %v", err) t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
} }
if ctr.SeccompProfile != sp { if opts.SeccompProfile != sp {
t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, ctr.SeccompProfile) t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, opts.SeccompProfile)
} }
// test valid label // test valid label
cfg.SecurityOpt = []string{"label=user:USER"} cfg.SecurityOpt = []string{"label=user:USER"}
if err := parseSecurityOpt(ctr, cfg); err != nil { if err := parseSecurityOpt(opts, cfg); err != nil {
t.Fatalf("Unexpected parseSecurityOpt error: %v", err) t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
} }
// test invalid label // test invalid label
cfg.SecurityOpt = []string{"label"} cfg.SecurityOpt = []string{"label"}
if err := parseSecurityOpt(ctr, cfg); err == nil { if err := parseSecurityOpt(opts, cfg); err == nil {
t.Fatal("Expected parseSecurityOpt error, got nil") t.Fatal("Expected parseSecurityOpt error, got nil")
} }
// test invalid opt // test invalid opt
cfg.SecurityOpt = []string{"test"} cfg.SecurityOpt = []string{"test"}
if err := parseSecurityOpt(ctr, cfg); err == nil { if err := parseSecurityOpt(opts, cfg); err == nil {
t.Fatal("Expected parseSecurityOpt error, got nil") t.Fatal("Expected parseSecurityOpt error, got nil")
} }
} }
func TestParseSecurityOpt(t *testing.T) { func TestParseSecurityOpt(t *testing.T) {
ctr := &container.Container{} t.Run("apparmor", func(t *testing.T) {
cfg := &containertypes.HostConfig{} secOpts := &container.SecurityOptions{}
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
// test apparmor SecurityOpt: []string{"apparmor=test_profile"},
cfg.SecurityOpt = []string{"apparmor=test_profile"} })
if err := parseSecurityOpt(ctr, cfg); err != nil { assert.Check(t, err)
t.Fatalf("Unexpected parseSecurityOpt error: %v", err) assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
} })
if ctr.AppArmorProfile != "test_profile" { t.Run("apparmor using legacy separator", func(t *testing.T) {
t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", ctr.AppArmorProfile) secOpts := &container.SecurityOptions{}
} err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
SecurityOpt: []string{"apparmor:test_profile"},
// test seccomp })
sp := "/path/to/seccomp_test.json" assert.Check(t, err)
cfg.SecurityOpt = []string{"seccomp=" + sp} assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
if err := parseSecurityOpt(ctr, cfg); err != nil { })
t.Fatalf("Unexpected parseSecurityOpt error: %v", err) t.Run("seccomp", func(t *testing.T) {
} secOpts := &container.SecurityOptions{}
if ctr.SeccompProfile != sp { err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
t.Fatalf("Unexpected SeccompProfile, expected: %q, got %q", sp, ctr.SeccompProfile) SecurityOpt: []string{"seccomp=/path/to/seccomp_test.json"},
} })
assert.Check(t, err)
// test valid label assert.Equal(t, secOpts.SeccompProfile, "/path/to/seccomp_test.json")
cfg.SecurityOpt = []string{"label=user:USER"} })
if err := parseSecurityOpt(ctr, cfg); err != nil { t.Run("valid label", func(t *testing.T) {
t.Fatalf("Unexpected parseSecurityOpt error: %v", err) secOpts := &container.SecurityOptions{}
} err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
SecurityOpt: []string{"label=user:USER"},
// test invalid label })
cfg.SecurityOpt = []string{"label"} assert.Check(t, err)
if err := parseSecurityOpt(ctr, cfg); err == nil { if selinux.GetEnabled() {
t.Fatal("Expected parseSecurityOpt error, got nil") // TODO(thaJeztah): set expected labels here (or "partial" if depends on host)
} // assert.Check(t, is.Equal(secOpts.MountLabel, ""))
// assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
// test invalid opt } else {
cfg.SecurityOpt = []string{"test"} assert.Check(t, is.Equal(secOpts.MountLabel, ""))
if err := parseSecurityOpt(ctr, cfg); err == nil { assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
t.Fatal("Expected parseSecurityOpt error, got nil") }
} })
t.Run("invalid label", func(t *testing.T) {
secOpts := &container.SecurityOptions{}
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
SecurityOpt: []string{"label"},
})
assert.Error(t, err, `invalid --security-opt 1: "label"`)
})
t.Run("invalid option (no value)", func(t *testing.T) {
secOpts := &container.SecurityOptions{}
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
SecurityOpt: []string{"unknown"},
})
assert.Error(t, err, `invalid --security-opt 1: "unknown"`)
})
t.Run("unknown option", func(t *testing.T) {
secOpts := &container.SecurityOptions{}
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
SecurityOpt: []string{"unknown=something"},
})
assert.Error(t, err, `invalid --security-opt 2: "unknown=something"`)
})
} }
func TestParseNNPSecurityOptions(t *testing.T) { func TestParseNNPSecurityOptions(t *testing.T) {
daemon := &Daemon{ daemon := &Daemon{
configStore: &config.Config{NoNewPrivileges: true}, configStore: &config.Config{NoNewPrivileges: true},
} }
ctr := &container.Container{} opts := &container.SecurityOptions{}
cfg := &containertypes.HostConfig{} cfg := &containertypes.HostConfig{}
// test NNP when "daemon:true" and "no-new-privileges=false"" // test NNP when "daemon:true" and "no-new-privileges=false""
cfg.SecurityOpt = []string{"no-new-privileges=false"} cfg.SecurityOpt = []string{"no-new-privileges=false"}
if err := daemon.parseSecurityOpt(ctr, cfg); err != nil { if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err) t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
} }
if ctr.NoNewPrivileges { if opts.NoNewPrivileges {
t.Fatalf("container.NoNewPrivileges should be FALSE: %v", ctr.NoNewPrivileges) t.Fatalf("container.NoNewPrivileges should be FALSE: %v", opts.NoNewPrivileges)
} }
// test NNP when "daemon:false" and "no-new-privileges=true"" // test NNP when "daemon:false" and "no-new-privileges=true""
daemon.configStore.NoNewPrivileges = false daemon.configStore.NoNewPrivileges = false
cfg.SecurityOpt = []string{"no-new-privileges=true"} cfg.SecurityOpt = []string{"no-new-privileges=true"}
if err := daemon.parseSecurityOpt(ctr, cfg); err != nil { if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err) t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
} }
if !ctr.NoNewPrivileges { if !opts.NoNewPrivileges {
t.Fatalf("container.NoNewPrivileges should be TRUE: %v", ctr.NoNewPrivileges) t.Fatalf("container.NoNewPrivileges should be TRUE: %v", opts.NoNewPrivileges)
} }
} }

View file

@ -55,7 +55,7 @@ func getPluginExecRoot(cfg *config.Config) string {
return filepath.Join(cfg.Root, "plugins") return filepath.Join(cfg.Root, "plugins")
} }
func (daemon *Daemon) parseSecurityOpt(container *container.Container, hostConfig *containertypes.HostConfig) error { func (daemon *Daemon) parseSecurityOpt(securityOptions *container.SecurityOptions, hostConfig *containertypes.HostConfig) error {
return nil return nil
} }

View file

@ -74,7 +74,7 @@ func TestExecSetPlatformOptAppArmor(t *testing.T) {
} }
t.Run(doc, func(t *testing.T) { t.Run(doc, func(t *testing.T) {
c := &container.Container{ c := &container.Container{
AppArmorProfile: tc.appArmorProfile, SecurityOptions: container.SecurityOptions{AppArmorProfile: tc.appArmorProfile},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: tc.privileged, Privileged: tc.privileged,
}, },

View file

@ -31,7 +31,7 @@ func TestWithSeccomp(t *testing.T) {
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: dconfig.SeccompProfileUnconfined, SecurityOptions: container.SecurityOptions{SeccompProfile: dconfig.SeccompProfileUnconfined},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: false, Privileged: false,
}, },
@ -45,7 +45,7 @@ func TestWithSeccomp(t *testing.T) {
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_LOG\" }", SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_LOG"}`},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: true, Privileged: true,
}, },
@ -59,7 +59,7 @@ func TestWithSeccomp(t *testing.T) {
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "", SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: true, Privileged: true,
}, },
@ -71,10 +71,10 @@ func TestWithSeccomp(t *testing.T) {
comment: "privileged container w/ daemon profile runs unconfined", comment: "privileged container w/ daemon profile runs unconfined",
daemon: &Daemon{ daemon: &Daemon{
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
seccompProfile: []byte("{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }"), seccompProfile: []byte(`{"defaultAction": "SCMP_ACT_ERRNO"}`),
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "", SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: true, Privileged: true,
}, },
@ -88,7 +88,7 @@ func TestWithSeccomp(t *testing.T) {
sysInfo: &sysinfo.SysInfo{Seccomp: false}, sysInfo: &sysinfo.SysInfo{Seccomp: false},
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }", SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_ERRNO"}`},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: false, Privileged: false,
}, },
@ -103,7 +103,7 @@ func TestWithSeccomp(t *testing.T) {
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "", SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: false, Privileged: false,
}, },
@ -122,7 +122,7 @@ func TestWithSeccomp(t *testing.T) {
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }", SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_ERRNO"}`},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: false, Privileged: false,
}, },
@ -141,10 +141,10 @@ func TestWithSeccomp(t *testing.T) {
comment: "load daemon's profile", comment: "load daemon's profile",
daemon: &Daemon{ daemon: &Daemon{
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
seccompProfile: []byte("{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }"), seccompProfile: []byte(`{"defaultAction": "SCMP_ACT_ERRNO"}`),
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "", SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: false, Privileged: false,
}, },
@ -163,10 +163,10 @@ func TestWithSeccomp(t *testing.T) {
comment: "load prioritise container profile over daemon's", comment: "load prioritise container profile over daemon's",
daemon: &Daemon{ daemon: &Daemon{
sysInfo: &sysinfo.SysInfo{Seccomp: true}, sysInfo: &sysinfo.SysInfo{Seccomp: true},
seccompProfile: []byte("{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }"), seccompProfile: []byte(`{"defaultAction": "SCMP_ACT_ERRNO"}`),
}, },
c: &container.Container{ c: &container.Container{
SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_LOG\" }", SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_LOG"}`},
HostConfig: &containertypes.HostConfig{ HostConfig: &containertypes.HostConfig{
Privileged: false, Privileged: false,
}, },