414 lines
13 KiB
Diff
414 lines
13 KiB
Diff
From 17cd15ba4160f0e0830453529b9b01edc308d847 Mon Sep 17 00:00:00 2001
|
|
From: Aleksa Sarai <asarai@suse.de>
|
|
Date: Mon, 11 Apr 2016 22:54:35 +1000
|
|
Subject: [PATCH] SUSE: implement SUSE container secrets
|
|
|
|
This allows for us to pass in host credentials to a container, allowing
|
|
for SUSEConnect to work with containers.
|
|
|
|
THIS PATCH IS NOT TO BE UPSTREAMED, DUE TO THE FACT THAT IT IS
|
|
SUSE-SPECIFIC, AND UPSTREAM DOES NOT APPROVE OF THIS CONCEPT BECAUSE IT
|
|
MAKES BUILDS NOT ENTIRELY REPRODUCIBLE.
|
|
|
|
Signed-off-by: Aleksa Sarai <asarai@suse.de>
|
|
---
|
|
container/container_unix.go | 63 ++++++++++++
|
|
daemon/container_operations_unix.go | 50 ++++++++++
|
|
daemon/daemon_unix.go | 6 +-
|
|
daemon/oci_linux.go | 6 ++
|
|
daemon/start.go | 6 ++
|
|
daemon/suse_secrets.go | 184 ++++++++++++++++++++++++++++++++++++
|
|
6 files changed, 313 insertions(+), 2 deletions(-)
|
|
create mode 100644 daemon/suse_secrets.go
|
|
|
|
diff --git a/container/container_unix.go b/container/container_unix.go
|
|
index 2727b81..07a0710 100644
|
|
--- a/container/container_unix.go
|
|
+++ b/container/container_unix.go
|
|
@@ -35,6 +35,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
|
|
}
|
|
@@ -256,6 +258,67 @@ func (container *Container) IpcMounts() []Mount {
|
|
return mounts
|
|
}
|
|
|
|
+// 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: 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 55bd3fc..a3ab7fb 100644
|
|
--- a/daemon/container_operations_unix.go
|
|
+++ b/daemon/container_operations_unix.go
|
|
@@ -184,6 +184,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/daemon_unix.go b/daemon/daemon_unix.go
|
|
index f266912..2ced1b8 100644
|
|
--- a/daemon/daemon_unix.go
|
|
+++ b/daemon/daemon_unix.go
|
|
@@ -809,8 +809,10 @@ func initBridgeDriver(controller libnetwork.NetworkController, config *Config) e
|
|
// the container from unwanted side-effects on the rw layer.
|
|
func setupInitLayer(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 4459d02..6af7d35 100644
|
|
--- a/daemon/oci_linux.go
|
|
+++ b/daemon/oci_linux.go
|
|
@@ -656,6 +656,10 @@ func (daemon *Daemon) createSpec(c *container.Container) (*libcontainerd.Spec, e
|
|
if err := daemon.setupIpcDirs(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 {
|
|
@@ -663,6 +667,8 @@ func (daemon *Daemon) createSpec(c *container.Container) (*libcontainerd.Spec, e
|
|
}
|
|
ms = append(ms, c.IpcMounts()...)
|
|
ms = append(ms, c.TmpfsMounts()...)
|
|
+ // 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 fcf24c5..57a8c33 100644
|
|
--- a/daemon/start.go
|
|
+++ b/daemon/start.go
|
|
@@ -173,6 +173,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/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
|
|
+}
|