Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions cmd/nerdctl/container/container_run_security_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ import (

"gotest.tools/v3/assert"

"github.com/containerd/nerdctl/mod/tigron/expect"
"github.com/containerd/nerdctl/mod/tigron/test"
"github.com/containerd/nerdctl/mod/tigron/tig"

"github.com/containerd/nerdctl/v2/pkg/apparmorutil"
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
"github.com/containerd/nerdctl/v2/pkg/testutil"
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)

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

func TestRunSelinuxWithSecurityOpt(t *testing.T) {
Comment thread
ningmingxiao marked this conversation as resolved.
testCase := nerdtest.Setup()
testCase.Require = nerdtest.Selinux
testContainer := testutil.Identifier(t)

testCase.SubTests = []*test.Case{
{
Description: "test run with selinux-enabled",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("--selinux-enabled", "run", "-d", "--security-opt", "label=type:container_t", "--name", testContainer, "sleep", "infinity")
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rm", "-f", testContainer)
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use expect.ExitCodeSuccess

Output: expect.All(
func(stdout string, t tig.T) {
inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer)
pid := strings.TrimSpace(inspectOut)
fileName := fmt.Sprintf("/proc/%s/attr/current", pid)
data, err := os.ReadFile(fileName)
assert.NilError(t, err)
assert.Equal(t, strings.Contains(string(data), "container_t"), true)
},
),
}
},
},
}
testCase.Run(t)
}
func TestRunSelinux(t *testing.T) {
Comment thread
ningmingxiao marked this conversation as resolved.
Comment thread
ChengyuZhu6 marked this conversation as resolved.
testCase := nerdtest.Setup()
testCase.Require = nerdtest.Selinux
testContainer := testutil.Identifier(t)

testCase.SubTests = []*test.Case{
{
Description: "test run with selinux-enabled",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("--selinux-enabled", "run", "-d", "--name", testContainer, "sleep", "infinity")
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rm", "-f", testContainer)
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 0,
Output: expect.All(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here can use expect.ExitCodeSuccess

func(stdout string, t tig.T) {
inspectOut := helpers.Capture("container", "inspect", "--format", "{{.State.Pid}}", testContainer)
pid := strings.TrimSpace(inspectOut)
fileName := fmt.Sprintf("/proc/%s/attr/current", pid)
data, err := os.ReadFile(fileName)
assert.NilError(t, err)
assert.Equal(t, strings.Contains(string(data), "container_t"), true)
},
),
}
},
},
}
testCase.Run(t)
}

func TestRunSelinuxWithVolumeLabel(t *testing.T) {
Comment thread
ningmingxiao marked this conversation as resolved.
testCase := nerdtest.Setup()
testCase.Require = nerdtest.Selinux
testContainer := testutil.Identifier(t)

testCase.SubTests = []*test.Case{
{
Description: "test run with selinux-enabled",
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("--selinux-enabled", "run", "-d", "-v", fmt.Sprintf("/%s:/%s:Z", testContainer, testContainer), "--name", testContainer, "sleep", "infinity")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lower z should be tested too
https://docs.docker.com/engine/storage/bind-mounts/#configure-the-selinux-label

The z option indicates that the bind mount content is shared among multiple containers.
The Z option indicates that the bind mount content is private and unshared.

This should be verified by launching multiple containers

},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rm", "-f", testContainer)
os.RemoveAll(fmt.Sprintf("/%s", testContainer))
},
Comment thread
ningmingxiao marked this conversation as resolved.
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 0,
Comment thread
ningmingxiao marked this conversation as resolved.
Output: expect.All(
func(stdout string, t tig.T) {
cmd := exec.Command("ls", "-Z", fmt.Sprintf("/%s", testContainer))
lsStdout, err := cmd.CombinedOutput()
assert.NilError(t, err)
assert.Equal(t, strings.Contains(string(lsStdout), "container_t"), true)
},
),
}
},
},
}
testCase.Run(t)
}

// TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976
func TestRunSeccompCapSysPtrace(t *testing.T) {
base := testutil.NewBase(t)
Expand Down
5 changes: 5 additions & 0 deletions cmd/nerdctl/helpers/flagutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
return types.GlobalCommandOptions{}, err
}

