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 }