diff --git a/container/container_unix.go b/container/container_unix.go index f92d586..70cc74f 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -39,6 +39,8 @@ type Container struct { HostsPath string ShmPath string ResolvConfPath string + // SUSE:secrets :: We need to add the container-specific secrets path here. + SuseSecretsPath string SeccompProfile string NoNewPrivileges bool } @@ -281,6 +283,67 @@ func (container *Container) UnmountSecrets() error { return detachMounted(container.SecretMountPath()) } +// SUSE:secrets :: SuseSecretsResourcePath returns the path to the container's +// personal /run/secrets tmpfs. +func (container *Container) SuseSecretsResourcePath() (string, error) { + return container.GetRootResourcePath("suse.secrets") +} + +// SUSE:secrets :: SuseSecretMounts returns the list of mounts required for the +// SUSE-specific /run/secrets patch. The container's personal /run/secrets tmpfs +// has already been set up at this point. +func (container *Container) SuseSecretMounts() []Mount { + var mounts []Mount + + logrus.WithFields(logrus.Fields{ + "container": container.ID, + "path": container.SuseSecretsPath, + "hasmount": container.HasMountFor("/run/secrets"), + }).Debug("SUSE:secrets :: adding container secrets to mountpoint") + + // TODO(SUSE): How do we register for HasMountFor(). + if !container.HasMountFor("/run/secrets") { + label.SetFileLabel(container.SuseSecretsPath, container.MountLabel) + mounts = append(mounts, Mount{ + Source: container.SuseSecretsPath, + Destination: "/run/secrets", + Writable: true, + Propagation: string(volume.DefaultPropagationMode), + }) + } + + return mounts +} + +// SUSE:secrets :: Unmounts the container's personal /run/secrets tmpfs using the +// provided function. This is done to clean up the mountpoints properly. +func (container *Container) UnmountSuseSecretMounts(unmount func(string) error) { + logrus.WithFields(logrus.Fields{ + "container": container.ID, + "hasmount": container.HasMountFor("/run/secrets"), + }).Debug("SUSE:secrets :: requested to clean up container secrets") + + if !container.HasMountFor("/run/secrets") { + logrus.Debugf("SUSE:secrets :: cleaning up secrets mount for container") + + suseSecretsPath, err := container.SuseSecretsResourcePath() + if err != nil { + logrus.Error("SUSE:secrets :: failed to clean up secrets mounts: no secrets resource path found for container %v: %v", container.ID, err) + } + + if suseSecretsPath != "" { + logrus.WithFields(logrus.Fields{ + "path": suseSecretsPath, + }).Debugf("SUSE:secrets :: actually unmounting conatiner secrets") + + if err := unmount(suseSecretsPath); err != nil && !os.IsNotExist(err) { + // We can't error out here. + logrus.Warnf("SUSE:secrets :: failed to clean up secrets mounts: failed to umount %s: %v", suseSecretsPath, err) + } + } + } +} + // UpdateContainer updates configuration of a container. func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error { container.Lock() diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go index 2296045..d6f7889 100644 --- a/daemon/container_operations_unix.go +++ b/daemon/container_operations_unix.go @@ -87,6 +87,56 @@ func (daemon *Daemon) getPidContainer(container *container.Container) (*containe return c, nil } +// SUSE:secrets :: Create a container's personal /run/secrets tmpfs and fill it +// with the host's credentials. +func (daemon *Daemon) setupSuseSecrets(c *container.Container) (err error) { + c.SuseSecretsPath, err = c.SuseSecretsResourcePath() + if err != nil { + return err + } + + if !c.HasMountFor("/run/secrets") { + rootUID, rootGID := daemon.GetRemappedUIDGID() + if err = idtools.MkdirAllAs(c.SuseSecretsPath, 0700, rootUID, rootGID); err != nil { + return fmt.Errorf("SUSE:secrets :: failed to create container secret: %v", err) + } + if err = syscall.Mount("tmpfs", c.SuseSecretsPath, "tmpfs", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), label.FormatMountLabel("", c.GetMountLabel())); err != nil { + return fmt.Errorf("SUSE:secrets :: mounting secrets tmpfs: %v", err) + } + // We need to defer a cleanup, to make sure errors that occur before the container + // starts don't cause wasted memory due to tmpfs-es that aren't being used. + defer func() { + if err != nil { + logrus.Infof("SUSE::secrets :: cleaning up secrets mount due to failed setup") + c.UnmountSuseSecretMounts(detachMounted) + } + }() + if err = os.Chown(c.SuseSecretsPath, rootUID, rootGID); err != nil { + return fmt.Errorf("SUSE:secrets :: failed to chown container secret to (uid=%d,gid=%d): %v", rootUID, rootGID, err) + } + + // Now we need to inject the credentials. But in order to play properly with + // user namespaces, they must be owned by rootUID:rootGID. + + data, err := getHostSuseSecretData() + if err != nil { + return fmt.Errorf("SUSE:secrets :: failed to get host secret data: %v", err) + } + + uidMap, gidMap := daemon.GetUIDGIDMaps() + for _, s := range data { + if err := s.SaveTo(c.SuseSecretsPath, uidMap, gidMap); err != nil { + logrus.WithFields(logrus.Fields{ + "s.path": s.Path, + "path": c.SuseSecretsPath, + }).Errorf("SUSE:secrets :: failed to save secret data: %v", err) + } + } + } + + return +} + func (daemon *Daemon) setupIpcDirs(c *container.Container) error { var err error diff --git a/daemon/initlayer/setup_unix.go b/daemon/initlayer/setup_unix.go index e83c275..8c56310 100644 --- a/daemon/initlayer/setup_unix.go +++ b/daemon/initlayer/setup_unix.go @@ -18,8 +18,10 @@ import ( // the container from unwanted side-effects on the rw layer. func Setup(initLayer string, rootUID, rootGID int) error { for pth, typ := range map[string]string{ - "/dev/pts": "dir", - "/dev/shm": "dir", + "/dev/pts": "dir", + "/dev/shm": "dir", + // SUSE:secrets :: We need to add the mountpoint in the init layer. + "/run/secrets": "dir", "/proc": "dir", "/sys": "dir", "/.dockerenv": "file", diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index 1daefc5..8e27f1d 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -693,6 +693,10 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { if err := daemon.setupSecretDir(c); err != nil { return nil, err } + // SUSE:secrets :: We need to set up the container-specific secrets tmpfs here. + if err := daemon.setupSuseSecrets(c); err != nil { + return nil, err + } ms, err := daemon.setupMounts(c) if err != nil { @@ -711,6 +715,9 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { ms = append(ms, *m) } + // SUSE:secrets :: We add the mounts to the OCI config which containerd then uses. + ms = append(ms, c.SuseSecretMounts()...) + sort.Sort(mounts(ms)) if err := setMounts(daemon, &s, c, ms); err != nil { return nil, fmt.Errorf("linux mounts: %v", err) diff --git a/daemon/start.go b/daemon/start.go index 6c94fd5..9bbdc21 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -205,6 +205,12 @@ func (daemon *Daemon) Cleanup(container *container.Container) { container.UnmountIpcMounts(detachMounted) + // TODO(SUSE): Make sure this gets called by containerCleanup. Do we need to + // port this part of the patch there as well? + + // SUSE:secrets :: We need to unmount stuff here so that we clean up properly. + container.UnmountSuseSecretMounts(detachMounted) + if err := daemon.conditionalUnmountOnCleanup(container); err != nil { // FIXME: remove once reference counting for graphdrivers has been refactored // Ensure that all the mounts are gone diff --git a/daemon/suse_secrets.go b/daemon/suse_secrets.go new file mode 100644 index 0000000..417a1a9 --- /dev/null +++ b/b/daemon/suse_secrets.go @@ -0,0 +1,184 @@ +package daemon + +// SUSE:secrets :: This is a set of functions to copy host credentials into a +// container's /run/secrets. + +import ( + "io/ioutil" + "os" + "path/filepath" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/idtools" +) + +// TODO(SUSE): We need to reimplement this to use tar. Immediately. + +// Creating a fake file. +type SuseFakeFile struct { + Path string + Uid int + Gid int + Mode os.FileMode + Data []byte +} + +func (s *SuseFakeFile) SaveTo(dir string, uidMap, gidMap []idtools.IDMap) error { + // Create non-existant path components with an owner of root (other FakeFiles + // will clean this up if the owner is critical). + rootUid, rootGid, err := idtools.GetRootUIDGID(uidMap, gidMap) + + path := filepath.Join(dir, s.Path) + if err := idtools.MkdirAllNewAs(filepath.Dir(path), 0755, rootUid, rootGid); err != nil && !os.IsExist(err) { + return err + } + + uid, err := idtools.ToHost(s.Uid, uidMap) + if err != nil { + return err + } + + gid, err := idtools.ToHost(s.Gid, gidMap) + if err != nil { + return err + } + + if s.Mode.IsDir() { + if err := idtools.MkdirAs(path, s.Mode, uid, gid); err != nil { + return err + } + } else { + if err := ioutil.WriteFile(path, s.Data, s.Mode); err != nil { + return err + } + } + + return os.Chown(path, uid, gid) +} + +// readDir will recurse into a directory prefix/dir, and return the set of secrets +// in that directory. The Path attribute of each has the prefix stripped. Symlinks +// are evaluated. +func readDir(prefix, dir string) ([]*SuseFakeFile, error) { + var suseFiles []*SuseFakeFile + + path := filepath.Join(prefix, dir) + + fi, err := os.Stat(path) + if err != nil { + // Ignore dangling symlinks. + if os.IsNotExist(err) { + logrus.Warnf("SUSE:secrets :: dangling symlink: %s", path) + return suseFiles, nil + } + return nil, err + } + + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + logrus.Warnf("SUSE:secrets :: failed to cast directory stat_t: defaulting to owned by root:root: %s", path) + } + + suseFiles = append(suseFiles, &SuseFakeFile{ + Path: dir, + Uid: int(stat.Uid), + Gid: int(stat.Gid), + Mode: fi.Mode(), + }) + + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + for _, f := range files { + subpath := filepath.Join(dir, f.Name()) + + if f.IsDir() { + secrets, err := readDir(prefix, subpath) + if err != nil { + return nil, err + } + suseFiles = append(suseFiles, secrets...) + } else { + secrets, err := readFile(prefix, subpath) + if err != nil { + return nil, err + } + suseFiles = append(suseFiles, secrets...) + } + } + + return suseFiles, nil +} + +func readFile(prefix, file string) ([]*SuseFakeFile, error) { + var suseFiles []*SuseFakeFile + + path := filepath.Join(prefix, file) + fi, err := os.Stat(path) + if err != nil { + // Ignore dangling symlinks. + if os.IsNotExist(err) { + logrus.Warnf("SUSE:secrets :: dangling symlink: %s", path) + return suseFiles, nil + } + return nil, err + } + + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + logrus.Warnf("SUSE:secrets :: failed to cast file stat_t: defaulting to owned by root:root: %s", path) + } + + if fi.IsDir() { + secrets, err := readDir(prefix, file) + if err != nil { + return nil, err + } + suseFiles = append(suseFiles, secrets...) + } else { + bytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + suseFiles = append(suseFiles, &SuseFakeFile{ + Path: file, + Uid: int(stat.Uid), + Gid: int(stat.Gid), + Mode: fi.Mode(), + Data: bytes, + }) + } + + return suseFiles, nil +} + +func getHostSuseSecretData() ([]*SuseFakeFile, error) { + secrets := []*SuseFakeFile{} + + credentials, err := readDir("/etc/zypp", "credentials.d") + if err != nil { + if os.IsNotExist(err) { + credentials = []*SuseFakeFile{} + } else { + logrus.Errorf("SUSE:secrets :: error while reading zypp credentials: %s", err) + return nil, err + } + } + secrets = append(secrets, credentials...) + + suseConnect, err := readFile("/etc", "SUSEConnect") + if err != nil { + if os.IsNotExist(err) { + suseConnect = []*SuseFakeFile{} + } else { + logrus.Errorf("SUSE:secrets :: error while reading /etc/SUSEConnect: %s", err) + return nil, err + } + } + secrets = append(secrets, suseConnect...) + + return secrets, nil +}