Browse code

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>

Sebastiaan van Stijn authored on 2023/04/14 16:27:20
Showing 8 changed files
... ...
@@ -79,9 +79,7 @@ type Container struct {
79 79
 	Name            string
80 80
 	Driver          string
81 81
 	OS              string
82
-	// MountLabel contains the options for the 'mount' command
83
-	MountLabel               string
84
-	ProcessLabel             string
82
+
85 83
 	RestartCount             int
86 84
 	HasBeenStartedBefore     bool
87 85
 	HasBeenManuallyStopped   bool // used for unless-stopped restart policy
... ...
@@ -99,13 +97,11 @@ type Container struct {
99 99
 	attachContext  *attachContext
100 100
 
101 101
 	// Fields here are specific to Unix platforms
102
-	AppArmorProfile string
103
-	HostnamePath    string
104
-	HostsPath       string
105
-	ShmPath         string
106
-	ResolvConfPath  string
107
-	SeccompProfile  string
108
-	NoNewPrivileges bool
102
+	SecurityOptions
103
+	HostnamePath   string
104
+	HostsPath      string
105
+	ShmPath        string
106
+	ResolvConfPath string
109 107
 
110 108
 	// Fields here are specific to Windows
111 109
 	NetworkSharedContainerID string            `json:"-"`
... ...
@@ -113,6 +109,15 @@ type Container struct {
113 113
 	LocalLogCacheMeta        localLogCacheMeta `json:",omitempty"`
114 114
 }
115 115
 
116
+type SecurityOptions struct {
117
+	// MountLabel contains the options for the "mount" command.
118
+	MountLabel      string
119
+	ProcessLabel    string
120
+	AppArmorProfile string
121
+	SeccompProfile  string
122
+	NoNewPrivileges bool
123
+}
124
+
116 125
 type localLogCacheMeta struct {
117 126
 	HaveNotifyEnabled bool
118 127
 }
... ...
@@ -209,7 +209,7 @@ func (daemon *Daemon) generateHostname(id string, config *containertypes.Config)
209 209
 func (daemon *Daemon) setSecurityOptions(container *container.Container, hostConfig *containertypes.HostConfig) error {
210 210
 	container.Lock()
211 211
 	defer container.Unlock()
212
-	return daemon.parseSecurityOpt(container, hostConfig)
212
+	return daemon.parseSecurityOpt(&container.SecurityOptions, hostConfig)
213 213
 }
214 214
 
215 215
 func (daemon *Daemon) setHostConfig(container *container.Container, hostConfig *containertypes.HostConfig) error {
... ...
@@ -15,7 +15,7 @@ func (daemon *Daemon) saveAppArmorConfig(container *container.Container) error {
15 15
 		return nil // if apparmor is disabled there is nothing to do here.
16 16
 	}
17 17
 
18
-	if err := parseSecurityOpt(container, container.HostConfig); err != nil {
18
+	if err := parseSecurityOpt(&container.SecurityOptions, container.HostConfig); err != nil {
19 19
 		return errdefs.InvalidParameter(err)
20 20
 	}
21 21
 
... ...
@@ -190,12 +190,12 @@ func getBlkioWeightDevices(config containertypes.Resources) ([]specs.LinuxWeight
190 190
 	return blkioWeightDevices, nil
191 191
 }
192 192
 
193
-func (daemon *Daemon) parseSecurityOpt(container *container.Container, hostConfig *containertypes.HostConfig) error {
194
-	container.NoNewPrivileges = daemon.configStore.NoNewPrivileges
195
-	return parseSecurityOpt(container, hostConfig)
193
+func (daemon *Daemon) parseSecurityOpt(securityOptions *container.SecurityOptions, hostConfig *containertypes.HostConfig) error {
194
+	securityOptions.NoNewPrivileges = daemon.configStore.NoNewPrivileges
195
+	return parseSecurityOpt(securityOptions, hostConfig)
196 196
 }
197 197
 
198
-func parseSecurityOpt(container *container.Container, config *containertypes.HostConfig) error {
198
+func parseSecurityOpt(securityOptions *container.SecurityOptions, config *containertypes.HostConfig) error {
199 199
 	var (
200 200
 		labelOpts []string
201 201
 		err       error
... ...
@@ -203,7 +203,7 @@ func parseSecurityOpt(container *container.Container, config *containertypes.Hos
203 203
 
204 204
 	for _, opt := range config.SecurityOpt {
205 205
 		if opt == "no-new-privileges" {
206
-			container.NoNewPrivileges = true
206
+			securityOptions.NoNewPrivileges = true
207 207
 			continue
208 208
 		}
209 209
 		if opt == "disable" {
... ...
@@ -227,21 +227,21 @@ func parseSecurityOpt(container *container.Container, config *containertypes.Hos
227 227
 		case "label":
228 228
 			labelOpts = append(labelOpts, v)
229 229
 		case "apparmor":
230
-			container.AppArmorProfile = v
230
+			securityOptions.AppArmorProfile = v
231 231
 		case "seccomp":
232
-			container.SeccompProfile = v
232
+			securityOptions.SeccompProfile = v
233 233
 		case "no-new-privileges":
234 234
 			noNewPrivileges, err := strconv.ParseBool(v)
235 235
 			if err != nil {
236 236
 				return fmt.Errorf("invalid --security-opt 2: %q", opt)
237 237
 			}
238
-			container.NoNewPrivileges = noNewPrivileges
238
+			securityOptions.NoNewPrivileges = noNewPrivileges
239 239
 		default:
240 240
 			return fmt.Errorf("invalid --security-opt 2: %q", opt)
241 241
 		}
242 242
 	}
243 243
 
244
-	container.ProcessLabel, container.MountLabel, err = label.InitLabels(labelOpts)
244
+	securityOptions.ProcessLabel, securityOptions.MountLabel, err = label.InitLabels(labelOpts)
245 245
 	return err
246 246
 }
247 247
 
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/docker/docker/container"
15 15
 	"github.com/docker/docker/daemon/config"
16 16
 	"github.com/docker/docker/pkg/sysinfo"
17
+	"github.com/opencontainers/selinux/go-selinux"
17 18
 	"golang.org/x/sys/unix"
18 19
 	"gotest.tools/v3/assert"
19 20
 	is "gotest.tools/v3/assert/cmp"
... ...
@@ -138,115 +139,136 @@ func TestAdjustCPUSharesNoAdjustment(t *testing.T) {
138 138
 
139 139
 // Unix test as uses settings which are not available on Windows
140 140
 func TestParseSecurityOptWithDeprecatedColon(t *testing.T) {
141
-	ctr := &container.Container{}
141
+	opts := &container.SecurityOptions{}
142 142
 	cfg := &containertypes.HostConfig{}
143 143
 
144 144
 	// test apparmor
145 145
 	cfg.SecurityOpt = []string{"apparmor=test_profile"}
146
-	if err := parseSecurityOpt(ctr, cfg); err != nil {
146
+	if err := parseSecurityOpt(opts, cfg); err != nil {
147 147
 		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
148 148
 	}
149
-	if ctr.AppArmorProfile != "test_profile" {
150
-		t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", ctr.AppArmorProfile)
149
+	if opts.AppArmorProfile != "test_profile" {
150
+		t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", opts.AppArmorProfile)
151 151
 	}
152 152
 
153 153
 	// test seccomp
154 154
 	sp := "/path/to/seccomp_test.json"
155 155
 	cfg.SecurityOpt = []string{"seccomp=" + sp}
156
-	if err := parseSecurityOpt(ctr, cfg); err != nil {
156
+	if err := parseSecurityOpt(opts, cfg); err != nil {
157 157
 		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
158 158
 	}
159
-	if ctr.SeccompProfile != sp {
160
-		t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, ctr.SeccompProfile)
159
+	if opts.SeccompProfile != sp {
160
+		t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, opts.SeccompProfile)
161 161
 	}
162 162
 
163 163
 	// test valid label
164 164
 	cfg.SecurityOpt = []string{"label=user:USER"}
165
-	if err := parseSecurityOpt(ctr, cfg); err != nil {
165
+	if err := parseSecurityOpt(opts, cfg); err != nil {
166 166
 		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
167 167
 	}
168 168
 
169 169
 	// test invalid label
170 170
 	cfg.SecurityOpt = []string{"label"}
171
-	if err := parseSecurityOpt(ctr, cfg); err == nil {
171
+	if err := parseSecurityOpt(opts, cfg); err == nil {
172 172
 		t.Fatal("Expected parseSecurityOpt error, got nil")
173 173
 	}
174 174
 
175 175
 	// test invalid opt
176 176
 	cfg.SecurityOpt = []string{"test"}
177
-	if err := parseSecurityOpt(ctr, cfg); err == nil {
177
+	if err := parseSecurityOpt(opts, cfg); err == nil {
178 178
 		t.Fatal("Expected parseSecurityOpt error, got nil")
179 179
 	}
180 180
 }
181 181
 
182 182
 func TestParseSecurityOpt(t *testing.T) {
183
-	ctr := &container.Container{}
184
-	cfg := &containertypes.HostConfig{}
185
-
186
-	// test apparmor
187
-	cfg.SecurityOpt = []string{"apparmor=test_profile"}
188
-	if err := parseSecurityOpt(ctr, cfg); err != nil {
189
-		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
190
-	}
191
-	if ctr.AppArmorProfile != "test_profile" {
192
-		t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", ctr.AppArmorProfile)
193
-	}
194
-
195
-	// test seccomp
196
-	sp := "/path/to/seccomp_test.json"
197
-	cfg.SecurityOpt = []string{"seccomp=" + sp}
198
-	if err := parseSecurityOpt(ctr, cfg); err != nil {
199
-		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
200
-	}
201
-	if ctr.SeccompProfile != sp {
202
-		t.Fatalf("Unexpected SeccompProfile, expected: %q, got %q", sp, ctr.SeccompProfile)
203
-	}
204
-
205
-	// test valid label
206
-	cfg.SecurityOpt = []string{"label=user:USER"}
207
-	if err := parseSecurityOpt(ctr, cfg); err != nil {
208
-		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
209
-	}
210
-
211
-	// test invalid label
212
-	cfg.SecurityOpt = []string{"label"}
213
-	if err := parseSecurityOpt(ctr, cfg); err == nil {
214
-		t.Fatal("Expected parseSecurityOpt error, got nil")
215
-	}
216
-
217
-	// test invalid opt
218
-	cfg.SecurityOpt = []string{"test"}
219
-	if err := parseSecurityOpt(ctr, cfg); err == nil {
220
-		t.Fatal("Expected parseSecurityOpt error, got nil")
221
-	}
183
+	t.Run("apparmor", func(t *testing.T) {
184
+		secOpts := &container.SecurityOptions{}
185
+		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
186
+			SecurityOpt: []string{"apparmor=test_profile"},
187
+		})
188
+		assert.Check(t, err)
189
+		assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
190
+	})
191
+	t.Run("apparmor using legacy separator", func(t *testing.T) {
192
+		secOpts := &container.SecurityOptions{}
193
+		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
194
+			SecurityOpt: []string{"apparmor:test_profile"},
195
+		})
196
+		assert.Check(t, err)
197
+		assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
198
+	})
199
+	t.Run("seccomp", func(t *testing.T) {
200
+		secOpts := &container.SecurityOptions{}
201
+		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
202
+			SecurityOpt: []string{"seccomp=/path/to/seccomp_test.json"},
203
+		})
204
+		assert.Check(t, err)
205
+		assert.Equal(t, secOpts.SeccompProfile, "/path/to/seccomp_test.json")
206
+	})
207
+	t.Run("valid label", func(t *testing.T) {
208
+		secOpts := &container.SecurityOptions{}
209
+		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
210
+			SecurityOpt: []string{"label=user:USER"},
211
+		})
212
+		assert.Check(t, err)
213
+		if selinux.GetEnabled() {
214
+			// TODO(thaJeztah): set expected labels here (or "partial" if depends on host)
215
+			// assert.Check(t, is.Equal(secOpts.MountLabel, ""))
216
+			// assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
217
+		} else {
218
+			assert.Check(t, is.Equal(secOpts.MountLabel, ""))
219
+			assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
220
+		}
221
+	})
222
+	t.Run("invalid label", func(t *testing.T) {
223
+		secOpts := &container.SecurityOptions{}
224
+		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
225
+			SecurityOpt: []string{"label"},
226
+		})
227
+		assert.Error(t, err, `invalid --security-opt 1: "label"`)
228
+	})
229
+	t.Run("invalid option (no value)", func(t *testing.T) {
230
+		secOpts := &container.SecurityOptions{}
231
+		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
232
+			SecurityOpt: []string{"unknown"},
233
+		})
234
+		assert.Error(t, err, `invalid --security-opt 1: "unknown"`)
235
+	})
236
+	t.Run("unknown option", func(t *testing.T) {
237
+		secOpts := &container.SecurityOptions{}
238
+		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
239
+			SecurityOpt: []string{"unknown=something"},
240
+		})
241
+		assert.Error(t, err, `invalid --security-opt 2: "unknown=something"`)
242
+	})
222 243
 }
223 244
 
224 245
 func TestParseNNPSecurityOptions(t *testing.T) {
225 246
 	daemon := &Daemon{
226 247
 		configStore: &config.Config{NoNewPrivileges: true},
227 248
 	}
228
-	ctr := &container.Container{}
249
+	opts := &container.SecurityOptions{}
229 250
 	cfg := &containertypes.HostConfig{}
230 251
 
231 252
 	// test NNP when "daemon:true" and "no-new-privileges=false""
232 253
 	cfg.SecurityOpt = []string{"no-new-privileges=false"}
233 254
 
234
-	if err := daemon.parseSecurityOpt(ctr, cfg); err != nil {
255
+	if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
235 256
 		t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
236 257
 	}
237
-	if ctr.NoNewPrivileges {
238
-		t.Fatalf("container.NoNewPrivileges should be FALSE: %v", ctr.NoNewPrivileges)
258
+	if opts.NoNewPrivileges {
259
+		t.Fatalf("container.NoNewPrivileges should be FALSE: %v", opts.NoNewPrivileges)
239 260
 	}
240 261
 
241 262
 	// test NNP when "daemon:false" and "no-new-privileges=true""
242 263
 	daemon.configStore.NoNewPrivileges = false
243 264
 	cfg.SecurityOpt = []string{"no-new-privileges=true"}
244 265
 
245
-	if err := daemon.parseSecurityOpt(ctr, cfg); err != nil {
266
+	if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
246 267
 		t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
247 268
 	}
248
-	if !ctr.NoNewPrivileges {
249
-		t.Fatalf("container.NoNewPrivileges should be TRUE: %v", ctr.NoNewPrivileges)
269
+	if !opts.NoNewPrivileges {
270
+		t.Fatalf("container.NoNewPrivileges should be TRUE: %v", opts.NoNewPrivileges)
250 271
 	}
251 272
 }
252 273
 
... ...
@@ -55,7 +55,7 @@ func getPluginExecRoot(cfg *config.Config) string {
55 55
 	return filepath.Join(cfg.Root, "plugins")
56 56
 }
57 57
 
58
-func (daemon *Daemon) parseSecurityOpt(container *container.Container, hostConfig *containertypes.HostConfig) error {
58
+func (daemon *Daemon) parseSecurityOpt(securityOptions *container.SecurityOptions, hostConfig *containertypes.HostConfig) error {
59 59
 	return nil
60 60
 }
61 61
 
... ...
@@ -74,7 +74,7 @@ func TestExecSetPlatformOptAppArmor(t *testing.T) {
74 74
 			}
75 75
 			t.Run(doc, func(t *testing.T) {
76 76
 				c := &container.Container{
77
-					AppArmorProfile: tc.appArmorProfile,
77
+					SecurityOptions: container.SecurityOptions{AppArmorProfile: tc.appArmorProfile},
78 78
 					HostConfig: &containertypes.HostConfig{
79 79
 						Privileged: tc.privileged,
80 80
 					},
... ...
@@ -31,7 +31,7 @@ func TestWithSeccomp(t *testing.T) {
31 31
 				sysInfo: &sysinfo.SysInfo{Seccomp: true},
32 32
 			},
33 33
 			c: &container.Container{
34
-				SeccompProfile: dconfig.SeccompProfileUnconfined,
34
+				SecurityOptions: container.SecurityOptions{SeccompProfile: dconfig.SeccompProfileUnconfined},
35 35
 				HostConfig: &containertypes.HostConfig{
36 36
 					Privileged: false,
37 37
 				},
... ...
@@ -45,7 +45,7 @@ func TestWithSeccomp(t *testing.T) {
45 45
 				sysInfo: &sysinfo.SysInfo{Seccomp: true},
46 46
 			},
47 47
 			c: &container.Container{
48
-				SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_LOG\" }",
48
+				SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_LOG"}`},
49 49
 				HostConfig: &containertypes.HostConfig{
50 50
 					Privileged: true,
51 51
 				},
... ...
@@ -59,7 +59,7 @@ func TestWithSeccomp(t *testing.T) {
59 59
 				sysInfo: &sysinfo.SysInfo{Seccomp: true},
60 60
 			},
61 61
 			c: &container.Container{
62
-				SeccompProfile: "",
62
+				SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
63 63
 				HostConfig: &containertypes.HostConfig{
64 64
 					Privileged: true,
65 65
 				},
... ...
@@ -71,10 +71,10 @@ func TestWithSeccomp(t *testing.T) {
71 71
 			comment: "privileged container w/ daemon profile runs unconfined",
72 72
 			daemon: &Daemon{
73 73
 				sysInfo:        &sysinfo.SysInfo{Seccomp: true},
74
-				seccompProfile: []byte("{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }"),
74
+				seccompProfile: []byte(`{"defaultAction": "SCMP_ACT_ERRNO"}`),
75 75
 			},
76 76
 			c: &container.Container{
77
-				SeccompProfile: "",
77
+				SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
78 78
 				HostConfig: &containertypes.HostConfig{
79 79
 					Privileged: true,
80 80
 				},
... ...
@@ -88,7 +88,7 @@ func TestWithSeccomp(t *testing.T) {
88 88
 				sysInfo: &sysinfo.SysInfo{Seccomp: false},
89 89
 			},
90 90
 			c: &container.Container{
91
-				SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }",
91
+				SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_ERRNO"}`},
92 92
 				HostConfig: &containertypes.HostConfig{
93 93
 					Privileged: false,
94 94
 				},
... ...
@@ -103,7 +103,7 @@ func TestWithSeccomp(t *testing.T) {
103 103
 				sysInfo: &sysinfo.SysInfo{Seccomp: true},
104 104
 			},
105 105
 			c: &container.Container{
106
-				SeccompProfile: "",
106
+				SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
107 107
 				HostConfig: &containertypes.HostConfig{
108 108
 					Privileged: false,
109 109
 				},
... ...
@@ -122,7 +122,7 @@ func TestWithSeccomp(t *testing.T) {
122 122
 				sysInfo: &sysinfo.SysInfo{Seccomp: true},
123 123
 			},
124 124
 			c: &container.Container{
125
-				SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }",
125
+				SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_ERRNO"}`},
126 126
 				HostConfig: &containertypes.HostConfig{
127 127
 					Privileged: false,
128 128
 				},
... ...
@@ -141,10 +141,10 @@ func TestWithSeccomp(t *testing.T) {
141 141
 			comment: "load daemon's profile",
142 142
 			daemon: &Daemon{
143 143
 				sysInfo:        &sysinfo.SysInfo{Seccomp: true},
144
-				seccompProfile: []byte("{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }"),
144
+				seccompProfile: []byte(`{"defaultAction": "SCMP_ACT_ERRNO"}`),
145 145
 			},
146 146
 			c: &container.Container{
147
-				SeccompProfile: "",
147
+				SecurityOptions: container.SecurityOptions{SeccompProfile: ""},
148 148
 				HostConfig: &containertypes.HostConfig{
149 149
 					Privileged: false,
150 150
 				},
... ...
@@ -163,10 +163,10 @@ func TestWithSeccomp(t *testing.T) {
163 163
 			comment: "load prioritise container profile over daemon's",
164 164
 			daemon: &Daemon{
165 165
 				sysInfo:        &sysinfo.SysInfo{Seccomp: true},
166
-				seccompProfile: []byte("{ \"defaultAction\": \"SCMP_ACT_ERRNO\" }"),
166
+				seccompProfile: []byte(`{"defaultAction": "SCMP_ACT_ERRNO"}`),
167 167
 			},
168 168
 			c: &container.Container{
169
-				SeccompProfile: "{ \"defaultAction\": \"SCMP_ACT_LOG\" }",
169
+				SecurityOptions: container.SecurityOptions{SeccompProfile: `{"defaultAction": "SCMP_ACT_LOG"}`},
170 170
 				HostConfig: &containertypes.HostConfig{
171 171
 					Privileged: false,
172 172
 				},