Skip to content

Commit d39281d

Browse files
authored
Merge pull request #4639 from ningmingxiao/selinux_support
feature: support selinux
2 parents 27d052d + 48e8d41 commit d39281d

17 files changed

Lines changed: 191 additions & 13 deletions

File tree

cmd/nerdctl/container/container_run_security_linux_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ import (
2626

2727
"gotest.tools/v3/assert"
2828

29+
"github.com/containerd/nerdctl/mod/tigron/expect"
30+
"github.com/containerd/nerdctl/mod/tigron/test"
31+
"github.com/containerd/nerdctl/mod/tigron/tig"
32+
2933
"github.com/containerd/nerdctl/v2/pkg/apparmorutil"
3034
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
3135
"github.com/containerd/nerdctl/v2/pkg/testutil"
36+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
3237
)
3338

3439
func getCapEff(base *testutil.Base, args ...string) uint64 {
@@ -186,6 +191,106 @@ func TestRunApparmor(t *testing.T) {
186191
base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined")
187192
}
188193

194+
func TestRunSelinuxWithSecurityOpt(t *testing.T) {
195+
testCase := nerdtest.Setup()
196+
testCase.Require = nerdtest.Selinux
197+
testContainer := testutil.Identifier(t)
198+
199+
testCase.SubTests = []*test.Case{
200+
{
201+
Description: "test run with selinux-enabled",
202+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
203+
return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", testContainer, "sleep", "infinity")
204+
},
205+
Cleanup: func(data test.Data, helpers test.Helpers) {
206+
helpers.Anyhow("rm", "-f", testContainer)
207+
},
208+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
209+
return &test.Expected{
210+
ExitCode: 0,
211+
Output: expect.All(
212+
func(stdout string, t tig.T) {
213+
inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer)
214+
pid := strings.TrimSpace(inspectOut)
215+
fileName := fmt.Sprintf("/proc/%s/attr/current", pid)
216+
data, err := os.ReadFile(fileName)
217+
assert.NilError(t, err)
218+
assert.Equal(t, strings.Contains(string(data), "container_t"), true)
219+
},
220+
),
221+
}
222+
},
223+
},
224+
}
225+
testCase.Run(t)
226+
}
227+
func TestRunSelinux(t *testing.T) {
228+
testCase := nerdtest.Setup()
229+
testCase.Require = nerdtest.Selinux
230+
testContainer := testutil.Identifier(t)
231+
232+
testCase.SubTests = []*test.Case{
233+
{
234+
Description: "test run with selinux-enabled",
235+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
236+
return helpers.Command("--selinux-enabled", "run", "-d", "--name", testContainer, "sleep", "infinity")
237+
},
238+
Cleanup: func(data test.Data, helpers test.Helpers) {
239+
helpers.Anyhow("rm", "-f", testContainer)
240+
},
241+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
242+
return &test.Expected{
243+
ExitCode: 0,
244+
Output: expect.All(
245+
func(stdout string, t tig.T) {
246+
inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer)
247+
pid := strings.TrimSpace(inspectOut)
248+
fileName := fmt.Sprintf("/proc/%s/attr/current", pid)
249+
data, err := os.ReadFile(fileName)
250+
assert.NilError(t, err)
251+
assert.Equal(t, strings.Contains(string(data), "container_t"), true)
252+
},
253+
),
254+
}
255+
},
256+
},
257+
}
258+
testCase.Run(t)
259+
}
260+
261+
func TestRunSelinuxWithVolumeLabel(t *testing.T) {
262+
testCase := nerdtest.Setup()
263+
testCase.Require = nerdtest.Selinux
264+
testContainer := testutil.Identifier(t)
265+
266+
testCase.SubTests = []*test.Case{
267+
{
268+
Description: "test run with selinux-enabled",
269+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
270+
return helpers.Command("--selinux-enabled", "run", "-d", "-v", fmt.Sprintf("/%s:/%s:Z", testContainer, testContainer), "--name", testContainer, "sleep", "infinity")
271+
},
272+
Cleanup: func(data test.Data, helpers test.Helpers) {
273+
helpers.Anyhow("rm", "-f", testContainer)
274+
os.RemoveAll(fmt.Sprintf("/%s", testContainer))
275+
},
276+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
277+
return &test.Expected{
278+
ExitCode: 0,
279+
Output: expect.All(
280+
func(stdout string, t tig.T) {
281+
cmd := exec.Command("ls", "-Z", fmt.Sprintf("/%s", testContainer))
282+
lsStdout, err := cmd.CombinedOutput()
283+
assert.NilError(t, err)
284+
assert.Equal(t, strings.Contains(string(lsStdout), "container_t"), true)
285+
},
286+
),
287+
}
288+
},
289+
},
290+
}
291+
testCase.Run(t)
292+
}
293+
189294
// TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976
190295
func TestRunSeccompCapSysPtrace(t *testing.T) {
191296
base := testutil.NewBase(t)

cmd/nerdctl/helpers/flagutil.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
154154
return types.GlobalCommandOptions{}, err
155155
}
156156

