github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_run_security_linux_test.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"os/exec"
    23  	"strconv"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/containerd/nerdctl/pkg/apparmorutil"
    28  	"github.com/containerd/nerdctl/pkg/rootlessutil"
    29  	"github.com/containerd/nerdctl/pkg/testutil"
    30  
    31  	"gotest.tools/v3/assert"
    32  )
    33  
    34  func getCapEff(base *testutil.Base, args ...string) uint64 {
    35  	fullArgs := []string{"run", "--rm"}
    36  	fullArgs = append(fullArgs, args...)
    37  	fullArgs = append(fullArgs,
    38  		testutil.AlpineImage,
    39  		"sh",
    40  		"-euc",
    41  		"grep -w ^CapEff: /proc/self/status | sed -e \"s/^CapEff:[[:space:]]*//g\"",
    42  	)
    43  	cmd := base.Cmd(fullArgs...)
    44  	res := cmd.Run()
    45  	assert.NilError(base.T, res.Error)
    46  	s := strings.TrimSpace(res.Stdout())
    47  	ui64, err := strconv.ParseUint(s, 16, 64)
    48  	assert.NilError(base.T, err)
    49  	return ui64
    50  }
    51  
    52  const (
    53  	CapNetRaw  = 13
    54  	CapIPCLock = 14
    55  )
    56  
    57  func TestRunCap(t *testing.T) {
    58  	t.Parallel()
    59  	base := testutil.NewBase(t)
    60  
    61  	// allCaps varies depending on the target version and the kernel version.
    62  	allCaps := getCapEff(base, "--privileged")
    63  
    64  	// https://github.com/containerd/containerd/blob/9a9bd097564b0973bfdb0b39bf8262aa1b7da6aa/oci/spec.go#L93
    65  	defaultCaps := uint64(0xa80425fb)
    66  
    67  	t.Logf("allCaps=%016x", allCaps)
    68  
    69  	type testCase struct {
    70  		args   []string
    71  		capEff uint64
    72  	}
    73  	testCases := []testCase{
    74  		{
    75  			capEff: allCaps & defaultCaps,
    76  		},
    77  		{
    78  			args:   []string{"--cap-add=all"},
    79  			capEff: allCaps,
    80  		},
    81  		{
    82  			args:   []string{"--cap-add=ipc_lock"},
    83  			capEff: (allCaps & defaultCaps) | (1 << CapIPCLock),
    84  		},
    85  		{
    86  			args:   []string{"--cap-add=all", "--cap-drop=net_raw"},
    87  			capEff: allCaps ^ (1 << CapNetRaw),
    88  		},
    89  		{
    90  			args:   []string{"--cap-drop=all", "--cap-add=net_raw"},
    91  			capEff: 1 << CapNetRaw,
    92  		},
    93  		{
    94  			args:   []string{"--cap-drop=all", "--cap-add=NET_RAW"},
    95  			capEff: 1 << CapNetRaw,
    96  		},
    97  		{
    98  			args:   []string{"--cap-drop=all", "--cap-add=cap_net_raw"},
    99  			capEff: 1 << CapNetRaw,
   100  		},
   101  		{
   102  			args:   []string{"--cap-drop=all", "--cap-add=CAP_NET_RAW"},
   103  			capEff: 1 << CapNetRaw,
   104  		},
   105  	}
   106  	for _, tc := range testCases {
   107  		tc := tc // IMPORTANT
   108  		name := "default"
   109  		if len(tc.args) > 0 {
   110  			name = strings.Join(tc.args, "_")
   111  		}
   112  		t.Run(name, func(t *testing.T) {
   113  			t.Parallel()
   114  			got := getCapEff(base, tc.args...)
   115  			assert.Equal(t, tc.capEff, got)
   116  		})
   117  	}
   118  }
   119  
   120  func TestRunSecurityOptSeccomp(t *testing.T) {
   121  	t.Parallel()
   122  	base := testutil.NewBase(t)
   123  	type testCase struct {
   124  		args    []string
   125  		seccomp int
   126  	}
   127  	testCases := []testCase{
   128  		{
   129  			seccomp: 2,
   130  		},
   131  		{
   132  			args:    []string{"--security-opt", "seccomp=unconfined"},
   133  			seccomp: 0,
   134  		},
   135  		{
   136  			args:    []string{"--privileged"},
   137  			seccomp: 0,
   138  		},
   139  	}
   140  	for _, tc := range testCases {
   141  		tc := tc // IMPORTANT
   142  		name := "default"
   143  		if len(tc.args) > 0 {
   144  			name = strings.Join(tc.args, "_")
   145  		}
   146  		t.Run(name, func(t *testing.T) {
   147  			t.Parallel()
   148  			args := []string{"run", "--rm"}
   149  			args = append(args, tc.args...)
   150  			// NOTE: busybox grep does not support -oP \K
   151  			args = append(args, testutil.AlpineImage, "grep", "-Eo", `^Seccomp:\s*([0-9]+)`, "/proc/1/status")
   152  			cmd := base.Cmd(args...)
   153  			f := func(expectedSeccomp int) func(string) error {
   154  				return func(stdout string) error {
   155  					s := strings.TrimPrefix(stdout, "Seccomp:")
   156  					s = strings.TrimSpace(s)
   157  					i, err := strconv.Atoi(s)
   158  					if err != nil {
   159  						return fmt.Errorf("failed to parse line %q: %w", stdout, err)
   160  					}
   161  					if i != expectedSeccomp {
   162  						return fmt.Errorf("expected Seccomp to be %d, got %d", expectedSeccomp, i)
   163  					}
   164  					return nil
   165  				}
   166  			}
   167  			cmd.AssertOutWithFunc(f(tc.seccomp))
   168  		})
   169  	}
   170  }
   171  
   172  func TestRunApparmor(t *testing.T) {
   173  	base := testutil.NewBase(t)
   174  	defaultProfile := fmt.Sprintf("%s-default", base.Target)
   175  	if !apparmorutil.CanLoadNewProfile() && !apparmorutil.CanApplySpecificExistingProfile(defaultProfile) {
   176  		t.Skipf("needs to be able to apply %q profile", defaultProfile)
   177  	}
   178  	attrCurrentPath := "/proc/self/attr/apparmor/current"
   179  	if _, err := os.Stat(attrCurrentPath); err != nil {
   180  		attrCurrentPath = "/proc/self/attr/current"
   181  	}
   182  	attrCurrentEnforceExpected := fmt.Sprintf("%s (enforce)\n", defaultProfile)
   183  	base.Cmd("run", "--rm", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected)
   184  	base.Cmd("run", "--rm", "--security-opt", "apparmor="+defaultProfile, testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected)
   185  	base.Cmd("run", "--rm", "--security-opt", "apparmor=unconfined", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly("unconfined\n")
   186  	base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly("unconfined\n")
   187  }
   188  
   189  // TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976
   190  func TestRunSeccompCapSysPtrace(t *testing.T) {
   191  	base := testutil.NewBase(t)
   192  	base.Cmd("run", "--rm", "--cap-add", "sys_ptrace", testutil.AlpineImage, "sh", "-euxc", "apk add -q strace && strace true").AssertOK()
   193  	// Docker/Moby 's seccomp profile allows ptrace(2) by default, but containerd does not (yet): https://github.com/containerd/containerd/issues/6802
   194  }
   195  
   196  func TestRunPrivileged(t *testing.T) {
   197  	// docker does not support --privileged-without-host-devices
   198  	testutil.DockerIncompatible(t)
   199  
   200  	if rootlessutil.IsRootless() {
   201  		t.Skip("test skipped for rootless privileged containers")
   202  	}
   203  
   204  	base := testutil.NewBase(t)
   205  
   206  	devPath := "/dev/dummy-zero"
   207  
   208  	// a dummy zero device: mknod /dev/dummy-zero c 1 5
   209  	helperCmd := exec.Command("mknod", []string{devPath, "c", "1", "5"}...)
   210  	if out, err := helperCmd.CombinedOutput(); err != nil {
   211  		err = fmt.Errorf("cannot create %q: %q: %w", devPath, string(out), err)
   212  		t.Fatal(err)
   213  	}
   214  
   215  	// ensure the file will be removed in case of failed in the test
   216  	defer func() {
   217  		exec.Command("rm", devPath).Run()
   218  	}()
   219  
   220  	// get device with host devices
   221  	base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "ls", devPath).AssertOutExactly(devPath + "\n")
   222  
   223  	// get device without host devices
   224  	res := base.Cmd("run", "--rm", "--privileged", "--security-opt", "privileged-without-host-devices", testutil.AlpineImage, "ls", devPath).Run()
   225  
   226  	// normally for not a exists file, the `ls` will return `1``.
   227  	assert.Check(t, res.ExitCode != 0, res.Combined())
   228  
   229  	// something like `ls: /dev/dummy-zero: No such file or directory`
   230  	assert.Check(t, strings.Contains(res.Combined(), "No such file or directory"))
   231  }