Skip to content

Commit 56a8926

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

File tree

9 files changed

+110
-4
lines changed

9 files changed

+110
-4
lines changed

cmd/nerdctl/container/container_run_security_linux_test.go

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

2727
"gotest.tools/v3/assert"
2828

29+
"github.com/containerd/nerdctl/mod/tigron/require"
30+
2931
"github.com/containerd/nerdctl/v2/pkg/apparmorutil"
3032
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
3133
"github.com/containerd/nerdctl/v2/pkg/testutil"
34+
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
3235
)
3336

3437
func getCapEff(base *testutil.Base, args ...string) uint64 {
@@ -186,6 +189,48 @@ func TestRunApparmor(t *testing.T) {
186189
base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined")
187190
}
188191

192+
func TestRunSelinuxWithSecurityOpt(t *testing.T) {
193+
require.Not(nerdtest.NoSelinux)
194+
base := testutil.NewBase(t)
195+
testContainer := testutil.Identifier(t)
196+
base.Cmd("run", "--name", testContainer, "-d", "--security-opt", "label=type:container_t", testutil.AlpineImage, "sleep", "infinity").AssertOK()
197+
defer func() {
198+
base.Cmd("rm", "-f", testContainer)
199+
}()
200+
inspectCmd := base.Cmd("inspect", testContainer, "--format", "{{.State.Pid}}")
201+
inspectRes := inspectCmd.Run()
202+
pid := strings.TrimSpace(inspectRes.Stdout())
203+
cmd := exec.Command("ps", "-Z", pid)
204+
stdout, err := cmd.Output()
205+
if err != nil {
206+
output := strings.TrimSpace(string(stdout))
207+
if strings.Contains(output, "container_t") {
208+
t.Fatal(fmt.Errorf("expect label container_t but get %s", output))
209+
}
210+
}
211+
}
212+
213+
func TestRunSelinux(t *testing.T) {
214+
require.Not(nerdtest.NoSelinux)
215+
base := testutil.NewBase(t)
216+
testContainer := testutil.Identifier(t)
217+
base.Cmd("--selinux-enabled", "run", "--name", testContainer, "-d", testutil.AlpineImage, "sleep", "infinity").AssertOK()
218+
defer func() {
219+
base.Cmd("rm", "-f", testContainer)
220+
}()
221+
inspectCmd := base.Cmd("inspect", testContainer, "--format", "{{.State.Pid}}")
222+
inspectRes := inspectCmd.Run()
223+
pid := strings.TrimSpace(inspectRes.Stdout())
224+
cmd := exec.Command("ps", "-Z", pid)
225+
stdout, err := cmd.Output()
226+
if err != nil {
227+
output := strings.TrimSpace(string(stdout))
228+
if strings.Contains(output, "container_t") {
229+
t.Fatal(fmt.Errorf("expect label container_t but get %s", output))
230+
}
231+
}
232+
}
233+
189234
// TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976
190235
func TestRunSeccompCapSysPtrace(t *testing.T) {
191236
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+
SexlinuxEnabled: 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.SexlinuxEnabled, "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).

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.SexlinuxEnabled, 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+
SexlinuxEnabled 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+
SexlinuxEnabled: 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)