From ec87479b7e2bf6f1b5bcc657a377c6e6a847574f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 15 Oct 2018 16:52:53 +0900 Subject: [PATCH] allow running `dockerd` in an unprivileged user namespace (rootless mode) Please refer to `docs/rootless.md`. TLDR: * Make sure `/etc/subuid` and `/etc/subgid` contain the entry for you * `dockerd-rootless.sh --experimental` * `docker -H unix://$XDG_RUNTIME_DIR/docker.sock run ...` Signed-off-by: Akihiro Suda --- Dockerfile | 8 +- cli/config/configdir.go | 2 + cmd/dockerd/config.go | 15 ++- cmd/dockerd/config_common_unix.go | 41 ++++++++- cmd/dockerd/config_unix.go | 11 ++- cmd/dockerd/config_unix_test.go | 3 +- cmd/dockerd/config_windows.go | 23 +++-- cmd/dockerd/daemon.go | 54 +++++++++-- cmd/dockerd/daemon_test.go | 32 ++++--- cmd/dockerd/daemon_unix.go | 28 +++++- cmd/dockerd/daemon_unix_test.go | 10 +- cmd/dockerd/daemon_windows.go | 8 +- cmd/dockerd/docker.go | 25 +++-- cmd/dockerd/options.go | 2 + contrib/dockerd-rootless.sh | 77 ++++++++++++++++ daemon/config/config_unix.go | 6 ++ daemon/config/config_windows.go | 5 + daemon/daemon.go | 1 + daemon/daemon_unix.go | 3 - daemon/info.go | 3 + daemon/info_unix.go | 4 + daemon/info_windows.go | 4 + daemon/listeners/listeners_linux.go | 5 + daemon/oci_linux.go | 9 +- docs/rootless.md | 92 +++++++++++++++++++ hack/dockerfile/install/rootlesskit.installer | 34 +++++++ hack/make/.binary-setup | 2 + hack/make/binary-daemon | 2 +- hack/make/install-binary | 2 + opts/hosts.go | 15 ++- opts/hosts_test.go | 4 +- pkg/homedir/homedir_linux.go | 88 ++++++++++++++++++ pkg/homedir/homedir_others.go | 20 ++++ pkg/sysinfo/sysinfo_linux.go | 4 +- rootless/rootless.go | 26 ++++++ rootless/specconv/specconv_linux.go | 38 ++++++++ 36 files changed, 638 insertions(+), 68 deletions(-) create mode 100755 contrib/dockerd-rootless.sh create mode 100644 docs/rootless.md create mode 100755 hack/dockerfile/install/rootlesskit.installer create mode 100644 rootless/rootless.go create mode 100644 rootless/specconv/specconv_linux.go diff --git a/Dockerfile b/Dockerfile index b4b9a40c39..6516403acf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -161,7 +161,12 @@ ENV INSTALL_BINARY_NAME=tini COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./ RUN PREFIX=/build ./install.sh $INSTALL_BINARY_NAME - +FROM base AS rootlesskit +ENV INSTALL_BINARY_NAME=rootlesskit +COPY hack/dockerfile/install/install.sh ./install.sh +COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./ +RUN PREFIX=/build/ ./install.sh $INSTALL_BINARY_NAME +COPY ./contrib/dockerd-rootless.sh /build # TODO: Some of this is only really needed for testing, it would be nice to split this up FROM runtime-dev AS dev @@ -233,6 +238,7 @@ RUN cd /docker-py \ && pip install paramiko==2.4.2 \ && pip install yamllint==1.5.0 \ && pip install -r test-requirements.txt +COPY --from=rootlesskit /build/ /usr/local/bin/ ENV PATH=/usr/local/cli:$PATH ENV DOCKER_BUILDTAGS apparmor seccomp selinux diff --git a/cli/config/configdir.go b/cli/config/configdir.go index 4bef4e104d..f0b68ee387 100644 --- a/cli/config/configdir.go +++ b/cli/config/configdir.go @@ -13,6 +13,8 @@ var ( ) // Dir returns the path to the configuration directory as specified by the DOCKER_CONFIG environment variable. +// If DOCKER_CONFIG is unset, Dir returns ~/.docker . +// Dir ignores XDG_CONFIG_HOME (same as the docker client). // TODO: this was copied from cli/config/configfile and should be removed once cmd/dockerd moves func Dir() string { return configDir diff --git a/cmd/dockerd/config.go b/cmd/dockerd/config.go index 2c8ed8edb4..bf64108ef4 100644 --- a/cmd/dockerd/config.go +++ b/cmd/dockerd/config.go @@ -17,8 +17,20 @@ const ( ) // installCommonConfigFlags adds flags to the pflag.FlagSet to configure the daemon -func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) { +func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error { var maxConcurrentDownloads, maxConcurrentUploads int + defaultPidFile, err := getDefaultPidFile() + if err != nil { + return err + } + defaultDataRoot, err := getDefaultDataRoot() + if err != nil { + return err + } + defaultExecRoot, err := getDefaultExecRoot() + if err != nil { + return err + } installRegistryServiceFlags(&conf.ServiceOptions, flags) @@ -80,6 +92,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) { conf.MaxConcurrentDownloads = &maxConcurrentDownloads conf.MaxConcurrentUploads = &maxConcurrentUploads + return nil } func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag.FlagSet) { diff --git a/cmd/dockerd/config_common_unix.go b/cmd/dockerd/config_common_unix.go index febf30ae9f..c6526285f3 100644 --- a/cmd/dockerd/config_common_unix.go +++ b/cmd/dockerd/config_common_unix.go @@ -3,17 +3,48 @@ package main import ( + "path/filepath" + "github.com/docker/docker/api/types" "github.com/docker/docker/daemon/config" "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/homedir" + "github.com/docker/docker/rootless" "github.com/spf13/pflag" ) -var ( - defaultPidFile = "/var/run/docker.pid" - defaultDataRoot = "/var/lib/docker" - defaultExecRoot = "/var/run/docker" -) +func getDefaultPidFile() (string, error) { + if !rootless.RunningWithNonRootUsername() { + return "/var/run/docker.pid", nil + } + runtimeDir, err := homedir.GetRuntimeDir() + if err != nil { + return "", err + } + return filepath.Join(runtimeDir, "docker.pid"), nil +} + +func getDefaultDataRoot() (string, error) { + if !rootless.RunningWithNonRootUsername() { + return "/var/lib/docker", nil + } + dataHome, err := homedir.GetDataHome() + if err != nil { + return "", err + } + return filepath.Join(dataHome, "docker"), nil +} + +func getDefaultExecRoot() (string, error) { + if !rootless.RunningWithNonRootUsername() { + return "/var/run/docker", nil + } + runtimeDir, err := homedir.GetRuntimeDir() + if err != nil { + return "", err + } + return filepath.Join(runtimeDir, "docker"), nil +} // installUnixConfigFlags adds command-line options to the top-level flag parser for // the current process that are common across Unix platforms. diff --git a/cmd/dockerd/config_unix.go b/cmd/dockerd/config_unix.go index 2dbd84b1db..cc42ff36c8 100644 --- a/cmd/dockerd/config_unix.go +++ b/cmd/dockerd/config_unix.go @@ -5,14 +5,17 @@ package main import ( "github.com/docker/docker/daemon/config" "github.com/docker/docker/opts" + "github.com/docker/docker/rootless" "github.com/docker/go-units" "github.com/spf13/pflag" ) // installConfigFlags adds flags to the pflag.FlagSet to configure the daemon -func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) { +func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error { // First handle install flags which are consistent cross-platform - installCommonConfigFlags(conf, flags) + if err := installCommonConfigFlags(conf, flags); err != nil { + return err + } // Then install flags common to unix platforms installUnixConfigFlags(conf, flags) @@ -46,5 +49,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) { flags.BoolVar(&conf.NoNewPrivileges, "no-new-privileges", false, "Set no-new-privileges by default for new containers") flags.StringVar(&conf.IpcMode, "default-ipc-mode", config.DefaultIpcMode, `Default mode for containers ipc ("shareable" | "private")`) flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "Default address pools for node specific local networks") - + // Mostly users don't need to set this flag explicitly. + flags.BoolVar(&conf.Rootless, "rootless", rootless.RunningWithNonRootUsername(), "Enable rootless mode (experimental)") + return nil } diff --git a/cmd/dockerd/config_unix_test.go b/cmd/dockerd/config_unix_test.go index d7dbf4b4cc..4b5e894beb 100644 --- a/cmd/dockerd/config_unix_test.go +++ b/cmd/dockerd/config_unix_test.go @@ -15,7 +15,8 @@ func TestDaemonParseShmSize(t *testing.T) { flags := pflag.NewFlagSet("test", pflag.ContinueOnError) conf := &config.Config{} - installConfigFlags(conf, flags) + err := installConfigFlags(conf, flags) + assert.NilError(t, err) // By default `--default-shm-size=64M` assert.Check(t, is.Equal(int64(64*1024*1024), conf.ShmSize.Value())) assert.Check(t, flags.Set("default-shm-size", "128M")) diff --git a/cmd/dockerd/config_windows.go b/cmd/dockerd/config_windows.go index 36af76645f..88b76196dc 100644 --- a/cmd/dockerd/config_windows.go +++ b/cmd/dockerd/config_windows.go @@ -8,19 +8,28 @@ import ( "github.com/spf13/pflag" ) -var ( - defaultPidFile string - defaultDataRoot = filepath.Join(os.Getenv("programdata"), "docker") - defaultExecRoot = filepath.Join(os.Getenv("programdata"), "docker", "exec-root") -) +func getDefaultPidFile() (string, error) { + return "", nil +} + +func getDefaultDataRoot() (string, error) { + return filepath.Join(os.Getenv("programdata"), "docker"), nil +} + +func getDefaultExecRoot() (string, error) { + return filepath.Join(os.Getenv("programdata"), "docker", "exec-root"), nil +} // installConfigFlags adds flags to the pflag.FlagSet to configure the daemon -func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) { +func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error { // First handle install flags which are consistent cross-platform - installCommonConfigFlags(conf, flags) + if err := installCommonConfigFlags(conf, flags); err != nil { + return err + } // Then platform-specific install flags. flags.StringVar(&conf.BridgeConfig.FixedCIDR, "fixed-cidr", "", "IPv4 subnet for fixed IPs") flags.StringVarP(&conf.BridgeConfig.Iface, "bridge", "b", "", "Attach containers to a virtual switch") flags.StringVarP(&conf.SocketGroup, "group", "G", "", "Users or groups that can access the named pipe") + return nil } diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index 0daf197270..f61246d5c6 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -40,12 +40,14 @@ import ( "github.com/docker/docker/libcontainerd/supervisor" dopts "github.com/docker/docker/opts" "github.com/docker/docker/pkg/authorization" + "github.com/docker/docker/pkg/homedir" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/pidfile" "github.com/docker/docker/pkg/plugingetter" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/system" "github.com/docker/docker/plugin" + "github.com/docker/docker/rootless" "github.com/docker/docker/runconfig" "github.com/docker/go-connections/tlsconfig" swarmapi "github.com/docker/swarmkit/api" @@ -97,6 +99,17 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { if cli.Config.Experimental { logrus.Warn("Running experimental build") + if cli.Config.IsRootless() { + logrus.Warn("Running in rootless mode. Cgroups, AppArmor, and CRIU are disabled.") + } + } else { + if cli.Config.IsRootless() { + return fmt.Errorf("rootless mode is supported only when running in experimental mode") + } + } + // return human-friendly error before creating files + if runtime.GOOS == "linux" && os.Geteuid() != 0 { + return fmt.Errorf("dockerd needs to be started with root. To see how to run dockerd in rootless mode with unprivileged user, see the documentation") } system.InitLCOW(cli.Config.Experimental) @@ -115,11 +128,14 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { return err } + potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot} + if cli.Pidfile != "" { pf, err := pidfile.New(cli.Pidfile) if err != nil { return errors.Wrap(err, "failed to start daemon") } + potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile) defer func() { if err := pf.Remove(); err != nil { logrus.Error(err) @@ -127,6 +143,12 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { }() } + // Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR + if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil { + // StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset + logrus.WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR") + } + serverConfig, err := newAPIServerConfig(cli) if err != nil { return errors.Wrap(err, "failed to create API server") @@ -140,7 +162,11 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { ctx, cancel := context.WithCancel(context.Background()) if cli.Config.ContainerdAddr == "" && runtime.GOOS != "windows" { - if !systemContainerdRunning() { + systemContainerdAddr, ok, err := systemContainerdRunning(cli.Config.IsRootless()) + if err != nil { + return errors.Wrap(err, "could not determine whether the system containerd is running") + } + if !ok { opts, err := cli.getContainerdDaemonOpts() if err != nil { cancel() @@ -157,7 +183,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { // Try to wait for containerd to shutdown defer r.WaitTimeout(10 * time.Second) } else { - cli.Config.ContainerdAddr = containerddefaults.DefaultAddress + cli.Config.ContainerdAddr = systemContainerdAddr } } defer cancel() @@ -403,9 +429,11 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) { } if conf.TrustKeyPath == "" { - conf.TrustKeyPath = filepath.Join( - getDaemonConfDir(conf.Root), - defaultTrustKeyFile) + daemonConfDir, err := getDaemonConfDir(conf.Root) + if err != nil { + return nil, err + } + conf.TrustKeyPath = filepath.Join(daemonConfDir, defaultTrustKeyFile) } if flags.Changed("graph") && flags.Changed("data-root") { @@ -585,7 +613,7 @@ func loadListeners(cli *DaemonCli, serverConfig *apiserver.Config) ([]string, er var hosts []string for i := 0; i < len(cli.Config.Hosts); i++ { var err error - if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil { + if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, rootless.RunningWithNonRootUsername(), cli.Config.Hosts[i]); err != nil { return nil, errors.Wrapf(err, "error parsing -H %s", cli.Config.Hosts[i]) } @@ -662,9 +690,17 @@ func validateAuthzPlugins(requestedPlugins []string, pg plugingetter.PluginGette return nil } -func systemContainerdRunning() bool { - _, err := os.Lstat(containerddefaults.DefaultAddress) - return err == nil +func systemContainerdRunning(isRootless bool) (string, bool, error) { + addr := containerddefaults.DefaultAddress + if isRootless { + runtimeDir, err := homedir.GetRuntimeDir() + if err != nil { + return "", false, err + } + addr = filepath.Join(runtimeDir, "containerd", "containerd.sock") + } + _, err := os.Lstat(addr) + return addr, err == nil, nil } // configureDaemonLogs sets the logrus logging level and formatting diff --git a/cmd/dockerd/daemon_test.go b/cmd/dockerd/daemon_test.go index 38b2d0fb53..fd167654af 100644 --- a/cmd/dockerd/daemon_test.go +++ b/cmd/dockerd/daemon_test.go @@ -11,18 +11,22 @@ import ( "gotest.tools/fs" ) -func defaultOptions(configFile string) *daemonOptions { +func defaultOptions(t *testing.T, configFile string) *daemonOptions { opts := newDaemonOptions(&config.Config{}) opts.flags = &pflag.FlagSet{} opts.InstallFlags(opts.flags) - installConfigFlags(opts.daemonConfig, opts.flags) + if err := installConfigFlags(opts.daemonConfig, opts.flags); err != nil { + t.Fatal(err) + } + defaultDaemonConfigFile, err := getDefaultDaemonConfigFile() + assert.NilError(t, err) opts.flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "") opts.configFile = configFile return opts } func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) { - opts := defaultOptions("") + opts := defaultOptions(t, "") opts.Debug = true loadedConfig, err := loadDaemonCliConfig(opts) @@ -34,7 +38,7 @@ func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) { } func TestLoadDaemonCliConfigWithTLS(t *testing.T) { - opts := defaultOptions("") + opts := defaultOptions(t, "") opts.TLSOptions.CAFile = "/tmp/ca.pem" opts.TLS = true @@ -49,7 +53,7 @@ func TestLoadDaemonCliConfigWithConflicts(t *testing.T) { defer tempFile.Remove() configFile := tempFile.Path() - opts := defaultOptions(configFile) + opts := defaultOptions(t, configFile) flags := opts.flags assert.Check(t, flags.Set("config-file", configFile)) @@ -65,7 +69,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) { defer tempFile.Remove() configFile := tempFile.Path() - opts := defaultOptions(configFile) + opts := defaultOptions(t, configFile) flags := opts.flags assert.Check(t, flags.Set("config-file", configFile)) @@ -77,7 +81,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) { } func TestLoadDaemonCliWithConflictingLabels(t *testing.T) { - opts := defaultOptions("") + opts := defaultOptions(t, "") flags := opts.flags assert.Check(t, flags.Set("label", "foo=bar")) @@ -88,7 +92,7 @@ func TestLoadDaemonCliWithConflictingLabels(t *testing.T) { } func TestLoadDaemonCliWithDuplicateLabels(t *testing.T) { - opts := defaultOptions("") + opts := defaultOptions(t, "") flags := opts.flags assert.Check(t, flags.Set("label", "foo=the-same")) @@ -102,7 +106,7 @@ func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": true}`)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) opts.TLSOptions.CAFile = "/tmp/ca.pem" loadedConfig, err := loadDaemonCliConfig(opts) @@ -115,7 +119,7 @@ func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": false}`)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) opts.TLSOptions.CAFile = "/tmp/ca.pem" loadedConfig, err := loadDaemonCliConfig(opts) @@ -128,7 +132,7 @@ func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) opts.TLSOptions.CAFile = "/tmp/ca.pem" loadedConfig, err := loadDaemonCliConfig(opts) @@ -141,7 +145,7 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-level": "warn"}`)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) loadedConfig, err := loadDaemonCliConfig(opts) assert.NilError(t, err) assert.Assert(t, loadedConfig != nil) @@ -153,7 +157,7 @@ func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(content)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) loadedConfig, err := loadDaemonCliConfig(opts) assert.NilError(t, err) assert.Assert(t, loadedConfig != nil) @@ -170,7 +174,7 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(content)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) loadedConfig, err := loadDaemonCliConfig(opts) assert.NilError(t, err) assert.Assert(t, loadedConfig != nil) diff --git a/cmd/dockerd/daemon_unix.go b/cmd/dockerd/daemon_unix.go index 7b03e28594..f6ada580aa 100644 --- a/cmd/dockerd/daemon_unix.go +++ b/cmd/dockerd/daemon_unix.go @@ -15,11 +15,33 @@ import ( "github.com/docker/docker/daemon" "github.com/docker/docker/daemon/config" "github.com/docker/docker/libcontainerd/supervisor" + "github.com/docker/docker/pkg/homedir" + "github.com/docker/docker/rootless" "github.com/docker/libnetwork/portallocator" "golang.org/x/sys/unix" ) -const defaultDaemonConfigFile = "/etc/docker/daemon.json" +func getDefaultDaemonConfigDir() (string, error) { + if !rootless.RunningWithNonRootUsername() { + return "/etc/docker", nil + } + // NOTE: CLI uses ~/.docker while the daemon uses ~/.config/docker, because + // ~/.docker was not designed to store daemon configurations. + // In future, the daemon directory may be renamed to ~/.config/moby-engine (?). + configHome, err := homedir.GetConfigHome() + if err != nil { + return "", nil + } + return filepath.Join(configHome, "docker"), nil +} + +func getDefaultDaemonConfigFile() (string, error) { + dir, err := getDefaultDaemonConfigDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "daemon.json"), nil +} // setDefaultUmask sets the umask to 0022 to avoid problems // caused by custom umask @@ -33,8 +55,8 @@ func setDefaultUmask() error { return nil } -func getDaemonConfDir(_ string) string { - return "/etc/docker" +func getDaemonConfDir(_ string) (string, error) { + return getDefaultDaemonConfigDir() } func (cli *DaemonCli) getPlatformContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) { diff --git a/cmd/dockerd/daemon_unix_test.go b/cmd/dockerd/daemon_unix_test.go index 692d0328c4..be8474f040 100644 --- a/cmd/dockerd/daemon_unix_test.go +++ b/cmd/dockerd/daemon_unix_test.go @@ -16,7 +16,7 @@ func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(content)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) opts.Debug = true opts.LogLevel = "info" assert.Check(t, opts.flags.Set("selinux-enabled", "true")) @@ -37,7 +37,7 @@ func TestLoadDaemonConfigWithNetwork(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(content)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) loadedConfig, err := loadDaemonCliConfig(opts) assert.NilError(t, err) assert.Assert(t, loadedConfig != nil) @@ -54,7 +54,7 @@ func TestLoadDaemonConfigWithMapOptions(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(content)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) loadedConfig, err := loadDaemonCliConfig(opts) assert.NilError(t, err) assert.Assert(t, loadedConfig != nil) @@ -71,7 +71,7 @@ func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(content)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) loadedConfig, err := loadDaemonCliConfig(opts) assert.NilError(t, err) assert.Assert(t, loadedConfig != nil) @@ -90,7 +90,7 @@ func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) { tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`)) defer tempFile.Remove() - opts := defaultOptions(tempFile.Path()) + opts := defaultOptions(t, tempFile.Path()) loadedConfig, err := loadDaemonCliConfig(opts) assert.NilError(t, err) assert.Assert(t, loadedConfig != nil) diff --git a/cmd/dockerd/daemon_windows.go b/cmd/dockerd/daemon_windows.go index 3b9ed9551f..f4d213da96 100644 --- a/cmd/dockerd/daemon_windows.go +++ b/cmd/dockerd/daemon_windows.go @@ -12,15 +12,17 @@ import ( "golang.org/x/sys/windows" ) -var defaultDaemonConfigFile = "" +func getDefaultDaemonConfigFile() (string, error) { + return "", nil +} // setDefaultUmask doesn't do anything on windows func setDefaultUmask() error { return nil } -func getDaemonConfDir(root string) string { - return filepath.Join(root, `\config`) +func getDaemonConfDir(root string) (string, error) { + return filepath.Join(root, `\config`), nil } // preNotifySystem sends a message to the host when the API is active, but before the daemon is diff --git a/cmd/dockerd/docker.go b/cmd/dockerd/docker.go index 6097e2c6bc..a6d3387828 100644 --- a/cmd/dockerd/docker.go +++ b/cmd/dockerd/docker.go @@ -16,7 +16,7 @@ import ( "github.com/spf13/cobra" ) -func newDaemonCommand() *cobra.Command { +func newDaemonCommand() (*cobra.Command, error) { opts := newDaemonOptions(config.New()) cmd := &cobra.Command{ @@ -36,12 +36,18 @@ func newDaemonCommand() *cobra.Command { flags := cmd.Flags() flags.BoolP("version", "v", false, "Print version information and quit") + defaultDaemonConfigFile, err := getDefaultDaemonConfigFile() + if err != nil { + return nil, err + } flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "Daemon configuration file") opts.InstallFlags(flags) - installConfigFlags(opts.daemonConfig, flags) + if err := installConfigFlags(opts.daemonConfig, flags); err != nil { + return nil, err + } installServiceFlags(flags) - return cmd + return cmd, nil } func init() { @@ -72,10 +78,17 @@ func main() { logrus.SetOutput(stderr) } - cmd := newDaemonCommand() - cmd.SetOutput(stdout) - if err := cmd.Execute(); err != nil { + onError := func(err error) { fmt.Fprintf(stderr, "%s\n", err) os.Exit(1) } + + cmd, err := newDaemonCommand() + if err != nil { + onError(err) + } + cmd.SetOutput(stdout) + if err := cmd.Execute(); err != nil { + onError(err) + } } diff --git a/cmd/dockerd/options.go b/cmd/dockerd/options.go index cb5601a768..f6ea21a6dc 100644 --- a/cmd/dockerd/options.go +++ b/cmd/dockerd/options.go @@ -49,6 +49,8 @@ func newDaemonOptions(config *config.Config) *daemonOptions { // InstallFlags adds flags for the common options on the FlagSet func (o *daemonOptions) InstallFlags(flags *pflag.FlagSet) { if dockerCertPath == "" { + // cliconfig.Dir returns $DOCKER_CONFIG or ~/.docker. + // cliconfig.Dir does not look up $XDG_CONFIG_HOME dockerCertPath = cliconfig.Dir() } diff --git a/contrib/dockerd-rootless.sh b/contrib/dockerd-rootless.sh new file mode 100755 index 0000000000..ca8f5d439f --- /dev/null +++ b/contrib/dockerd-rootless.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# dockerd-rootless.sh executes dockerd in rootless mode. +# +# Usage: dockerd-rootless.sh --experimental [DOCKERD_OPTIONS] +# Currently, specifying --experimental is mandatory. +# +# External dependencies: +# * newuidmap and newgidmap needs to be installed. +# * /etc/subuid and /etc/subgid needs to be configured for the current user. +# * Either slirp4netns (v0.3+) or VPNKit needs to be installed. +# +# See the documentation for the further information. + +set -e -x +if ! [ -w $XDG_RUNTIME_DIR ]; then + echo "XDG_RUNTIME_DIR needs to be set and writable" + exit 1 +fi +if ! [ -w $HOME ]; then + echo "HOME needs to be set and writable" + exit 1 +fi + +rootlesskit="" +for f in docker-rootlesskit rootlesskit; do + if which $f >/dev/null 2>&1; then + rootlesskit=$f + break + fi +done +if [ -z $rootlesskit ]; then + echo "rootlesskit needs to be installed" + exit 1 +fi + +net="" +mtu="" +if which slirp4netns >/dev/null 2>&1; then + if slirp4netns --help | grep -- --disable-host-loopback; then + net=slirp4netns + mtu=65520 + else + echo "slirp4netns does not support --disable-host-loopback. Falling back to VPNKit." + fi +fi +if [ -z $net ]; then + if which vpnkit >/dev/null 2>&1; then + net=vpnkit + mtu=1500 + else + echo "Either slirp4netns (v0.3+) or vpnkit needs to be installed" + exit 1 + fi +fi + +if [ -z $_DOCKERD_ROOTLESS_CHILD ]; then + _DOCKERD_ROOTLESS_CHILD=1 + export _DOCKERD_ROOTLESS_CHILD + # Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces. + # + # --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks + # * /etc: copy-up is required so as to prevent `/etc/resolv.conf` in the + # namespace from being unexpectedly unmounted when `/etc/resolv.conf` is recreated on the host + # (by either systemd-networkd or NetworkManager) + # * /run: copy-up is required so that we can create /run/docker (hardcoded for plugins) in our namespace + $rootlesskit \ + --net=$net --mtu=$mtu --disable-host-loopback \ + --copy-up=/etc --copy-up=/run \ + $DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS \ + $0 $@ +else + [ $_DOCKERD_ROOTLESS_CHILD = 1 ] + # remove the symlinks for the existing files in the parent namespace if any, + # so that we can create our own files in our mount namespace. + rm -f /run/docker /run/xtables.lock + dockerd $@ +fi diff --git a/daemon/config/config_unix.go b/daemon/config/config_unix.go index 5ed6abd89e..0ba7e754fb 100644 --- a/daemon/config/config_unix.go +++ b/daemon/config/config_unix.go @@ -39,6 +39,7 @@ type Config struct { IpcMode string `json:"default-ipc-mode,omitempty"` // ResolvConf is the path to the configuration of the host resolver ResolvConf string `json:"resolv-conf,omitempty"` + Rootless bool `json:"rootless,omitempty"` } // BridgeConfig stores all the bridge driver specific @@ -87,3 +88,8 @@ func verifyDefaultIpcMode(mode string) error { func (conf *Config) ValidatePlatformConfig() error { return verifyDefaultIpcMode(conf.IpcMode) } + +// IsRootless returns conf.Rootless +func (conf *Config) IsRootless() bool { + return conf.Rootless +} diff --git a/daemon/config/config_windows.go b/daemon/config/config_windows.go index 0aa7d54bf2..47624fab46 100644 --- a/daemon/config/config_windows.go +++ b/daemon/config/config_windows.go @@ -55,3 +55,8 @@ func (conf *Config) IsSwarmCompatible() error { func (conf *Config) ValidatePlatformConfig() error { return nil } + +// IsRootless returns conf.Rootless on Unix but false on Windows +func (conf *Config) IsRootless() bool { + return false +} diff --git a/daemon/daemon.go b/daemon/daemon.go index 651a57a139..428da80846 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -800,6 +800,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S logrus.Warnf("Failed to configure golang's threads limit: %v", err) } + // ensureDefaultAppArmorProfile does nothing if apparmor is disabled if err := ensureDefaultAppArmorProfile(); err != nil { logrus.Errorf(err.Error()) } diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go index 817025e93b..f4b18b3ca5 100644 --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -745,9 +745,6 @@ func verifyDaemonSettings(conf *config.Config) error { // checkSystem validates platform-specific requirements func checkSystem() error { - if os.Geteuid() != 0 { - return fmt.Errorf("The Docker daemon needs to be run as root") - } return checkKernel() } diff --git a/daemon/info.go b/daemon/info.go index 7b9a8588a2..b2e4724aee 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -175,6 +175,9 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf if rootIDs := daemon.idMapping.RootPair(); rootIDs.UID != 0 || rootIDs.GID != 0 { securityOptions = append(securityOptions, "name=userns") } + if daemon.configStoreRootless() { + securityOptions = append(securityOptions, "name=rootless") + } v.SecurityOptions = securityOptions } diff --git a/daemon/info_unix.go b/daemon/info_unix.go index 0bc255e41f..b367171bfa 100644 --- a/daemon/info_unix.go +++ b/daemon/info_unix.go @@ -245,3 +245,7 @@ func parseRuncVersion(v string) (version string, commit string, err error) { } return version, commit, err } + +func (daemon *Daemon) configStoreRootless() bool { + return daemon.configStore.Rootless +} diff --git a/daemon/info_windows.go b/daemon/info_windows.go index fe9b1e6732..b8611d7f82 100644 --- a/daemon/info_windows.go +++ b/daemon/info_windows.go @@ -13,3 +13,7 @@ func (daemon *Daemon) fillPlatformVersion(v *types.Version) {} func fillDriverWarnings(v *types.Info) { } + +func (daemon *Daemon) configStoreRootless() bool { + return false +} diff --git a/daemon/listeners/listeners_linux.go b/daemon/listeners/listeners_linux.go index c8956db258..9b5b3b7e77 100644 --- a/daemon/listeners/listeners_linux.go +++ b/daemon/listeners/listeners_linux.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/coreos/go-systemd/activation" + "github.com/docker/docker/pkg/homedir" "github.com/docker/go-connections/sockets" "github.com/sirupsen/logrus" ) @@ -45,6 +46,10 @@ func Init(proto, addr, socketGroup string, tlsConfig *tls.Config) ([]net.Listene if err != nil { return nil, fmt.Errorf("can't create unix socket %s: %v", addr, err) } + if _, err := homedir.StickRuntimeDirContents([]string{addr}); err != nil { + // StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset + logrus.WithError(err).Warnf("cannot set sticky bit on socket %s under XDG_RUNTIME_DIR", addr) + } ls = append(ls, l) default: return nil, fmt.Errorf("invalid protocol format: %q", proto) diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go index ca6074cfac..574f29c7d9 100644 --- a/daemon/oci_linux.go +++ b/daemon/oci_linux.go @@ -17,10 +17,12 @@ import ( "github.com/docker/docker/oci/caps" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/rootless/specconv" volumemounts "github.com/docker/docker/volume/mounts" "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/devices" + rsystem "github.com/opencontainers/runc/libcontainer/system" "github.com/opencontainers/runc/libcontainer/user" "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -89,7 +91,7 @@ func setDevices(s *specs.Spec, c *container.Container) error { // Build lists of devices allowed and created within the container. var devs []specs.LinuxDevice devPermissions := s.Linux.Resources.Devices - if c.HostConfig.Privileged { + if c.HostConfig.Privileged && !rsystem.RunningInUserNS() { hostDevices, err := devices.HostDevices() if err != nil { return err @@ -867,6 +869,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths } + if daemon.configStore.Rootless { + if err := specconv.ToRootless(&s); err != nil { + return nil, err + } + } return &s, nil } diff --git a/docs/rootless.md b/docs/rootless.md new file mode 100644 index 0000000000..bb0486b6e2 --- /dev/null +++ b/docs/rootless.md @@ -0,0 +1,92 @@ +# Rootless mode (Experimental) + +The rootless mode allows running `dockerd` as an unprivileged user, using `user_namespaces(7)`, `mount_namespaces(7)`, `network_namespaces(7)`. + +No SETUID/SETCAP binary is required except `newuidmap` and `newgidmap`. + +## Requirements +* `newuidmap` and `newgidmap` need to be installed on the host. These commands are provided by the `uidmap` package on most distros. + +* `/etc/subuid` and `/etc/subgid` should contain >= 65536 sub-IDs. e.g. `penguin:231072:65536`. + +```console +$ id -u +1001 +$ whoami +penguin +$ grep ^$(whoami): /etc/subuid +penguin:231072:65536 +$ grep ^$(whoami): /etc/subgid +penguin:231072:65536 +``` + +* Either [slirp4netns](https://github.com/rootless-containers/slirp4netns) (v0.3+) or [VPNKit](https://github.com/moby/vpnkit) needs to be installed. slirp4netns is preferred for the best performance. + +### Distribution-specific hint + +#### Debian (excluding Ubuntu) +* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required + +#### Arch Linux +* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required + +#### openSUSE +* `sudo modprobe ip_tables iptable_mangle iptable_nat iptable_filter` is required. (This is likely to be required on other distros as well) + +#### RHEL/CentOS 7 +* `sudo sh -c "echo 28633 > /proc/sys/user/max_user_namespaces"` is required +* [COPR package `vbatts/shadow-utils-newxidmap`](https://copr.fedorainfracloud.org/coprs/vbatts/shadow-utils-newxidmap/) needs to be installed + +## Restrictions + +* Only `vfs` graphdriver is supported. However, on [Ubuntu](http://kernel.ubuntu.com/git/ubuntu/ubuntu-artful.git/commit/fs/overlayfs?h=Ubuntu-4.13.0-25.29&id=0a414bdc3d01f3b61ed86cfe3ce8b63a9240eba7) and a few distros, `overlay2` and `overlay` are also supported. +* Following features are not supported: + * Cgroups (including `docker top`, which depends on the cgroups device controller) + * Apparmor + * Checkpoint + * Overlay network + +## Usage + +### Daemon + +You need to run `dockerd-rootless.sh` instead of `dockerd`. + +```console +$ dockerd-rootless.sh --experimental" +``` +As Rootless mode is experimental per se, currently you always need to run `dockerd-rootless.sh` with `--experimental`. + +Remarks: +* The socket path is set to `$XDG_RUNTIME_DIR/docker.sock` by default. `$XDG_RUNTIME_DIR` is typically set to `/run/user/$UID`. +* The data dir is set to `~/.local/share/docker` by default. +* The exec dir is set to `$XDG_RUNTIME_DIR/docker` by default. +* The daemon config dir is set to `~/.config/docker` (not `~/.docker`, which is used by the client) by default. +* The `dockerd-rootless.sh` script executes `dockerd` in its own user, mount, and network namespaces. You can enter the namespaces by running `nsenter -U --preserve-credentials -n -m -t $(cat $XDG_RUNTIME_DIR/docker.pid)`. + +### Client + +You can just use the upstream Docker client but you need to set the socket path explicitly. + +```console +$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d nginx +``` + +### Exposing ports + +In addition to exposing container ports to the `dockerd` network namespace, you also need to expose the ports in the `dockerd` network namespace to the host network namespace. + +```console +$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d -p 80:80 nginx +$ socat -t -- TCP-LISTEN:8080,reuseaddr,fork EXEC:"nsenter -U -n -t $(cat $XDG_RUNTIME_DIR/docker.pid) socat -t -- STDIN TCP4\:127.0.0.1\:80" +``` + +In future, `dockerd` will be able to expose the ports automatically. + +### Routing ping packets + +To route ping packets, you need to set up `net.ipv4.ping_group_range` properly as the root. + +```console +$ sudo sh -c "echo 0 2147483647 > /proc/sys/net/ipv4/ping_group_range" +``` diff --git a/hack/dockerfile/install/rootlesskit.installer b/hack/dockerfile/install/rootlesskit.installer new file mode 100755 index 0000000000..fd6ecef543 --- /dev/null +++ b/hack/dockerfile/install/rootlesskit.installer @@ -0,0 +1,34 @@ +#!/bin/sh + +# v0.3.0-alpha.0 +ROOTLESSKIT_COMMIT=3c4582e950e3a67795c2832179c125b258b78124 + +install_rootlesskit() { + case "$1" in + "dynamic") + install_rootlesskit_dynamic + return + ;; + "") + export CGO_ENABLED=0 + _install_rootlesskit + ;; + *) + echo 'Usage: $0 [dynamic]' + ;; + esac +} + +install_rootlesskit_dynamic() { + export ROOTLESSKIT_LDFLAGS="-linkmode=external" install_rootlesskit + export BUILD_MODE="-buildmode=pie" + _install_rootlesskit +} + +_install_rootlesskit() { + echo "Install rootlesskit version $ROOTLESSKIT_COMMIT" + git clone https://github.com/rootless-containers/rootlesskit.git "$GOPATH/src/github.com/rootless-containers/rootlesskit" + cd "$GOPATH/src/github.com/rootless-containers/rootlesskit" + git checkout -q "$ROOTLESSKIT_COMMIT" + go build $BUILD_MODE -ldflags="$ROOTLESSKIT_LDFLAGS" -o "${PREFIX}/rootlesskit" github.com/rootless-containers/rootlesskit/cmd/rootlesskit +} diff --git a/hack/make/.binary-setup b/hack/make/.binary-setup index 69bb39b364..43e4b2cdc1 100644 --- a/hack/make/.binary-setup +++ b/hack/make/.binary-setup @@ -7,3 +7,5 @@ DOCKER_CONTAINERD_CTR_BINARY_NAME='ctr' DOCKER_CONTAINERD_SHIM_BINARY_NAME='containerd-shim' DOCKER_PROXY_BINARY_NAME='docker-proxy' DOCKER_INIT_BINARY_NAME='docker-init' +DOCKER_ROOTLESSKIT_BINARY_NAME='rootlesskit' +DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME='dockerd-rootless.sh' diff --git a/hack/make/binary-daemon b/hack/make/binary-daemon index c1a6e6f9ed..dbf51b1426 100644 --- a/hack/make/binary-daemon +++ b/hack/make/binary-daemon @@ -14,7 +14,7 @@ copy_binaries() { return fi echo "Copying nested executables into $dir" - for file in containerd containerd-shim ctr runc docker-init docker-proxy; do + for file in containerd containerd-shim ctr runc docker-init docker-proxy rootlesskit dockerd-rootless.sh; do cp -f `which "$file"` "$dir/" if [ "$hash" == "hash" ]; then hash_files "$dir/$file" diff --git a/hack/make/install-binary b/hack/make/install-binary index f6a4361fdb..4ad2fb1d75 100644 --- a/hack/make/install-binary +++ b/hack/make/install-binary @@ -26,4 +26,6 @@ install_binary() { install_binary "${DEST}/${DOCKER_CONTAINERD_SHIM_BINARY_NAME}" install_binary "${DEST}/${DOCKER_PROXY_BINARY_NAME}" install_binary "${DEST}/${DOCKER_INIT_BINARY_NAME}" + install_binary "${DEST}/${DOCKER_ROOTLESSKIT_BINARY_NAME}" + install_binary "${DEST}/${DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME}" ) diff --git a/opts/hosts.go b/opts/hosts.go index 2adf4211d5..3d8785f11c 100644 --- a/opts/hosts.go +++ b/opts/hosts.go @@ -4,8 +4,11 @@ import ( "fmt" "net" "net/url" + "path/filepath" "strconv" "strings" + + "github.com/docker/docker/pkg/homedir" ) var ( @@ -41,12 +44,20 @@ func ValidateHost(val string) (string, error) { return val, nil } -// ParseHost and set defaults for a Daemon host string -func ParseHost(defaultToTLS bool, val string) (string, error) { +// ParseHost and set defaults for a Daemon host string. +// defaultToTLS is preferred over defaultToUnixRootless. +func ParseHost(defaultToTLS, defaultToUnixRootless bool, val string) (string, error) { host := strings.TrimSpace(val) if host == "" { if defaultToTLS { host = DefaultTLSHost + } else if defaultToUnixRootless { + runtimeDir, err := homedir.GetRuntimeDir() + if err != nil { + return "", err + } + socket := filepath.Join(runtimeDir, "docker.sock") + host = "unix://" + socket } else { host = DefaultHost } diff --git a/opts/hosts_test.go b/opts/hosts_test.go index e46326a5be..8c54ec0f4b 100644 --- a/opts/hosts_test.go +++ b/opts/hosts_test.go @@ -38,13 +38,13 @@ func TestParseHost(t *testing.T) { } for _, value := range invalid { - if _, err := ParseHost(false, value); err == nil { + if _, err := ParseHost(false, false, value); err == nil { t.Errorf("Expected an error for %v, got [nil]", value) } } for value, expected := range valid { - if actual, err := ParseHost(false, value); err != nil || actual != expected { + if actual, err := ParseHost(false, false, value); err != nil || actual != expected { t.Errorf("Expected for %v [%v], got [%v, %v]", value, expected, actual, err) } } diff --git a/pkg/homedir/homedir_linux.go b/pkg/homedir/homedir_linux.go index ee15ed52b1..47ecd0c092 100644 --- a/pkg/homedir/homedir_linux.go +++ b/pkg/homedir/homedir_linux.go @@ -1,7 +1,10 @@ package homedir // import "github.com/docker/docker/pkg/homedir" import ( + "errors" "os" + "path/filepath" + "strings" "github.com/docker/docker/pkg/idtools" ) @@ -19,3 +22,88 @@ func GetStatic() (string, error) { } return usr.Home, nil } + +// GetRuntimeDir returns XDG_RUNTIME_DIR. +// XDG_RUNTIME_DIR is typically configured via pam_systemd. +// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set. +// +// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html +func GetRuntimeDir() (string, error) { + if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" { + return xdgRuntimeDir, nil + } + return "", errors.New("could not get XDG_RUNTIME_DIR") +} + +// StickRuntimeDirContents sets the sticky bit on files that are under +// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system. +// +// StickyRuntimeDir returns slice of sticked files. +// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set. +// +// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html +func StickRuntimeDirContents(files []string) ([]string, error) { + runtimeDir, err := GetRuntimeDir() + if err != nil { + // ignore error if runtimeDir is empty + return nil, nil + } + runtimeDir, err = filepath.Abs(runtimeDir) + if err != nil { + return nil, err + } + var sticked []string + for _, f := range files { + f, err = filepath.Abs(f) + if err != nil { + return sticked, err + } + if strings.HasPrefix(f, runtimeDir+"/") { + if err = stick(f); err != nil { + return sticked, err + } + sticked = append(sticked, f) + } + } + return sticked, nil +} + +func stick(f string) error { + st, err := os.Stat(f) + if err != nil { + return err + } + m := st.Mode() + m |= os.ModeSticky + return os.Chmod(f, m) +} + +// GetDataHome returns XDG_DATA_HOME. +// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set. +// +// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html +func GetDataHome() (string, error) { + if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" { + return xdgDataHome, nil + } + home := os.Getenv("HOME") + if home == "" { + return "", errors.New("could not get either XDG_DATA_HOME or HOME") + } + return filepath.Join(home, ".local", "share"), nil +} + +// GetConfigHome returns XDG_CONFIG_HOME. +// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set. +// +// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html +func GetConfigHome() (string, error) { + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { + return xdgConfigHome, nil + } + home := os.Getenv("HOME") + if home == "" { + return "", errors.New("could not get either XDG_CONFIG_HOME or HOME") + } + return filepath.Join(home, ".config"), nil +} diff --git a/pkg/homedir/homedir_others.go b/pkg/homedir/homedir_others.go index 75ada2fe54..f0a363dedf 100644 --- a/pkg/homedir/homedir_others.go +++ b/pkg/homedir/homedir_others.go @@ -11,3 +11,23 @@ import ( func GetStatic() (string, error) { return "", errors.New("homedir.GetStatic() is not supported on this system") } + +// GetRuntimeDir is unsupported on non-linux system. +func GetRuntimeDir() (string, error) { + return "", errors.New("homedir.GetRuntimeDir() is not supported on this system") +} + +// StickRuntimeDirContents is unsupported on non-linux system. +func StickRuntimeDirContents(files []string) ([]string, error) { + return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system") +} + +// GetDataHome is unsupported on non-linux system. +func GetDataHome() (string, error) { + return "", errors.New("homedir.GetDataHome() is not supported on this system") +} + +// GetConfigHome is unsupported on non-linux system. +func GetConfigHome() (string, error) { + return "", errors.New("homedir.GetConfigHome() is not supported on this system") +} diff --git a/pkg/sysinfo/sysinfo_linux.go b/pkg/sysinfo/sysinfo_linux.go index 1fcf08310c..040c5a4dd6 100644 --- a/pkg/sysinfo/sysinfo_linux.go +++ b/pkg/sysinfo/sysinfo_linux.go @@ -51,7 +51,9 @@ func New(quiet bool) *SysInfo { // Check if AppArmor is supported. if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) { - sysInfo.AppArmor = true + if _, err := ioutil.ReadFile("/sys/kernel/security/apparmor/profiles"); err == nil { + sysInfo.AppArmor = true + } } // Check if Seccomp is supported, via CONFIG_SECCOMP. diff --git a/rootless/rootless.go b/rootless/rootless.go new file mode 100644 index 0000000000..d5b07a033d --- /dev/null +++ b/rootless/rootless.go @@ -0,0 +1,26 @@ +package rootless + +import ( + "os" + "sync" +) + +var ( + runningWithNonRootUsername bool + runningWithNonRootUsernameOnce sync.Once +) + +// RunningWithNonRootUsername returns true if we $USER is set to a non-root value, +// regardless to the UID/EUID value. +// +// The value of this variable is mostly used for configuring default paths. +// If the value is true, $HOME and $XDG_RUNTIME_DIR should be honored for setting up the default paths. +// If false (not only EUID==0 but also $USER==root), $HOME and $XDG_RUNTIME_DIR should be ignored +// even if we are in a user namespace. +func RunningWithNonRootUsername() bool { + runningWithNonRootUsernameOnce.Do(func() { + u := os.Getenv("USER") + runningWithNonRootUsername = u != "" && u != "root" + }) + return runningWithNonRootUsername +} diff --git a/rootless/specconv/specconv_linux.go b/rootless/specconv/specconv_linux.go new file mode 100644 index 0000000000..52f74b9457 --- /dev/null +++ b/rootless/specconv/specconv_linux.go @@ -0,0 +1,38 @@ +package specconv + +import ( + "io/ioutil" + "strconv" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +// ToRootless converts spec to be compatible with "rootless" runc. +// * Remove cgroups (will be supported in separate PR when delegation permission is configured) +// * Fix up OOMScoreAdj +func ToRootless(spec *specs.Spec) error { + return toRootless(spec, getCurrentOOMScoreAdj()) +} + +func getCurrentOOMScoreAdj() int { + b, err := ioutil.ReadFile("/proc/self/oom_score_adj") + if err != nil { + return 0 + } + i, err := strconv.Atoi(string(b)) + if err != nil { + return 0 + } + return i +} + +func toRootless(spec *specs.Spec, currentOOMScoreAdj int) error { + // Remove cgroup settings. + spec.Linux.Resources = nil + spec.Linux.CgroupsPath = "" + + if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj { + *spec.Process.OOMScoreAdj = currentOOMScoreAdj + } + return nil +}