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  }