Skip to content

Commit a210aa3

Browse files
committed
feature: support selinux
Signed-off-by: ningmingxiao <[email protected]>
1 parent 5604f90 commit a210aa3

File tree

10 files changed

+137
-4
lines changed

10 files changed

+137
-4
lines changed

cmd/nerdctl/container/container_run_security_linux_test.go

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

2727
"gotest.tools/v3/assert"
2828

29+
"github.com/containerd/nerdctl/mod/tigron/expect"
30+
"github.com/containerd/nerdctl/mod/tigron/require"
31+
32+
"github.com/containerd/nerdctl/mod/tigron/test"
33+
"github.com/containerd/nerdctl/mod/tigron/tig"
2934
"github.com/containerd/nerdctl/v2/pkg/apparmorutil"
3035
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
3136
"github.com/containerd/nerdctl/v2/pkg/testutil"
37+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
3238
)
3339

3440
func getCapEff(base *testutil.Base, args ...string) uint64 {
@@ -186,6 +192,71 @@ func TestRunApparmor(t *testing.T) {
186192
base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined")
187193
}
188194

195+
func TestRunSelinuxWithSecurityOpt(t *testing.T) {
196+
testCase := nerdtest.Setup()
197+
testCase.Require = require.Not(nerdtest.NoSelinux)
198+
testContainer := testutil.Identifier(t)
199+
200+
testCase.SubTests = []*test.Case{
201+
{
202+
Description: "test run with selinux-enabled",
203+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
204+
return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", testContainer, "sleep", "infinity")
205+
},
206+
Cleanup: func(data test.Data, helpers test.Helpers) {
207+
helpers.Anyhow("rm", "-f", testContainer)
208+
},
209+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
210+
return &test.Expected{
211+
ExitCode: 0,
212+
Output: expect.All(
213+
func(stdout string, t tig.T) {
214+
inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer)
215+
pid := strings.TrimSpace(string(inspectOut))
216+
fileName := fmt.Sprintf("/proc/%s/attr/current", pid)
217+
data, err := os.ReadFile(fileName)
218+
assert.NilError(t, err)
219+
assert.Equal(t, strings.Contains(string(data), "container_t"), true)
220+
},
221+
),
222+
}
223+
},
224+
},
225+
}
226+
}
227+
func TestRunSelinux(t *testing.T) {
228+
testCase := nerdtest.Setup()
229+
testCase.Require = require.Not(nerdtest.NoSelinux)
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(string(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+
}
259+
189260
// TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976
190261
func TestRunSeccompCapSysPtrace(t *testing.T) {
191262
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
@@ -188,6 +188,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet,
188188
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")
189189
helpers.AddPersistentStringFlag(rootCmd, "bridge-ip", nil, nil, nil, aliasToBeInherited, cfg.BridgeIP, "NERDCTL_BRIDGE_IP", "IP address for the default nerdctl bridge network")
190190
rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io")
191+
rootCmd.PersistentFlags().Bool("selinux-enabled", cfg.SelinuxEnabled, "Enable selinux support")
191192
rootCmd.PersistentFlags().StringSlice("cdi-spec-dirs", cfg.CDISpecDirs, "The directories to search for CDI spec files. Defaults to /etc/cdi,/var/run/cdi")
192193
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")
193194
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
@@ -254,6 +254,7 @@ Security flags:
254254

255255
- :whale: `--security-opt seccomp=<PROFILE_JSON_FILE>`: specify custom seccomp profile
256256
- :whale: `--security-opt apparmor=<PROFILE>`: specify custom AppArmor profile
257+
:whale: `--security-opt label=<selinuxbel>`: specify custom selinux label
257258
- :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities
258259
- :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container
259260
- :whale: `--security-opt writable-cgroups`: making the cgroups writeable
@@ -1959,6 +1960,7 @@ Flags:
19591960
- :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
19601961
- Default: the IP address of the host
19611962
- :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>])
1963+
- :nerd_face: `--selinux-enabled`: Enable selinux support
19621964

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

docs/config.md

Lines changed: 1 addition & 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

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ require (
5353
github.com/opencontainers/go-digest v1.0.0
5454
github.com/opencontainers/image-spec v1.1.1
5555
github.com/opencontainers/runtime-spec v1.2.1
56+
github.com/opencontainers/selinux v1.13.0
5657
github.com/pelletier/go-toml/v2 v2.2.4
5758
github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined
5859
github.com/rootless-containers/rootlesskit/v2 v2.3.5 //gomodjail:unconfined
@@ -113,7 +114,6 @@ require (
113114
github.com/multiformats/go-multihash v0.2.3 // indirect
114115
github.com/multiformats/go-varint v0.1.0 // indirect
115116
github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 // indirect
116-
github.com/opencontainers/selinux v1.13.0 // indirect
117117
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
118118
github.com/philhofer/fwd v1.2.0 // indirect
119119
github.com/pkg/errors v0.9.1 // indirect

pkg/cmd/container/run_linux.go

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

pkg/cmd/container/run_security_linux.go

Lines changed: 39 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,23 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
95100
opts = append(opts, apparmor.WithProfile(defaults.AppArmorProfileName))
96101
}
97102
}
103+
var labelOpts []string
104+
if selinuxLabel, ok := securityOptsMap["label"]; ok {
105+
labelOpts = append(labelOpts, selinuxLabel)
106+
processLabel, mountLabel, err := label.InitLabels(labelOpts)
107+
if err != nil {
108+
return nil, err
109+
}
110+
opts = append(opts, WithSelinuxLabel(processLabel, mountLabel))
111+
}
112+
// if selinux-enabled=true and security-opt selinux label is not set.
113+
if selinuxEnabled && len(labelOpts) == 0 {
114+
processLabel, mountLabel, err := label.InitLabels(labelOpts)
115+
if err != nil {
116+
return nil, err
117+
}
118+
opts = append(opts, WithSelinuxLabel(processLabel, mountLabel))
119+
}
98120

99121
nnp, err := maputil.MapBoolValueAsOpt(securityOptsMap, "no-new-privileges")
100122
if err != nil {
@@ -141,6 +163,21 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
141163
return opts, nil
142164
}
143165

166+
// WithSelinuxLabels sets the mount and process labels
167+
func WithSelinuxLabel(process, mount string) oci.SpecOpts {
168+
return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
169+
if s.Linux == nil {
170+
s.Linux = &specs.Linux{}
171+
}
172+
if s.Process == nil {
173+
s.Process = &specs.Process{}
174+
}
175+
s.Linux.MountLabel = mount
176+
s.Process.SelinuxLabel = process
177+
return nil
178+
}
179+
}
180+
144181
func canonicalizeCapName(s string) string {
145182
if s == "" {
146183
return ""

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(),

pkg/testutil/nerdtest/requirements.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"strings"
2626

2727
"github.com/Masterminds/semver/v3"
28+
"github.com/opencontainers/selinux/go-selinux"
2829
"gotest.tools/v3/assert"
2930

3031
"github.com/containerd/containerd/v2/defaults"
@@ -161,6 +162,19 @@ var Rootless = &test.Requirement{
161162
},
162163
}
163164

165+
// NoSexlinux marks a test as suitable only for the noselinux enable environment
166+
var NoSelinux = &test.Requirement{
167+
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {
168+
ret = !selinux.GetEnabled()
169+
if ret {
170+
mess = "selinux is disabled"
171+
} else {
172+
mess = "selinux is enabled"
173+
}
174+
return ret, mess
175+
},
176+
}
177+
164178
// RootlessWithDetachNetNS marks a test as suitable only for rootless environment with detached netns support.
165179
var RootlessWithDetachNetNS = &test.Requirement{
166180
Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) {

0 commit comments

Comments
 (0)