Skip to content

Commit 2eaa687

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

File tree

9 files changed

+87
-4
lines changed

9 files changed

+87
-4
lines changed

cmd/nerdctl/container/container_run_security_linux_test.go

Lines changed: 22 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,25 @@ func TestRunApparmor(t *testing.T) {
186189
base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined")
187190
}
188191

192+
func TestRunSelinux(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+
198+
inspectCmd := base.Cmd("inspect", testContainer, "--format", "{{.State.Pid}}")
199+
inspectRes := inspectCmd.Run()
200+
pid := strings.TrimSpace(inspectRes.Stdout())
201+
cmd := exec.Command("ps", "-Z", pid)
202+
stdout, err := cmd.Output()
203+
if err != nil {
204+
output := strings.TrimSpace(string(stdout))
205+
if strings.Contains(output, "container_t") {
206+
t.Fatal(fmt.Errorf("expect label container_t but get %s", output))
207+
}
208+
}
209+
}
210+
189211
// TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976
190212
func TestRunSeccompCapSysPtrace(t *testing.T) {
191213
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: 1 addition & 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

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: 40 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,24 @@ 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+
labelOpts = []string{"level:s0"}
115+
processLabel, mountLabel, err := label.InitLabels(labelOpts)
116+
if err != nil {
117+
return nil, err
118+
}
119+
opts = append(opts, WithSelinuxLabel(processLabel, mountLabel))
120+
}
98121

99122
nnp, err := maputil.MapBoolValueAsOpt(securityOptsMap, "no-new-privileges")
100123
if err != nil {
@@ -141,6 +164,21 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
141164
return opts, nil
142165
}
143166

167+
// WithSelinuxLabels sets the mount and process labels
168+
func WithSelinuxLabel(process, mount string) oci.SpecOpts {
169+
return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
170+
if s.Linux == nil {
171+
s.Linux = &specs.Linux{}
172+
}
173+
if s.Process == nil {
174+
s.Process = &specs.Process{}
175+
}
176+
s.Linux.MountLabel = mount
177+
s.Process.SelinuxLabel = process
178+
return nil
179+
}
180+
}
181+
144182
func canonicalizeCapName(s string) string {
145183
if s == "" {
146184
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)