selinuxEnabled, err := cmd.Flags().GetBool("selinux-enabled")
if err != nil {
return types.GlobalCommandOptions{}, err
}
// Point to dataRoot for filesystem-helpers implementing rollback / backups.
err = fs.InitFS(dataRoot)
if err != nil {
Expand All @@ -180,6 +184,7 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
DNS: dns,
DNSOpts: dnsOpts,
DNSSearch: dnsSearch,
SelinuxEnabled: selinuxEnabled,
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions cmd/nerdctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet,
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")
helpers.AddPersistentStringFlag(rootCmd, "bridge-ip", nil, nil, nil, aliasToBeInherited, cfg.BridgeIP, "NERDCTL_BRIDGE_IP", "IP address for the default nerdctl bridge network")
rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io")
rootCmd.PersistentFlags().Bool("selinux-enabled", cfg.SelinuxEnabled, "Enable selinux support")
rootCmd.PersistentFlags().StringSlice("cdi-spec-dirs", cfg.CDISpecDirs, "The directories to search for CDI spec files. Defaults to /etc/cdi,/var/run/cdi")
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")
helpers.HiddenPersistentStringArrayFlag(rootCmd, "global-dns", cfg.DNS, "Global DNS servers for containers")
Expand Down
2 changes: 2 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ Security flags:

- :whale: `--security-opt seccomp=<PROFILE_JSON_FILE>`: specify custom seccomp profile
- :whale: `--security-opt apparmor=<PROFILE>`: specify custom AppArmor profile
:whale: `--security-opt label=<selinuxlabel>`: specify custom selinux label
- :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities
- :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container
- :whale: `--security-opt writable-cgroups`: making the cgroups writeable
Expand Down Expand Up @@ -1977,6 +1978,7 @@ Flags:
- :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
- Default: the IP address of the host
- :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>])
- :nerd_face: `--selinux-enabled`: Enable selinux support
Comment thread
ningmingxiao marked this conversation as resolved.

