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 <suda.akihiro@lab.ntt.co.jp>
This commit is contained in:
Akihiro Suda 2018-10-15 16:52:53 +09:00
parent 50e63adf30
commit ec87479b7e
36 changed files with 638 additions and 68 deletions

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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.

View File

@ -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
}

View File

@ -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"))

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()
}

77
contrib/dockerd-rootless.sh Executable file
View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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())
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -13,3 +13,7 @@ func (daemon *Daemon) fillPlatformVersion(v *types.Version) {}
func fillDriverWarnings(v *types.Info) {
}
func (daemon *Daemon) configStoreRootless() bool {
return false
}

View File

@ -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)

View File

@ -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
}

92
docs/rootless.md Normal file
View File

@ -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"
```

View File

@ -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
}

View File

@ -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'

View File

@ -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"

View File

@ -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}"
)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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.

26
rootless/rootless.go Normal file
View File

@ -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
}

View File

@ -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
}