From 9b33a267ec637d7d8a29259246033bfe1b5f47bc Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Wed, 8 Mar 2017 11:43:29 +1100 Subject: [PATCH 2/2] 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 --- daemon/start.go | 5 + daemon/suse_secrets.go | 260 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 daemon/suse_secrets.go diff --git a/daemon/start.go b/daemon/start.go index eddb5d3d5060..eb74e2ab1096 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -141,6 +141,11 @@ func (daemon *Daemon) containerStart(container *container.Container, checkpoint return err } + // SUSE:secrets -- inject the SUSE secret store + if err := daemon.injectSuseSecretStore(container); err != nil { + return err + } + spec, err := daemon.createSpec(container) if err != nil { return err diff --git a/daemon/suse_secrets.go b/daemon/suse_secrets.go new file mode 100644 index 000000000000..b577b7081976 --- /dev/null +++ b/daemon/suse_secrets.go @@ -0,0 +1,260 @@ +/* + * suse-secrets: patch for Docker to implement SUSE secrets + * Copyright (C) 2017 SUSE LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package daemon + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "syscall" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/container" + "github.com/docker/docker/pkg/idtools" + "github.com/opencontainers/go-digest" + + swarmtypes "github.com/docker/docker/api/types/swarm" + swarmexec "github.com/docker/swarmkit/agent/exec" + swarmapi "github.com/docker/swarmkit/api" +) + +func init() { + // Output to tell us in logs that SUSE:secrets is enabled. + logrus.Infof("SUSE:secrets :: enabled") +} + +// Creating a fake file. +type SuseFakeFile struct { + Path string + Uid int + Gid int + Mode os.FileMode + Data []byte +} + +func (s SuseFakeFile) id() string { + return fmt.Sprintf("suse::%s:%s", digest.FromBytes(s.Data), s.Path) +} + +func (s SuseFakeFile) toSecret() *swarmapi.Secret { + return &swarmapi.Secret{ + ID: s.id(), + Internal: true, + Spec: swarmapi.SecretSpec{ + Data: s.Data, + }, + } +} + +func (s SuseFakeFile) toSecretReference(uidMaps, gidMaps []idtools.IDMap) *swarmtypes.SecretReference { + // Figure out the host-facing {uid,gid} based on the provided maps. Fall + // back to root if the UID/GID don't match (we are guaranteed that root is + // mapped). + hostUid, hostGid, _ := idtools.GetRootUIDGID(uidMaps, gidMaps) + if uid, err := idtools.ToHost(s.Uid, uidMaps); err == nil { + hostUid = uid + } + if gid, err := idtools.ToHost(s.Gid, gidMaps); err == nil { + hostGid = gid + } + + // Return the secret reference as a file target. + return &swarmtypes.SecretReference{ + SecretID: s.id(), + SecretName: s.id(), + File: &swarmtypes.SecretReferenceFileTarget{ + Name: s.Path, + UID: fmt.Sprintf("%d", hostUid), + GID: fmt.Sprintf("%d", hostGid), + Mode: s.Mode, + }, + } +} + +// 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 +} + +// readFile returns a secret given a file under a given prefix. +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 +} + +// getHostSuseSecretData returns the list of SuseFakeFiles the need to be added +// as SUSE secrets. +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 +} + +// In order to reduce the amount of code touched outside of this file, we +// implement the swarm API for SecretGetter. This asserts that this requirement +// will always be matched. +var _ swarmexec.SecretGetter = &suseSecretGetter{} + +type suseSecretGetter struct { + dfl swarmexec.SecretGetter + secrets map[string]*swarmapi.Secret +} + +func (s *suseSecretGetter) Get(id string) *swarmapi.Secret { + logrus.Debugf("SUSE:secrets :: id=%s requested from suseSecretGetter", id) + + secret, ok := s.secrets[id] + if !ok { + // fallthrough + return s.dfl.Get(id) + } + + return secret +} + +func (daemon *Daemon) injectSuseSecretStore(c *container.Container) error { + newSecretStore := &suseSecretGetter{ + dfl: c.SecretStore, + secrets: make(map[string]*swarmapi.Secret), + } + + secrets, err := getHostSuseSecretData() + if err != nil { + return err + } + + uidMaps, gidMaps := daemon.GetUIDGIDMaps() + for _, secret := range secrets { + newSecretStore.secrets[secret.id()] = secret.toSecret() + c.SecretReferences = append(c.SecretReferences, secret.toSecretReference(uidMaps, gidMaps)) + } + + c.SecretStore = newSecretStore + return nil +} -- 2.14.1