From ad34d7a3fd838ff43e1da9e82caaa37917382eb4 Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Sun, 28 Oct 2018 14:31:19 +0100 Subject: [PATCH] Manage config files hostname, hosts, resolv.conf outside of container's rootfs and bind mount them read-only into the container. (to prevent those runtime-related changes from being included into a container image on commit) Signed-off-by: Max Goltzsche --- README.md | 2 +- bundle/bundlebuilder.go | 50 ++++++++++++++++++++++++++++++------- bundle/store/bundlestore.go | 2 +- cmd/net.go | 4 ++- model/oci/ocitransform.go | 9 ++++--- net/configbuilder.go | 23 ++++++++--------- pkg/generate/generate.go | 1 + 7 files changed, 64 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 14349c5..648b7eb 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ to either use an external runc binary or use libcontainer (no runtime dependenci - health check - improved Docker Compose support - service discovery integration (hook / DNS; consul, etcd) -- daemon mode +- detached mode - systemd integration (cgroup, startup notification) - **1.0 release** - advanced logging diff --git a/bundle/bundlebuilder.go b/bundle/bundlebuilder.go index c20ef4a..1a28321 100644 --- a/bundle/bundlebuilder.go +++ b/bundle/bundlebuilder.go @@ -1,21 +1,26 @@ package bundle import ( + "os" "path/filepath" + "github.com/cyphar/filepath-securejoin" "github.com/mgoltzsche/ctnr/pkg/generate" + "github.com/openSUSE/umoci/pkg/fseval" + "github.com/pkg/errors" ) type BundleBuilder struct { id string *generate.SpecBuilder - image BundleImage + image BundleImage + managedFiles map[string]bool } func Builder(id string) *BundleBuilder { specgen := generate.NewSpecBuilder() specgen.SetRootPath("rootfs") - b := &BundleBuilder{"", &specgen, nil} + b := &BundleBuilder{"", &specgen, nil, map[string]bool{}} b.SetID(id) return b } @@ -29,28 +34,55 @@ func (b *BundleBuilder) SetID(id string) { b.AddAnnotation(ANNOTATION_BUNDLE_ID, id) } -func (b *BundleBuilder) GetID() string { - return b.id -} - func (b *BundleBuilder) SetImage(image BundleImage) { b.ApplyImage(image.Config()) b.image = image } +// Overlays the provided file path with a bind mounted read-only copy. +// The file's content is supposed to be managed by an OCI hook. +func (b *BundleBuilder) AddBindMountConfig(path string) { + path = filepath.Clean(path) + opts := []string{"bind", "mode=0444", "nosuid", "noexec", "nodev", "ro"} + b.managedFiles[path] = true + b.AddBindMount(filepath.Join("mount", path), path, opts) +} + func (b *BundleBuilder) Build(bundle *LockedBundle) (err error) { // Prepare rootfs if err = bundle.UpdateRootfs(b.image); err != nil { - return + return errors.Wrap(err, "build bundle") + } + + // Generate managed config files + for path := range b.managedFiles { + if err = b.touchManagedFile(bundle.Dir(), path); err != nil { + return errors.Wrap(err, "build bundle") + } } // Resolve user/group names rootfs := filepath.Join(bundle.Dir(), b.Generator.Spec().Root.Path) spec, err := b.Spec(rootfs) if err != nil { - return + return errors.Wrap(err, "build bundle") } // Apply spec - return bundle.SetSpec(spec) + return errors.Wrap(bundle.SetSpec(spec), "build bundle") +} + +func (b *BundleBuilder) touchManagedFile(bundleDir, path string) (err error) { + file, err := securejoin.SecureJoinVFS(filepath.Join(bundleDir, "mount"), path, fseval.RootlessFsEval) + if err != nil { + return + } + if err = os.MkdirAll(filepath.Dir(file), 0755); err != nil { + return + } + f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return + } + return f.Close() } diff --git a/bundle/store/bundlestore.go b/bundle/store/bundlestore.go index 27221fa..8fad600 100644 --- a/bundle/store/bundlestore.go +++ b/bundle/store/bundlestore.go @@ -50,7 +50,7 @@ func (s *BundleStore) Bundle(id string) (r bundle.Bundle, err error) { func (s *BundleStore) CreateBundle(id string, update bool) (b *bundle.LockedBundle, err error) { dir := filepath.Join(s.dir, id) if id == "" { - if err = os.MkdirAll(s.dir, 0770); err != nil { + if err = os.MkdirAll(s.dir, 0750); err != nil { return nil, errors.Wrap(err, "create bundle") } if dir, err = ioutil.TempDir(s.dir, ""); err != nil { diff --git a/cmd/net.go b/cmd/net.go index a002054..b62d2ed 100644 --- a/cmd/net.go +++ b/cmd/net.go @@ -101,7 +101,9 @@ func runNetInit(cmd *cobra.Command, args []string) (err error) { // Generate hostname, hosts, resolv.conf files cfg.SetHostname(spec.Hostname) applyArgs(&cfg) - return cfg.WriteConfigFiles(filepath.Join(state.Bundle, spec.Root.Path)) + rootfs := filepath.Join(state.Bundle, spec.Root.Path) + mounts := filepath.Join(state.Bundle, "mount") + return cfg.WriteConfigFiles(rootfs, mounts) } func runNetRemove(cmd *cobra.Command, args []string) (err error) { diff --git a/model/oci/ocitransform.go b/model/oci/ocitransform.go index 6695a51..2065d55 100644 --- a/model/oci/ocitransform.go +++ b/model/oci/ocitransform.go @@ -136,20 +136,23 @@ func ToSpec(service *model.Service, res model.ResourceResolver, rootless bool, p return errors.New("transform: no networks supported in rootless mode") } - // Use host networks by removing 'network' namespace + // Use host network by removing 'network' namespace if useHostNetwork { spec.UseHostNetwork() } else { spec.AddOrReplaceLinuxNamespace(specs.NetworkNamespace, "") } - // Add hostname. Empty string results in host's hostname - if service.Hostname != "" || useHostNetwork { + // Add hostname + if service.Hostname != "" { spec.SetHostname(service.Hostname) } // Add network hook if len(networks) > 0 { + spec.AddBindMountConfig("/etc/hostname") + spec.AddBindMountConfig("/etc/hosts") + spec.AddBindMountConfig("/etc/resolv.conf") hook, err := generate.NewHookBuilderFromSpec(sp) if err != nil { return err diff --git a/net/configbuilder.go b/net/configbuilder.go index 85e9b1d..fed95f8 100644 --- a/net/configbuilder.go +++ b/net/configbuilder.go @@ -81,17 +81,10 @@ func (b *ConfigFileGenerator) AddDnsOptions(opts []string) { } } -func (b *ConfigFileGenerator) WriteConfigFiles(rootfs string) error { - // Create /etc dir in bundle's rootfs - etcDir := filepath.Join(rootfs, "etc") - if _, err := os.Stat(etcDir); os.IsNotExist(err) { - if err = os.Mkdir(etcDir, 0755); err != nil { - return err - } - } - hostnameFile := filepath.Join(etcDir, "hostname") - hostsFile := filepath.Join(etcDir, "hosts") - resolvConfFile := filepath.Join(etcDir, "resolv.conf") +func (b *ConfigFileGenerator) WriteConfigFiles(rootfs, overlay string) error { + hostnameFile := filepath.Join(overlay, "etc", "hostname") + hostsFile := filepath.Join(overlay, "etc", "hosts") + resolvConfFile := filepath.Join(overlay, "etc", "resolv.conf") // Write /etc/hostname hostname := b.hostname @@ -102,11 +95,13 @@ func (b *ConfigFileGenerator) WriteConfigFiles(rootfs string) error { } // Write /etc/resolv.conf if value set + // TODO: apply existing resolv.conf first if err := b.writeResolvConf(resolvConfFile); err != nil { return err } // Write /etc/hosts if not empty + // TODO: apply existing hosts first return b.writeHosts(hostsFile) } @@ -181,7 +176,11 @@ func (b *ConfigFileGenerator) writeHosts(dest string) error { func writeFile(dest, content string) error { f, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { - return err + if os.IsNotExist(err) { + errors.Errorf("file %s does not exist. needs to exist and bind mounted into the containers's rootfs", dest) + } else { + return err + } } if _, err := f.Write([]byte(content)); err != nil { f.Close() diff --git a/pkg/generate/generate.go b/pkg/generate/generate.go index 7c278b1..00c4466 100644 --- a/pkg/generate/generate.go +++ b/pkg/generate/generate.go @@ -61,6 +61,7 @@ func (b *SpecBuilder) ToRootless() { func (b *SpecBuilder) UseHostNetwork() { b.RemoveLinuxNamespace(rspecs.NetworkNamespace) + b.SetHostname("") // empty hostname results in host's hostname opts := []string{"bind", "mode=0444", "nosuid", "noexec", "nodev", "ro"} b.AddBindMount("/etc/hosts", "/etc/hosts", opts) b.AddBindMount("/etc/resolv.conf", "/etc/resolv.conf", opts)