github.com/cloudfoundry-attic/garden-linux@v0.333.2-candidate/integration/lifecycle/security_test.go (about) 1 package lifecycle_test 2 3 import ( 4 "fmt" 5 "os" 6 "path" 7 "path/filepath" 8 "strings" 9 "syscall" 10 "time" 11 12 "github.com/cloudfoundry-incubator/garden" 13 14 "os/exec" 15 16 "io" 17 "io/ioutil" 18 19 . "github.com/onsi/ginkgo" 20 . "github.com/onsi/gomega" 21 "github.com/onsi/gomega/gbytes" 22 "github.com/onsi/gomega/gexec" 23 ) 24 25 var _ = Describe("Security", func() { 26 Describe("PID namespace", func() { 27 It("does not keep any host files open", func() { 28 client = startGarden() 29 container, err := client.Create(garden.ContainerSpec{}) 30 Expect(err).ToNot(HaveOccurred()) 31 32 ps, err := gexec.Start( 33 exec.Command("sh", "-c", 34 fmt.Sprintf("ps -A -opid,args | grep wshd | grep %s | head -n 1 | awk '{ print $1 }'", container.Handle())), 35 GinkgoWriter, GinkgoWriter) 36 Expect(err).ToNot(HaveOccurred()) 37 Eventually(ps).Should(gexec.Exit(0)) 38 39 lsof, err := gexec.Start( 40 // List all files 41 exec.Command( 42 "lsof", "-a", 43 // open by a specific process id (initd) 44 "-p", strings.TrimSpace(string(ps.Out.Contents())), 45 // AND their FDs are not txt or mem 46 "-d", "^txt,^mem", 47 // AND they are found in the host mount namespace 48 "/", 49 ), 50 GinkgoWriter, GinkgoWriter) 51 52 Eventually(lsof.Wait()).Should(gexec.Exit()) 53 Expect(lsof.Out).To(gbytes.Say(`\A\z`)) 54 }) 55 }) 56 57 Describe("Mount namespace", func() { 58 It("does not allow mounts in the container to show in the host", func() { 59 client = startGarden() 60 container, err := client.Create(garden.ContainerSpec{Privileged: true}) 61 Expect(err).ToNot(HaveOccurred()) 62 63 process, err := container.Run(garden.ProcessSpec{ 64 User: "alice", 65 Path: "/bin/mkdir", 66 Args: []string{"/home/alice/lawn"}, 67 }, garden.ProcessIO{ 68 Stdout: GinkgoWriter, 69 Stderr: GinkgoWriter, 70 }) 71 Expect(err).ToNot(HaveOccurred()) 72 exitStatus, err := process.Wait() 73 Expect(err).ToNot(HaveOccurred()) 74 Expect(exitStatus).To(Equal(0)) 75 76 process, err = container.Run(garden.ProcessSpec{ 77 User: "alice", 78 Path: "/bin/mkdir", 79 Args: []string{"/home/alice/gnome"}, 80 }, garden.ProcessIO{ 81 Stdout: GinkgoWriter, 82 Stderr: GinkgoWriter, 83 }) 84 Expect(err).ToNot(HaveOccurred()) 85 exitStatus, err = process.Wait() 86 Expect(err).ToNot(HaveOccurred()) 87 Expect(exitStatus).To(Equal(0)) 88 89 process, err = container.Run(garden.ProcessSpec{ 90 User: "root", 91 Path: "/bin/mount", 92 Args: []string{"--bind", "/home/alice/lawn", "/home/alice/gnome"}, 93 }, garden.ProcessIO{ 94 Stdout: GinkgoWriter, 95 Stderr: GinkgoWriter, 96 }) 97 Expect(err).ToNot(HaveOccurred()) 98 exitStatus, err = process.Wait() 99 Expect(err).ToNot(HaveOccurred()) 100 Expect(exitStatus).To(Equal(0)) 101 102 stdout := gbytes.NewBuffer() 103 process, err = container.Run(garden.ProcessSpec{ 104 User: "root", 105 Path: "/bin/cat", 106 Args: []string{"/proc/mounts"}, 107 }, garden.ProcessIO{ 108 Stdout: stdout, 109 Stderr: GinkgoWriter, 110 }) 111 Expect(err).ToNot(HaveOccurred()) 112 113 exitStatus, err = process.Wait() 114 Expect(err).ToNot(HaveOccurred()) 115 Expect(exitStatus).To(Equal(0)) 116 117 Expect(stdout).To(gbytes.Say(`gnome`)) 118 119 cat := exec.Command("/bin/cat", "/proc/mounts") 120 catSession, err := gexec.Start(cat, GinkgoWriter, GinkgoWriter) 121 Expect(err).ToNot(HaveOccurred()) 122 Eventually(catSession).Should(gexec.Exit(0)) 123 Expect(catSession).ToNot(gbytes.Say("gnome")) 124 }) 125 }) 126 127 Describe("Network namespace", func() { 128 It("does not allow network configuration in the container to show in the host", func() { 129 client = startGarden() 130 container, err := client.Create(garden.ContainerSpec{Privileged: true}) 131 Expect(err).ToNot(HaveOccurred()) 132 133 process, err := container.Run(garden.ProcessSpec{ 134 User: "root", 135 Path: "ifconfig", 136 Args: []string{"lo:0", "1.2.3.4", "up"}, 137 }, garden.ProcessIO{ 138 Stdout: GinkgoWriter, 139 Stderr: GinkgoWriter, 140 }) 141 Expect(err).ToNot(HaveOccurred()) 142 exitStatus, err := process.Wait() 143 Expect(err).ToNot(HaveOccurred()) 144 Expect(exitStatus).To(Equal(0)) 145 146 stdout := gbytes.NewBuffer() 147 process, err = container.Run(garden.ProcessSpec{ 148 User: "root", 149 Path: "ifconfig", 150 }, garden.ProcessIO{ 151 Stdout: stdout, 152 Stderr: GinkgoWriter, 153 }) 154 Expect(err).ToNot(HaveOccurred()) 155 156 exitStatus, err = process.Wait() 157 Expect(err).ToNot(HaveOccurred()) 158 Expect(exitStatus).To(Equal(0)) 159 160 Expect(stdout).To(gbytes.Say(`lo:0`)) 161 162 cat := exec.Command("ifconfig") 163 catSession, err := gexec.Start(cat, GinkgoWriter, GinkgoWriter) 164 Expect(err).ToNot(HaveOccurred()) 165 Eventually(catSession).Should(gexec.Exit(0)) 166 Expect(catSession).ToNot(gbytes.Say("lo:0")) 167 }) 168 }) 169 170 Describe("IPC namespace", func() { 171 var sharedDir string 172 var container garden.Container 173 174 BeforeEach(func() { 175 var err error 176 sharedDir, err = ioutil.TempDir("", "shared-mount") 177 Expect(err).ToNot(HaveOccurred()) 178 Expect(os.MkdirAll(sharedDir, 0755)).To(Succeed()) 179 }) 180 181 AfterEach(func() { 182 if container != nil { 183 Expect(client.Destroy(container.Handle())).To(Succeed()) 184 } 185 if sharedDir != "" { 186 Expect(os.RemoveAll(sharedDir)).To(Succeed()) 187 } 188 }) 189 190 It("does not allow shared memory segments in the host to be accessed by the container", func() { 191 Expect(copyFile(shmTestBin, path.Join(sharedDir, "shm_test"))).To(Succeed()) 192 193 client = startGarden() 194 var err error 195 container, err = client.Create(garden.ContainerSpec{ 196 Privileged: true, 197 BindMounts: []garden.BindMount{{ 198 SrcPath: sharedDir, 199 DstPath: "/mnt/shared", 200 }}, 201 }) 202 Expect(err).ToNot(HaveOccurred()) 203 204 // Create shared memory segment in the host. 205 localSHM := exec.Command(shmTestBin) 206 createLocal, err := gexec.Start( 207 localSHM, 208 GinkgoWriter, 209 GinkgoWriter, 210 ) 211 Expect(err).ToNot(HaveOccurred()) 212 Eventually(createLocal).Should(gbytes.Say("ok")) 213 214 // Create shared memory segment in the container. 215 // If there is no IPC namespace, this will collide with the segment in the host and fail. 216 stdout := gbytes.NewBuffer() 217 _, err = container.Run(garden.ProcessSpec{ 218 User: "root", 219 Path: "/mnt/shared/shm_test", 220 }, garden.ProcessIO{ 221 Stdout: stdout, 222 Stderr: GinkgoWriter, 223 }) 224 Expect(err).ToNot(HaveOccurred()) 225 Eventually(stdout).Should(gbytes.Say("ok")) 226 227 localSHM.Process.Signal(syscall.SIGUSR2) 228 229 Eventually(createLocal).Should(gexec.Exit(0)) 230 231 }) 232 }) 233 234 Describe("UTS namespace", func() { 235 It("changing the container's hostname does not affect the host's hostname", func() { 236 client = startGarden() 237 container, err := client.Create(garden.ContainerSpec{Privileged: true}) 238 Expect(err).ToNot(HaveOccurred()) 239 240 process, err := container.Run(garden.ProcessSpec{ 241 User: "root", 242 Path: "/bin/hostname", 243 Args: []string{"newhostname"}, 244 }, garden.ProcessIO{ 245 Stdout: GinkgoWriter, 246 Stderr: GinkgoWriter, 247 }) 248 Expect(err).ToNot(HaveOccurred()) 249 exitStatus, err := process.Wait() 250 Expect(err).ToNot(HaveOccurred()) 251 Expect(exitStatus).To(Equal(0)) 252 253 stdout := gbytes.NewBuffer() 254 process, err = container.Run(garden.ProcessSpec{ 255 User: "root", 256 Path: "/bin/hostname", 257 }, garden.ProcessIO{ 258 Stdout: stdout, 259 Stderr: GinkgoWriter, 260 }) 261 Expect(err).ToNot(HaveOccurred()) 262 263 exitStatus, err = process.Wait() 264 Expect(err).ToNot(HaveOccurred()) 265 Expect(exitStatus).To(Equal(0)) 266 Expect(stdout).To(gbytes.Say(`newhostname`)) 267 268 localHostname := exec.Command("hostname") 269 localHostnameSession, err := gexec.Start(localHostname, GinkgoWriter, GinkgoWriter) 270 Eventually(localHostnameSession).Should(gexec.Exit(0)) 271 Expect(localHostnameSession).ToNot(gbytes.Say("newhostname")) 272 }) 273 }) 274 275 Context("with an empty rootfs", func() { 276 var ( 277 rootFSPath string 278 container garden.Container 279 ) 280 281 BeforeEach(func() { 282 rootFSPath = os.Getenv("GARDEN_EMPTY_TEST_ROOTFS") 283 if rootFSPath == "" { 284 Skip("GARDEN_EMPTY_TEST_ROOTFS undefined") 285 } 286 287 client = startGarden() 288 }) 289 290 JustBeforeEach(func() { 291 var err error 292 293 container, err = client.Create( 294 garden.ContainerSpec{ 295 RootFSPath: rootFSPath, 296 }, 297 ) 298 Expect(err).ToNot(HaveOccurred()) 299 }) 300 301 It("runs a statically compiled executable in the container", func() { 302 stdout := gbytes.NewBuffer() 303 stderr := gbytes.NewBuffer() 304 process, err := container.Run( 305 garden.ProcessSpec{ 306 User: "alice", 307 Path: "/hello", 308 Dir: "/", 309 }, 310 garden.ProcessIO{ 311 Stdout: stdout, 312 Stderr: stderr, 313 }, 314 ) 315 Expect(err).ToNot(HaveOccurred()) 316 317 exitStatus, err := process.Wait() 318 Expect(err).ToNot(HaveOccurred()) 319 Expect(exitStatus).To(Equal(0)) 320 321 Expect(string(stdout.Contents())).To(Equal("hello from stdout")) 322 Expect(string(stderr.Contents())).To(Equal("hello from stderr")) 323 }) 324 325 Context("that has a list command", func() { 326 BeforeEach(func() { 327 var err error 328 329 tempRootFSPath, err := ioutil.TempDir("", "") 330 Expect(err).ToNot(HaveOccurred()) 331 cmd := exec.Command("bash", "-c", fmt.Sprintf("cp -R %s/* %s", rootFSPath, tempRootFSPath)) 332 Expect(cmd.Run()).To(Succeed()) 333 rootFSPath = tempRootFSPath 334 335 lsPath, err := gexec.Build("github.com/cloudfoundry-incubator/garden-linux/integration/test-images/empty_ls") 336 Expect(err).ToNot(HaveOccurred()) 337 cmd = exec.Command("cp", lsPath, path.Join(rootFSPath, "ls")) 338 Expect(cmd.Run()).To(Succeed()) 339 }) 340 341 AfterEach(func() { 342 os.RemoveAll(rootFSPath) 343 }) 344 345 It("should only list known files and directories", func() { 346 stdout := gbytes.NewBuffer() 347 process, err := container.Run( 348 garden.ProcessSpec{ 349 User: "alice", 350 Path: "/ls", 351 Dir: "/", 352 }, 353 garden.ProcessIO{ 354 Stdout: stdout, 355 Stderr: GinkgoWriter, 356 }, 357 ) 358 Expect(err).ToNot(HaveOccurred()) 359 360 exitStatus, err := process.Wait() 361 Expect(err).ToNot(HaveOccurred()) 362 Expect(exitStatus).To(Equal(0)) 363 Expect(string(stdout.Contents())).To(Equal(`dev 364 etc 365 hello 366 ls 367 proc 368 sys 369 tmp 370 `)) 371 }) 372 }) 373 }) 374 375 Describe("Denying access to network ranges", func() { 376 var ( 377 blockedListener garden.Container 378 blockedListenerIP string = fmt.Sprintf("11.0.%d.2", GinkgoParallelNode()) 379 380 unblockedListener garden.Container 381 unblockedListenerIP string = fmt.Sprintf("11.1.%d.2", GinkgoParallelNode()) 382 383 allowedListener garden.Container 384 allowedListenerIP string = fmt.Sprintf("11.2.%d.2", GinkgoParallelNode()) 385 386 sender garden.Container 387 ) 388 389 BeforeEach(func() { 390 client = startGarden( 391 "-denyNetworks", strings.Join([]string{ 392 blockedListenerIP + "/32", 393 allowedListenerIP + "/32", 394 }, ","), 395 "-allowNetworks", allowedListenerIP+"/32", 396 ) 397 398 var err error 399 400 // create a listener to which we deny network access 401 blockedListener, err = client.Create(garden.ContainerSpec{Network: blockedListenerIP + "/30"}) 402 Expect(err).ToNot(HaveOccurred()) 403 blockedListenerIP = containerIP(blockedListener) 404 405 // create a listener to which we do not deny access 406 unblockedListener, err = client.Create(garden.ContainerSpec{Network: unblockedListenerIP + "/30"}) 407 Expect(err).ToNot(HaveOccurred()) 408 unblockedListenerIP = containerIP(unblockedListener) 409 410 // create a listener to which we exclicitly allow access 411 allowedListener, err = client.Create(garden.ContainerSpec{Network: allowedListenerIP + "/30"}) 412 Expect(err).ToNot(HaveOccurred()) 413 allowedListenerIP = containerIP(allowedListener) 414 415 // create a container with the new deny network configuration 416 sender, err = client.Create(garden.ContainerSpec{}) 417 Expect(err).ToNot(HaveOccurred()) 418 419 }) 420 421 AfterEach(func() { 422 err := client.Destroy(sender.Handle()) 423 Expect(err).ToNot(HaveOccurred()) 424 425 err = client.Destroy(blockedListener.Handle()) 426 Expect(err).ToNot(HaveOccurred()) 427 428 err = client.Destroy(unblockedListener.Handle()) 429 Expect(err).ToNot(HaveOccurred()) 430 431 err = client.Destroy(allowedListener.Handle()) 432 Expect(err).ToNot(HaveOccurred()) 433 }) 434 435 runInContainer := func(container garden.Container, script string) garden.Process { 436 process, err := container.Run(garden.ProcessSpec{ 437 User: "alice", 438 Path: "sh", 439 Args: []string{"-c", script}, 440 }, garden.ProcessIO{ 441 Stdout: GinkgoWriter, 442 Stderr: GinkgoWriter, 443 }) 444 Expect(err).ToNot(HaveOccurred()) 445 446 return process 447 } 448 449 It("makes that block of ip addresses inaccessible to the container", func() { 450 runInContainer(blockedListener, "nc -l 0.0.0.0:12345") 451 runInContainer(unblockedListener, "nc -l 0.0.0.0:12345") 452 runInContainer(allowedListener, "nc -l 0.0.0.0:12345") 453 454 // a bit of time for the listeners to start, since they block 455 time.Sleep(time.Second) 456 457 process := runInContainer( 458 sender, 459 fmt.Sprintf("echo hello | nc -w 1 %s 12345", blockedListenerIP), 460 ) 461 Expect(process.Wait()).To(Equal(1)) 462 463 process = runInContainer( 464 sender, 465 fmt.Sprintf("echo hello | nc -w 1 %s 12345", unblockedListenerIP), 466 ) 467 Expect(process.Wait()).To(Equal(0)) 468 469 process = runInContainer( 470 sender, 471 fmt.Sprintf("echo hello | nc -w 1 %s 12345", allowedListenerIP), 472 ) 473 Expect(process.Wait()).To(Equal(0)) 474 }) 475 }) 476 477 Describe("Rootfs with symlinks", func() { 478 var rootfsPath string 479 480 BeforeEach(func() { 481 client = startGarden() 482 483 var err error 484 rootfsPath, err = ioutil.TempDir("", "") 485 Expect(err).NotTo(HaveOccurred()) 486 }) 487 488 AfterEach(func() { 489 Expect(os.RemoveAll(rootfsPath)).To(Succeed()) 490 }) 491 492 Context("when symlinking /etc/hosts to a file in the host", func() { 493 var ( 494 hostFilePath string 495 ) 496 497 BeforeEach(func() { 498 srcPath := filepath.Join(rootfsPath, "/etc/hosts") 499 Expect(os.MkdirAll(filepath.Dir(srcPath), 0777)).To(Succeed()) 500 501 hostFile, err := ioutil.TempFile("", "") 502 Expect(err).NotTo(HaveOccurred()) 503 defer hostFile.Close() 504 hostFilePath = hostFile.Name() 505 506 Expect(os.Symlink(hostFilePath, srcPath)).To(Succeed()) 507 }) 508 509 AfterEach(func() { 510 Expect(os.Remove(hostFilePath)).To(Succeed()) 511 }) 512 513 It("should not write in the host file", func() { 514 _, err := client.Create(garden.ContainerSpec{ 515 RootFSPath: rootfsPath, 516 }) 517 Expect(err).NotTo(HaveOccurred()) 518 519 fi, err := os.Stat(hostFilePath) 520 Expect(err).NotTo(HaveOccurred()) 521 522 Expect(fi.Size()).To(BeZero()) 523 }) 524 }) 525 526 Context("when symlinking /sys to a directory in the host", func() { 527 var ( 528 hostDirPath string 529 ) 530 531 BeforeEach(func() { 532 srcPath := filepath.Join(rootfsPath, "proc") 533 534 var err error 535 hostDirPath, err = ioutil.TempDir("", "") 536 Expect(err).NotTo(HaveOccurred()) 537 538 Expect(os.Symlink(hostDirPath, srcPath)).To(Succeed()) 539 }) 540 541 AfterEach(func() { 542 Expect(os.RemoveAll(hostDirPath)).To(Succeed()) 543 }) 544 545 It("should not change the ownership of the host directory", func() { 546 client.Create(garden.ContainerSpec{ 547 RootFSPath: rootfsPath, 548 }) 549 550 fi, err := os.Stat(hostDirPath) 551 Expect(err).NotTo(HaveOccurred()) 552 553 Expect(fi.Sys().(*syscall.Stat_t).Uid).To(BeNumerically("==", os.Getuid())) 554 }) 555 }) 556 }) 557 }) 558 559 func copyFile(src, dst string) error { 560 s, err := os.Open(src) 561 if err != nil { 562 return err 563 } 564 565 defer s.Close() 566 567 d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, 0755) 568 if err != nil { 569 return err 570 } 571 572 _, err = io.Copy(d, s) 573 if err != nil { 574 d.Close() 575 return err 576 } 577 578 return d.Close() 579 }