Skip to content

feat: add support for userns #3941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -140,7 +140,7 @@ jobs:
docker run --privileged --rm tonistiigi/binfmt --install linux/arm64
docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7
- name: "Run integration tests"
run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false
run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false -test.allow-modify-users=true
- name: "Run integration tests (flaky)"
run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true

2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -114,7 +114,7 @@ linters:
arguments: [7]
- name: function-length
# 155 occurrences (at default 0, 75). Really long functions should really be broken up in most cases.
arguments: [0, 400]
arguments: [0, 450]
- name: cyclomatic
# 204 occurrences (at default 10)
arguments: [100]
9 changes: 9 additions & 0 deletions cmd/nerdctl/builder/builder_build.go
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@ import (

"github.com/spf13/cobra"

"github.com/containerd/log"

"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
"github.com/containerd/nerdctl/v2/pkg/api/types"
@@ -209,6 +211,13 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
return types.BuilderBuildOptions{}, err
}

usernsRemap, err := cmd.Flags().GetString("userns-remap")
if err != nil {
return types.BuilderBuildOptions{}, err
} else if usernsRemap != "" {
log.L.Warn("WARNING! userns remap is not supported with nerdctl build. dropping the config.")
}

return types.BuilderBuildOptions{
GOptions: globalOptions,
BuildKitHost: buildKitHost,
24 changes: 24 additions & 0 deletions cmd/nerdctl/container/container_create.go
Original file line number Diff line number Diff line change
@@ -454,6 +454,30 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) {
}
// #endregion

// #region for UserNS
opt.UserNS, err = cmd.Flags().GetString("userns-remap")
if err != nil {
return opt, err
}

userns, err := cmd.Flags().GetString("userns")
if err != nil {
return opt, err
}

if userns == "host" {
opt.UserNS = ""
} else if userns != "" {
return opt, fmt.Errorf("invalid user mode")
}

if opt.Privileged && opt.UserNS != "" {
//userns-remap is not supported with privileged flag.
// Ref: https://docs.docker.com/engine/security/userns-remap/
return opt, fmt.Errorf("privileged flag cannot be used with userns-remap")
}
// #endregion

return opt, nil
}

197 changes: 197 additions & 0 deletions cmd/nerdctl/container/container_create_linux_test.go
Original file line number Diff line number Diff line change
@@ -19,9 +19,12 @@ package container
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"testing"

"github.com/opencontainers/go-digest"
@@ -325,3 +328,197 @@ func TestCreateFromOCIArchive(t *testing.T) {
base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK()
base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive")
}

func TestUsernsMappingCreateCmd(t *testing.T) {
nerdtest.Setup()

testCase := &test.Case{
Require: require.All(
nerdtest.AllowModifyUserns,
nerdtest.RemapIDs,
require.Not(nerdtest.Docker)),
NoParallel: true,
Setup: func(data test.Data, helpers test.Helpers) {
data.Labels().Set("validUserns", "nerdctltestuser")
data.Labels().Set("expectedHostUID", "123456789")
data.Labels().Set("invalidUserns", "invaliduser")
},
SubTests: []*test.Case{
{
Description: "Test container create with valid Userns",
NoParallel: true, // Changes system config so running in non parallel mode
Setup: func(data test.Data, helpers test.Helpers) {
err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers)
assert.NilError(t, err, "Failed to append Userns config")
},
Cleanup: func(data test.Data, helpers test.Helpers) {
removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers)
helpers.Anyhow("rm", "-f", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
helpers.Ensure("create", "--tty", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage)
return helpers.Command("start", data.Identifier())
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 0,
Output: func(stdout string, info string, t *testing.T) {
actualHostUID, err := getContainerHostUID(helpers, data.Identifier())
assert.NilError(t, err, "Failed to get container host UID")
assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info)
},
}
},
},
{
Description: "Test container create failure with valid Userns and privileged flag",
NoParallel: true, // Changes system config so running in non parallel mode
Setup: func(data test.Data, helpers test.Helpers) {
err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers)
assert.NilError(t, err, "Failed to append Userns config")
},
Cleanup: func(data test.Data, helpers test.Helpers) {
removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers)
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("create", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage)
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 1,
}
},
},
{
Description: "Test container create with invalid Userns",
NoParallel: true, // Changes system config so running in non parallel mode
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rm", "-f", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("create", "--tty", "--userns-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage)
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 1,
}
},
},
},
}
testCase.Run(t)
}