157+
selinuxEnabled, err := cmd.Flags().GetBool("selinux-enabled")
158+
if err != nil {
159+
return types.GlobalCommandOptions{}, err
160+
}
157161
// Point to dataRoot for filesystem-helpers implementing rollback / backups.
158162
err = fs.InitFS(dataRoot)
159163
if err != nil {
@@ -180,6 +184,7 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
180184
DNS: dns,
181185
DNSOpts: dnsOpts,
182186
DNSSearch: dnsSearch,
187+
SelinuxEnabled: selinuxEnabled,
183188
}, nil
184189
}
185190

cmd/nerdctl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet,
191191
helpers.AddPersistentStringFlag(rootCmd, "host-gateway-ip", nil, nil, nil, aliasToBeInherited, cfg.HostGatewayIP, "NERDCTL_HOST_GATEWAY_IP", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host")
192192
helpers.AddPersistentStringFlag(rootCmd, "bridge-ip", nil, nil, nil, aliasToBeInherited, cfg.BridgeIP, "NERDCTL_BRIDGE_IP", "IP address for the default nerdctl bridge network")
193193
rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io")
194+
rootCmd.PersistentFlags().Bool("selinux-enabled", cfg.SelinuxEnabled, "Enable selinux support")
194195
rootCmd.PersistentFlags().StringSlice("cdi-spec-dirs", cfg.CDISpecDirs, "The directories to search for CDI spec files. Defaults to /etc/cdi,/var/run/cdi")
195196
rootCmd.PersistentFlags().String("userns-remap", cfg.UsernsRemap, "Support idmapping for creating and running containers. This options is only supported on linux. If `host` is passed, no idmapping is done. if a user name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively")
196197
helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns", cfg.DNS, "Global DNS servers for containers")

docs/command-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ Security flags:
255255

256256
- :whale: `--security-opt seccomp=<PROFILE_JSON_FILE>`: specify custom seccomp profile
257257
- :whale: `--security-opt apparmor=<PROFILE>`: specify custom AppArmor profile
258+
:whale: `--security-opt label=<selinuxlabel>`: specify custom selinux label
258259
- :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities
259260
- :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container
260261
- :whale: `--security-opt writable-cgroups`: making the cgroups writeable
@@ -1977,6 +1978,7 @@ Flags:
19771978
- :nerd_face: `--host-gateway-ip`: IP address that the special 'host-gateway' string in --add-host resolves to. It has no effect without setting --add-host
19781979
- Default: the IP address of the host
19791980
- :nerd_face: `--userns-remap=<username>:<groupname>`: Support idmapping of containers. This options is only supported on rootful linux for container create and run if a user name and optionally group name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively. Note: `--userns-remap` is not supported for building containers. Nerdctl Build doesn't support userns-remap feature. (format: <name|uid>[:<group|gid>])
1981+
- :nerd_face: `--selinux-enabled`: Enable selinux support
19801982

19811983
The global flags can be also specified in `/etc/nerdctl/nerdctl.toml` (rootful) and `~/.config/nerdctl/nerdctl.toml` (rootless).
19821984
See [`./config.md`](./config.md).

docs/config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ userns_remap = ""
3030
dns = ["8.8.8.8", "1.1.1.1"]
3131
dns_opts = ["ndots:1", "timeout:2"]
3232
dns_search = ["example.com", "example.org"]
33+
selinux_enabled= true
3334
```
3435

3536
## Properties
@@ -56,6 +57,7 @@ dns_search = ["example.com", "example.org"]
5657
| `dns` | | | Set global DNS servers for containers | Since 2.1.3 |
5758
| `dns_opts` | | | Set global DNS options for containers | Since 2.1.3 |
5859
| `dns_search` | | | Set global DNS search domains for containers | Since 2.1.3 |
60+
| `selinux_enabled` | | |Enable selinux support for containers | Since 2.3.0 |
5961

6062
The properties are parsed in the following precedence:
6163
1. CLI flag

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ require (
5454
github.com/opencontainers/go-digest v1.0.0
5555
github.com/opencontainers/image-spec v1.1.1
5656
github.com/opencontainers/runtime-spec v1.3.0
57+
github.com/opencontainers/selinux v1.13.1
5758
github.com/pelletier/go-toml/v2 v2.3.0
5859
github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined
5960
github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined
@@ -148,6 +149,7 @@ require (
148149
)
149150

150151
require (
152+
cyphar.com/go-pathrs v0.2.1 // indirect
151153
github.com/cespare/xxhash/v2 v2.3.0 // indirect
152154
github.com/moby/moby/api v1.54.2 // indirect
153155
github.com/moby/sys/capability v0.4.0 // indirect

pkg/cmd/container/run_linux.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func setPlatformOptions(ctx context.Context, client *containerd.Client, id, uts
7171
}
7272
opts = append(opts, capOpts...)
7373
securityOptsMaps := strutil.ConvertKVStringsToMap(strutil.DedupeStrSlice(options.SecurityOpt))
74-
secOpts, err := generateSecurityOpts(options.Privileged, securityOptsMaps)
74+
secOpts, err := generateSecurityOpts(options.Privileged, options.GOptions.SelinuxEnabled, securityOptsMaps)
7575
if err != nil {
7676
return nil, err
7777
}

pkg/cmd/container/run_security_linux.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717
package container
1818

1919
import (
20+
"context"
2021
"errors"
2122
"fmt"
2223
"strconv"
2324
"strings"
2425
"sync"
2526

27+
"github.com/opencontainers/runtime-spec/specs-go"
28+
"github.com/opencontainers/selinux/go-selinux/label"
29+
2630
"github.com/containerd/containerd/v2/contrib/apparmor"
2731
"github.com/containerd/containerd/v2/contrib/seccomp"
32+
"github.com/containerd/containerd/v2/core/containers"
2833
"github.com/containerd/containerd/v2/pkg/cap"
2934
"github.com/containerd/containerd/v2/pkg/oci"
3035
"github.com/containerd/log"
@@ -51,10 +56,10 @@ const (
5156
systemPathsUnconfined = "unconfined"
5257
)
5358

54-
func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) {
59+
func generateSecurityOpts(privileged bool, selinuxEnabled bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) {
5560
for k := range securityOptsMap {
5661
switch k {
57-
case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices", "writable-cgroups":
62+
case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices", "writable-cgroups", "label":
5863
default:
5964
log.L.Warnf("unknown security-opt: %q", k)
6065
}
@@ -95,6 +100,18 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
95100
opts = append(opts, apparmor.WithProfile(defaults.AppArmorProfileName))
96101
}
97102
}
103+
// TODO: should set unique MCS categorie.
104+
if !privileged && selinuxEnabled {
105+
var labelOpts []string
106+
if selinuxLabel, ok := securityOptsMap["label"]; ok {
107+
labelOpts = append(labelOpts, selinuxLabel)
108+
}
109+
processLabel, mountLabel, err := label.InitLabels(labelOpts)
110+
if err != nil {
111+
return nil, err
112+
}
113+
opts = append(opts, WithSelinuxLabel(processLabel, mountLabel))
114+
}
98115

99116
nnp, err := maputil.MapBoolValueAsOpt(securityOptsMap, "no-new-privileges")
100117
if err != nil {
@@ -141,6 +158,21 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
141158
return opts, nil
142159
}
143160

161+
// WithSelinuxLabels sets the mount and process labels
162+
func WithSelinuxLabel(process, mount string) oci.SpecOpts {
163+
return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
164+
if s.Linux == nil {
165+
s.Linux = &specs.Linux{}
166+
}
167+
if s.Process == nil {
168+
s.Process = &specs.Process{}
169+
}
170+
s.Linux.MountLabel = mount
171+
s.Process.SelinuxLabel = process
172+
return nil
173+
}
174+
}
175+
144176
func canonicalizeCapName(s string) string {
145177
if s == "" {
146178
return ""

pkg/cmd/system/info.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func Info(ctx context.Context, client *containerd.Client, options types.SystemIn
6969
return err
7070
}
7171
case "dockercompat":
72-
infoCompat, err = infoutil.Info(ctx, client, options.GOptions.Snapshotter, options.GOptions.CgroupManager)
72+
infoCompat, err = infoutil.Info(ctx, client, options.GOptions.Snapshotter, options.GOptions.CgroupManager, options.GOptions.SelinuxEnabled)
7373
if err != nil {
7474
return err
7575
}

pkg/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Config struct {
4747
DNSOpts []string `toml:"dns_opts,omitempty"`
4848
DNSSearch []string `toml:"dns_search,omitempty"`
4949
DisableHCSystemd bool `toml:"disable_hc_systemd"`
50+
SelinuxEnabled bool `toml:"selinux_enabled"`
5051
}
5152

5253
// New creates a default Config object statically,
@@ -63,6 +64,7 @@ func New() *Config {
6364
DataRoot: ncdefaults.DataRoot(),
6465
CgroupManager: ncdefaults.CgroupManager(),
6566
InsecureRegistry: false,
67+
SelinuxEnabled: false,
6668
HostsDir: ncdefaults.HostsDirs(),
6769
Experimental: true,
6870
HostGatewayIP: ncdefaults.HostGatewayIP(),

0 commit comments

Comments
 (0)