The global flags can be also specified in `/etc/nerdctl/nerdctl.toml` (rootful) and `~/.config/nerdctl/nerdctl.toml` (rootless).
See [`./config.md`](./config.md).
Expand Down
2 changes: 2 additions & 0 deletions docs/config.md
Comment thread
ChengyuZhu6 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ userns_remap = ""
dns = ["8.8.8.8", "1.1.1.1"]
dns_opts = ["ndots:1", "timeout:2"]
dns_search = ["example.com", "example.org"]
selinux_enabled= true
```

## Properties
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be updated

Copy link
Copy Markdown
Contributor Author

@ningmingxiao ningmingxiao Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you meean ? about selinux_enabled= true @AkihiroSuda

Expand All @@ -56,6 +57,7 @@ dns_search = ["example.com", "example.org"]
| `dns` | | | Set global DNS servers for containers | Since 2.1.3 |
| `dns_opts` | | | Set global DNS options for containers | Since 2.1.3 |
| `dns_search` | | | Set global DNS search domains for containers | Since 2.1.3 |
| `selinux_enabled` | | |Enable selinux support for containers | Since 2.3.0 |

The properties are parsed in the following precedence:
1. CLI flag
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/opencontainers/runtime-spec v1.3.0
github.com/opencontainers/selinux v1.13.1
github.com/pelletier/go-toml/v2 v2.3.0
github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined
github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined
Expand Down Expand Up @@ -148,6 +149,7 @@ require (
)

require (
cyphar.com/go-pathrs v0.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/container/run_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func setPlatformOptions(ctx context.Context, client *containerd.Client, id, uts
}
opts = append(opts, capOpts...)
securityOptsMaps := strutil.ConvertKVStringsToMap(strutil.DedupeStrSlice(options.SecurityOpt))
secOpts, err := generateSecurityOpts(options.Privileged, securityOptsMaps)
secOpts, err := generateSecurityOpts(options.Privileged, options.GOptions.SelinuxEnabled, securityOptsMaps)
if err != nil {
return nil, err
}
Expand Down
36 changes: 34 additions & 2 deletions pkg/cmd/container/run_security_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@
package container

import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"sync"

"github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/selinux/go-selinux/label"

"github.com/containerd/containerd/v2/contrib/apparmor"
"github.com/containerd/containerd/v2/contrib/seccomp"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/cap"
"github.com/containerd/containerd/v2/pkg/oci"
"github.com/containerd/log"
Expand All @@ -51,10 +56,10 @@ const (
systemPathsUnconfined = "unconfined"
)

func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) {
func generateSecurityOpts(privileged bool, selinuxEnabled bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) {
for k := range securityOptsMap {
switch k {
case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices", "writable-cgroups":
case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices", "writable-cgroups", "label":
default:
log.L.Warnf("unknown security-opt: %q", k)
}
Expand Down Expand Up @@ -95,6 +100,18 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
opts = append(opts, apparmor.WithProfile(defaults.AppArmorProfileName))
}
}
// TODO: should set unique MCS categorie.
if !privileged && selinuxEnabled {
var labelOpts []string
if selinuxLabel, ok := securityOptsMap["label"]; ok {
labelOpts = append(labelOpts, selinuxLabel)
}
processLabel, mountLabel, err := label.InitLabels(labelOpts)
if err != nil {
return nil, err
}
opts = append(opts, WithSelinuxLabel(processLabel, mountLabel))
}

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

// WithSelinuxLabels sets the mount and process labels
func WithSelinuxLabel(process, mount string) oci.SpecOpts {
return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
if s.Linux == nil {
s.Linux = &specs.Linux{}
}
if s.Process == nil {
s.Process = &specs.Process{}
}
s.Linux.MountLabel = mount
s.Process.SelinuxLabel = process
return nil
}
}

func canonicalizeCapName(s string) string {
if s == "" {
return ""
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/system/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func Info(ctx context.Context, client *containerd.Client, options types.SystemIn
return err
}
case "dockercompat":
infoCompat, err = infoutil.Info(ctx, client, options.GOptions.Snapshotter, options.GOptions.CgroupManager)
infoCompat, err = infoutil.Info(ctx, client, options.GOptions.Snapshotter, options.GOptions.CgroupManager, options.GOptions.SelinuxEnabled)
if err != nil {
return err
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type Config struct {
DNSOpts []string `toml:"dns_opts,omitempty"`
DNSSearch []string `toml:"dns_search,omitempty"`
DisableHCSystemd bool `toml:"disable_hc_systemd"`
SelinuxEnabled bool `toml:"selinux_enabled"`
}

// New creates a default Config object statically,
Expand All @@ -63,6 +64,7 @@ func New() *Config {
DataRoot: ncdefaults.DataRoot(),
CgroupManager: ncdefaults.CgroupManager(),
InsecureRegistry: false,
SelinuxEnabled: false,
HostsDir: ncdefaults.HostsDirs(),
Experimental: true,
HostGatewayIP: ncdefaults.HostGatewayIP(),
Expand Down
4 changes: 2 additions & 2 deletions pkg/infoutil/infoutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func NativeDaemonInfo(ctx context.Context, client *containerd.Client) (*native.D
return daemonInfo, nil
}

func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupManager string) (*dockercompat.Info, error) {
func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupManager string, selinuxEnabled bool) (*dockercompat.Info, error) {
daemonVersion, err := client.Version(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -95,7 +95,7 @@ func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupMan
return nil, err
}
info.ServerVersion = daemonVersion.Version
fulfillPlatformInfo(&info)
fulfillPlatformInfo(&info, selinuxEnabled)
return &info, nil
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/infoutil/infoutil_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func CgroupsVersion() string {
return ""
}

func fulfillPlatformInfo(info *dockercompat.Info) {
func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) {
// unimplemented
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/infoutil/infoutil_freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func CgroupsVersion() string {
return ""
}

func fulfillPlatformInfo(info *dockercompat.Info) {
func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) {
// unimplemented
}

Expand Down
9 changes: 6 additions & 3 deletions pkg/infoutil/infoutil_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func CgroupsVersion() string {
return "1"
}

func fulfillSecurityOptions(info *dockercompat.Info) {
func fulfillSecurityOptions(info *dockercompat.Info, selinuxEnabled bool) {
if apparmorutil.CanApplyExistingProfile() {
info.SecurityOptions = append(info.SecurityOptions, "name=apparmor")
if rootlessutil.IsRootless() && !apparmorutil.CanApplySpecificExistingProfile(defaults.AppArmorProfileName) {
Expand All @@ -52,6 +52,9 @@ WARNING: AppArmor profile %q is not loaded.
This warning is negligible if you do not intend to use AppArmor.`), defaults.AppArmorProfileName))
}
}
if selinuxEnabled {
info.SecurityOptions = append(info.SecurityOptions, "name=selinux")
}
info.SecurityOptions = append(info.SecurityOptions, "name=seccomp,profile="+defaults.SeccompProfileName)
if defaults.CgroupnsMode() == "private" {
info.SecurityOptions = append(info.SecurityOptions, "name=cgroupns")
Expand All @@ -65,8 +68,8 @@ WARNING: AppArmor profile %q is not loaded.
//
// fulfillPlatformInfo requires the following fields to be set:
// SecurityOptions, CgroupDriver, CgroupVersion
func fulfillPlatformInfo(info *dockercompat.Info) {
fulfillSecurityOptions(info)
func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) {
fulfillSecurityOptions(info, selinuxEnabled)
mobySysInfo := mobySysInfo(info)

if info.CgroupDriver == "none" {
Expand Down
2 changes: 1 addition & 1 deletion pkg/infoutil/infoutil_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func CgroupsVersion() string {
return ""
}

func fulfillPlatformInfo(info *dockercompat.Info) {
func fulfillPlatformInfo(info *dockercompat.Info, selinuxEnabled bool) {
mobySysInfo := mobySysInfo(info)

// NOTE: cgroup fields are not available on Windows
Expand Down
Loading
Loading