func getContainerHostUID(helpers test.Helpers, containerName string) (string, error) {
result := helpers.Capture("inspect", "--format", "{{.State.Pid}}", containerName)
pidStr := strings.TrimSpace(result)
pid, err := strconv.Atoi(pidStr)
if err != nil {
return "", fmt.Errorf("invalid PID: %v", err)
}

stat, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
if err != nil {
return "", fmt.Errorf("failed to stat process: %v", err)
}

uid := int(stat.Sys().(*syscall.Stat_t).Uid)
return strconv.Itoa(uid), nil
}

func appendUsernsConfig(userns string, hostUID string, helpers test.Helpers) error {
if err := addUser(userns, hostUID, helpers); err != nil {
return fmt.Errorf("failed to add user %s: %w", userns, err)
}

entry := fmt.Sprintf("%s:%s:65536\n", userns, hostUID)

tempDir := helpers.T().TempDir()
files := []string{"subuid", "subgid"}
for _, file := range files {

fileBak := filepath.Join(tempDir, file)
defer os.Remove(fileBak)
d, err := os.Create(fileBak)
if err != nil {
return fmt.Errorf("failed to create %s: %w", fileBak, err)
}

s, err := os.Open(filepath.Join("/etc", file))
if err != nil {
return fmt.Errorf("failed to open %s: %w", file, err)
}
defer s.Close()

_, err = io.Copy(d, s)
if err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", file, fileBak, err)
}

f, err := os.OpenFile(fmt.Sprintf("/etc/%s", file), os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open %s: %w", file, err)
}
defer f.Close()

if _, err := f.WriteString(entry); err != nil {
return fmt.Errorf("failed to write to %s: %w", file, err)
}
}
return nil
}

func addUser(username string, hostID string, helpers test.Helpers) error {
helpers.Custom("groupadd", "-g", hostID, username).Run(&test.Expected{
ExitCode: 0})
helpers.Custom("useradd", "-u", hostID, "-g", hostID, "-s", "/bin/false", username).Run(&test.Expected{
ExitCode: 0})
return nil
}

func removeUsernsConfig(t *testing.T, userns string, helpers test.Helpers) {
if err := delUser(userns, helpers); err != nil {
t.Logf("failed to del user %s, Error: %s", userns, err)
}

if err := delGroup(userns, helpers); err != nil {
t.Logf("failed to del group %s, Error: %s", userns, err)
}

tempDir := helpers.T().TempDir()
files := []string{"subuid", "subgid"}
for _, file := range files {
fileBak := filepath.Join(tempDir, file)
s, err := os.Open(fileBak)
if err != nil {
t.Logf("failed to open %s, Error: %s", fileBak, err)
continue
}
defer s.Close()

d, err := os.Open(filepath.Join("/etc/%s", file))
if err != nil {
t.Logf("failed to open %s, Error: %s", file, err)
continue

}
defer d.Close()

_, err = io.Copy(d, s)
if err != nil {
t.Logf("failed to restore. Copy %s to %s failed, Error %s", fileBak, file, err)
continue
}

}
}

func delUser(username string, helpers test.Helpers) error {
helpers.Custom("userdel", username).RunAnyhow()
return nil
}

func delGroup(groupname string, helpers test.Helpers) error {
helpers.Custom("groupdel", groupname).RunAnyhow()
return nil
}
2 changes: 1 addition & 1 deletion cmd/nerdctl/container/container_run.go
Original file line number Diff line number Diff line change
@@ -288,14 +288,14 @@ func setCreateFlags(cmd *cobra.Command) {
// #endregion

cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)")

cmd.Flags().String("isolation", "default", "Specify isolation technology for container. On Linux the only valid value is default. Windows options are host, process and hyperv with process isolation as the default")
cmd.RegisterFlagCompletionFunc("isolation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if runtime.GOOS == "windows" {
return []string{"default", "host", "process", "hyperv"}, cobra.ShellCompDirectiveNoFileComp
}
return []string{"default"}, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().String("userns", "", "Specify host to disable userns-remap")

}

Loading