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>
| ... | ... |
@@ -161,7 +161,12 @@ ENV INSTALL_BINARY_NAME=tini |
| 161 | 161 |
COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./ |
| 162 | 162 |
RUN PREFIX=/build ./install.sh $INSTALL_BINARY_NAME |
| 163 | 163 |
|
| 164 |
- |
|
| 164 |
+FROM base AS rootlesskit |
|
| 165 |
+ENV INSTALL_BINARY_NAME=rootlesskit |
|
| 166 |
+COPY hack/dockerfile/install/install.sh ./install.sh |
|
| 167 |
+COPY hack/dockerfile/install/$INSTALL_BINARY_NAME.installer ./ |
|
| 168 |
+RUN PREFIX=/build/ ./install.sh $INSTALL_BINARY_NAME |
|
| 169 |
+COPY ./contrib/dockerd-rootless.sh /build |
|
| 165 | 170 |
|
| 166 | 171 |
# TODO: Some of this is only really needed for testing, it would be nice to split this up |
| 167 | 172 |
FROM runtime-dev AS dev |
| ... | ... |
@@ -233,6 +238,7 @@ RUN cd /docker-py \ |
| 233 | 233 |
&& pip install paramiko==2.4.2 \ |
| 234 | 234 |
&& pip install yamllint==1.5.0 \ |
| 235 | 235 |
&& pip install -r test-requirements.txt |
| 236 |
+COPY --from=rootlesskit /build/ /usr/local/bin/ |
|
| 236 | 237 |
|
| 237 | 238 |
ENV PATH=/usr/local/cli:$PATH |
| 238 | 239 |
ENV DOCKER_BUILDTAGS apparmor seccomp selinux |
| ... | ... |
@@ -13,6 +13,8 @@ var ( |
| 13 | 13 |
) |
| 14 | 14 |
|
| 15 | 15 |
// Dir returns the path to the configuration directory as specified by the DOCKER_CONFIG environment variable. |
| 16 |
+// If DOCKER_CONFIG is unset, Dir returns ~/.docker . |
|
| 17 |
+// Dir ignores XDG_CONFIG_HOME (same as the docker client). |
|
| 16 | 18 |
// TODO: this was copied from cli/config/configfile and should be removed once cmd/dockerd moves |
| 17 | 19 |
func Dir() string {
|
| 18 | 20 |
return configDir |
| ... | ... |
@@ -17,8 +17,20 @@ const ( |
| 17 | 17 |
) |
| 18 | 18 |
|
| 19 | 19 |
// installCommonConfigFlags adds flags to the pflag.FlagSet to configure the daemon |
| 20 |
-func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
|
| 20 |
+func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
|
|
| 21 | 21 |
var maxConcurrentDownloads, maxConcurrentUploads int |
| 22 |
+ defaultPidFile, err := getDefaultPidFile() |
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return err |
|
| 25 |
+ } |
|
| 26 |
+ defaultDataRoot, err := getDefaultDataRoot() |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return err |
|
| 29 |
+ } |
|
| 30 |
+ defaultExecRoot, err := getDefaultExecRoot() |
|
| 31 |
+ if err != nil {
|
|
| 32 |
+ return err |
|
| 33 |
+ } |
|
| 22 | 34 |
|
| 23 | 35 |
installRegistryServiceFlags(&conf.ServiceOptions, flags) |
| 24 | 36 |
|
| ... | ... |
@@ -80,6 +92,7 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
| 80 | 80 |
|
| 81 | 81 |
conf.MaxConcurrentDownloads = &maxConcurrentDownloads |
| 82 | 82 |
conf.MaxConcurrentUploads = &maxConcurrentUploads |
| 83 |
+ return nil |
|
| 83 | 84 |
} |
| 84 | 85 |
|
| 85 | 86 |
func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag.FlagSet) {
|
| ... | ... |
@@ -3,17 +3,48 @@ |
| 3 | 3 |
package main |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
+ "path/filepath" |
|
| 7 |
+ |
|
| 6 | 8 |
"github.com/docker/docker/api/types" |
| 7 | 9 |
"github.com/docker/docker/daemon/config" |
| 8 | 10 |
"github.com/docker/docker/opts" |
| 11 |
+ "github.com/docker/docker/pkg/homedir" |
|
| 12 |
+ "github.com/docker/docker/rootless" |
|
| 9 | 13 |
"github.com/spf13/pflag" |
| 10 | 14 |
) |
| 11 | 15 |
|
| 12 |
-var ( |
|
| 13 |
- defaultPidFile = "/var/run/docker.pid" |
|
| 14 |
- defaultDataRoot = "/var/lib/docker" |
|
| 15 |
- defaultExecRoot = "/var/run/docker" |
|
| 16 |
-) |
|
| 16 |
+func getDefaultPidFile() (string, error) {
|
|
| 17 |
+ if !rootless.RunningWithNonRootUsername() {
|
|
| 18 |
+ return "/var/run/docker.pid", nil |
|
| 19 |
+ } |
|
| 20 |
+ runtimeDir, err := homedir.GetRuntimeDir() |
|
| 21 |
+ if err != nil {
|
|
| 22 |
+ return "", err |
|
| 23 |
+ } |
|
| 24 |
+ return filepath.Join(runtimeDir, "docker.pid"), nil |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func getDefaultDataRoot() (string, error) {
|
|
| 28 |
+ if !rootless.RunningWithNonRootUsername() {
|
|
| 29 |
+ return "/var/lib/docker", nil |
|
| 30 |
+ } |
|
| 31 |
+ dataHome, err := homedir.GetDataHome() |
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ return "", err |
|
| 34 |
+ } |
|
| 35 |
+ return filepath.Join(dataHome, "docker"), nil |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+func getDefaultExecRoot() (string, error) {
|
|
| 39 |
+ if !rootless.RunningWithNonRootUsername() {
|
|
| 40 |
+ return "/var/run/docker", nil |
|
| 41 |
+ } |
|
| 42 |
+ runtimeDir, err := homedir.GetRuntimeDir() |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return "", err |
|
| 45 |
+ } |
|
| 46 |
+ return filepath.Join(runtimeDir, "docker"), nil |
|
| 47 |
+} |
|
| 17 | 48 |
|
| 18 | 49 |
// installUnixConfigFlags adds command-line options to the top-level flag parser for |
| 19 | 50 |
// the current process that are common across Unix platforms. |
| ... | ... |
@@ -5,14 +5,17 @@ package main |
| 5 | 5 |
import ( |
| 6 | 6 |
"github.com/docker/docker/daemon/config" |
| 7 | 7 |
"github.com/docker/docker/opts" |
| 8 |
+ "github.com/docker/docker/rootless" |
|
| 8 | 9 |
"github.com/docker/go-units" |
| 9 | 10 |
"github.com/spf13/pflag" |
| 10 | 11 |
) |
| 11 | 12 |
|
| 12 | 13 |
// installConfigFlags adds flags to the pflag.FlagSet to configure the daemon |
| 13 |
-func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
|
| 14 |
+func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
|
|
| 14 | 15 |
// First handle install flags which are consistent cross-platform |
| 15 |
- installCommonConfigFlags(conf, flags) |
|
| 16 |
+ if err := installCommonConfigFlags(conf, flags); err != nil {
|
|
| 17 |
+ return err |
|
| 18 |
+ } |
|
| 16 | 19 |
|
| 17 | 20 |
// Then install flags common to unix platforms |
| 18 | 21 |
installUnixConfigFlags(conf, flags) |
| ... | ... |
@@ -46,5 +49,7 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
| 46 | 46 |
flags.BoolVar(&conf.NoNewPrivileges, "no-new-privileges", false, "Set no-new-privileges by default for new containers") |
| 47 | 47 |
flags.StringVar(&conf.IpcMode, "default-ipc-mode", config.DefaultIpcMode, `Default mode for containers ipc ("shareable" | "private")`)
|
| 48 | 48 |
flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "Default address pools for node specific local networks") |
| 49 |
- |
|
| 49 |
+ // Mostly users don't need to set this flag explicitly. |
|
| 50 |
+ flags.BoolVar(&conf.Rootless, "rootless", rootless.RunningWithNonRootUsername(), "Enable rootless mode (experimental)") |
|
| 51 |
+ return nil |
|
| 50 | 52 |
} |
| ... | ... |
@@ -15,7 +15,8 @@ func TestDaemonParseShmSize(t *testing.T) {
|
| 15 | 15 |
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
| 16 | 16 |
|
| 17 | 17 |
conf := &config.Config{}
|
| 18 |
- installConfigFlags(conf, flags) |
|
| 18 |
+ err := installConfigFlags(conf, flags) |
|
| 19 |
+ assert.NilError(t, err) |
|
| 19 | 20 |
// By default `--default-shm-size=64M` |
| 20 | 21 |
assert.Check(t, is.Equal(int64(64*1024*1024), conf.ShmSize.Value())) |
| 21 | 22 |
assert.Check(t, flags.Set("default-shm-size", "128M"))
|
| ... | ... |
@@ -8,19 +8,28 @@ import ( |
| 8 | 8 |
"github.com/spf13/pflag" |
| 9 | 9 |
) |
| 10 | 10 |
|
| 11 |
-var ( |
|
| 12 |
- defaultPidFile string |
|
| 13 |
- defaultDataRoot = filepath.Join(os.Getenv("programdata"), "docker")
|
|
| 14 |
- defaultExecRoot = filepath.Join(os.Getenv("programdata"), "docker", "exec-root")
|
|
| 15 |
-) |
|
| 11 |
+func getDefaultPidFile() (string, error) {
|
|
| 12 |
+ return "", nil |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+func getDefaultDataRoot() (string, error) {
|
|
| 16 |
+ return filepath.Join(os.Getenv("programdata"), "docker"), nil
|
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+func getDefaultExecRoot() (string, error) {
|
|
| 20 |
+ return filepath.Join(os.Getenv("programdata"), "docker", "exec-root"), nil
|
|
| 21 |
+} |
|
| 16 | 22 |
|
| 17 | 23 |
// installConfigFlags adds flags to the pflag.FlagSet to configure the daemon |
| 18 |
-func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) {
|
|
| 24 |
+func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
|
|
| 19 | 25 |
// First handle install flags which are consistent cross-platform |
| 20 |
- installCommonConfigFlags(conf, flags) |
|
| 26 |
+ if err := installCommonConfigFlags(conf, flags); err != nil {
|
|
| 27 |
+ return err |
|
| 28 |
+ } |
|
| 21 | 29 |
|
| 22 | 30 |
// Then platform-specific install flags. |
| 23 | 31 |
flags.StringVar(&conf.BridgeConfig.FixedCIDR, "fixed-cidr", "", "IPv4 subnet for fixed IPs") |
| 24 | 32 |
flags.StringVarP(&conf.BridgeConfig.Iface, "bridge", "b", "", "Attach containers to a virtual switch") |
| 25 | 33 |
flags.StringVarP(&conf.SocketGroup, "group", "G", "", "Users or groups that can access the named pipe") |
| 34 |
+ return nil |
|
| 26 | 35 |
} |
| ... | ... |
@@ -40,12 +40,14 @@ import ( |
| 40 | 40 |
"github.com/docker/docker/libcontainerd/supervisor" |
| 41 | 41 |
dopts "github.com/docker/docker/opts" |
| 42 | 42 |
"github.com/docker/docker/pkg/authorization" |
| 43 |
+ "github.com/docker/docker/pkg/homedir" |
|
| 43 | 44 |
"github.com/docker/docker/pkg/jsonmessage" |
| 44 | 45 |
"github.com/docker/docker/pkg/pidfile" |
| 45 | 46 |
"github.com/docker/docker/pkg/plugingetter" |
| 46 | 47 |
"github.com/docker/docker/pkg/signal" |
| 47 | 48 |
"github.com/docker/docker/pkg/system" |
| 48 | 49 |
"github.com/docker/docker/plugin" |
| 50 |
+ "github.com/docker/docker/rootless" |
|
| 49 | 51 |
"github.com/docker/docker/runconfig" |
| 50 | 52 |
"github.com/docker/go-connections/tlsconfig" |
| 51 | 53 |
swarmapi "github.com/docker/swarmkit/api" |
| ... | ... |
@@ -97,6 +99,17 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
| 97 | 97 |
|
| 98 | 98 |
if cli.Config.Experimental {
|
| 99 | 99 |
logrus.Warn("Running experimental build")
|
| 100 |
+ if cli.Config.IsRootless() {
|
|
| 101 |
+ logrus.Warn("Running in rootless mode. Cgroups, AppArmor, and CRIU are disabled.")
|
|
| 102 |
+ } |
|
| 103 |
+ } else {
|
|
| 104 |
+ if cli.Config.IsRootless() {
|
|
| 105 |
+ return fmt.Errorf("rootless mode is supported only when running in experimental mode")
|
|
| 106 |
+ } |
|
| 107 |
+ } |
|
| 108 |
+ // return human-friendly error before creating files |
|
| 109 |
+ if runtime.GOOS == "linux" && os.Geteuid() != 0 {
|
|
| 110 |
+ 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")
|
|
| 100 | 111 |
} |
| 101 | 112 |
|
| 102 | 113 |
system.InitLCOW(cli.Config.Experimental) |
| ... | ... |
@@ -115,11 +128,14 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
| 115 | 115 |
return err |
| 116 | 116 |
} |
| 117 | 117 |
|
| 118 |
+ potentiallyUnderRuntimeDir := []string{cli.Config.ExecRoot}
|
|
| 119 |
+ |
|
| 118 | 120 |
if cli.Pidfile != "" {
|
| 119 | 121 |
pf, err := pidfile.New(cli.Pidfile) |
| 120 | 122 |
if err != nil {
|
| 121 | 123 |
return errors.Wrap(err, "failed to start daemon") |
| 122 | 124 |
} |
| 125 |
+ potentiallyUnderRuntimeDir = append(potentiallyUnderRuntimeDir, cli.Pidfile) |
|
| 123 | 126 |
defer func() {
|
| 124 | 127 |
if err := pf.Remove(); err != nil {
|
| 125 | 128 |
logrus.Error(err) |
| ... | ... |
@@ -127,6 +143,12 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
| 127 | 127 |
}() |
| 128 | 128 |
} |
| 129 | 129 |
|
| 130 |
+ // Set sticky bit if XDG_RUNTIME_DIR is set && the file is actually under XDG_RUNTIME_DIR |
|
| 131 |
+ if _, err := homedir.StickRuntimeDirContents(potentiallyUnderRuntimeDir); err != nil {
|
|
| 132 |
+ // StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset |
|
| 133 |
+ logrus.WithError(err).Warn("cannot set sticky bit on files under XDG_RUNTIME_DIR")
|
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 130 | 136 |
serverConfig, err := newAPIServerConfig(cli) |
| 131 | 137 |
if err != nil {
|
| 132 | 138 |
return errors.Wrap(err, "failed to create API server") |
| ... | ... |
@@ -140,7 +162,11 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
| 140 | 140 |
|
| 141 | 141 |
ctx, cancel := context.WithCancel(context.Background()) |
| 142 | 142 |
if cli.Config.ContainerdAddr == "" && runtime.GOOS != "windows" {
|
| 143 |
- if !systemContainerdRunning() {
|
|
| 143 |
+ systemContainerdAddr, ok, err := systemContainerdRunning(cli.Config.IsRootless()) |
|
| 144 |
+ if err != nil {
|
|
| 145 |
+ return errors.Wrap(err, "could not determine whether the system containerd is running") |
|
| 146 |
+ } |
|
| 147 |
+ if !ok {
|
|
| 144 | 148 |
opts, err := cli.getContainerdDaemonOpts() |
| 145 | 149 |
if err != nil {
|
| 146 | 150 |
cancel() |
| ... | ... |
@@ -157,7 +183,7 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) {
|
| 157 | 157 |
// Try to wait for containerd to shutdown |
| 158 | 158 |
defer r.WaitTimeout(10 * time.Second) |
| 159 | 159 |
} else {
|
| 160 |
- cli.Config.ContainerdAddr = containerddefaults.DefaultAddress |
|
| 160 |
+ cli.Config.ContainerdAddr = systemContainerdAddr |
|
| 161 | 161 |
} |
| 162 | 162 |
} |
| 163 | 163 |
defer cancel() |
| ... | ... |
@@ -403,9 +429,11 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
|
| 403 | 403 |
} |
| 404 | 404 |
|
| 405 | 405 |
if conf.TrustKeyPath == "" {
|
| 406 |
- conf.TrustKeyPath = filepath.Join( |
|
| 407 |
- getDaemonConfDir(conf.Root), |
|
| 408 |
- defaultTrustKeyFile) |
|
| 406 |
+ daemonConfDir, err := getDaemonConfDir(conf.Root) |
|
| 407 |
+ if err != nil {
|
|
| 408 |
+ return nil, err |
|
| 409 |
+ } |
|
| 410 |
+ conf.TrustKeyPath = filepath.Join(daemonConfDir, defaultTrustKeyFile) |
|
| 409 | 411 |
} |
| 410 | 412 |
|
| 411 | 413 |
if flags.Changed("graph") && flags.Changed("data-root") {
|
| ... | ... |
@@ -585,7 +613,7 @@ func loadListeners(cli *DaemonCli, serverConfig *apiserver.Config) ([]string, er |
| 585 | 585 |
var hosts []string |
| 586 | 586 |
for i := 0; i < len(cli.Config.Hosts); i++ {
|
| 587 | 587 |
var err error |
| 588 |
- if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, cli.Config.Hosts[i]); err != nil {
|
|
| 588 |
+ if cli.Config.Hosts[i], err = dopts.ParseHost(cli.Config.TLS, rootless.RunningWithNonRootUsername(), cli.Config.Hosts[i]); err != nil {
|
|
| 589 | 589 |
return nil, errors.Wrapf(err, "error parsing -H %s", cli.Config.Hosts[i]) |
| 590 | 590 |
} |
| 591 | 591 |
|
| ... | ... |
@@ -662,9 +690,17 @@ func validateAuthzPlugins(requestedPlugins []string, pg plugingetter.PluginGette |
| 662 | 662 |
return nil |
| 663 | 663 |
} |
| 664 | 664 |
|
| 665 |
-func systemContainerdRunning() bool {
|
|
| 666 |
- _, err := os.Lstat(containerddefaults.DefaultAddress) |
|
| 667 |
- return err == nil |
|
| 665 |
+func systemContainerdRunning(isRootless bool) (string, bool, error) {
|
|
| 666 |
+ addr := containerddefaults.DefaultAddress |
|
| 667 |
+ if isRootless {
|
|
| 668 |
+ runtimeDir, err := homedir.GetRuntimeDir() |
|
| 669 |
+ if err != nil {
|
|
| 670 |
+ return "", false, err |
|
| 671 |
+ } |
|
| 672 |
+ addr = filepath.Join(runtimeDir, "containerd", "containerd.sock") |
|
| 673 |
+ } |
|
| 674 |
+ _, err := os.Lstat(addr) |
|
| 675 |
+ return addr, err == nil, nil |
|
| 668 | 676 |
} |
| 669 | 677 |
|
| 670 | 678 |
// configureDaemonLogs sets the logrus logging level and formatting |
| ... | ... |
@@ -11,18 +11,22 @@ import ( |
| 11 | 11 |
"gotest.tools/fs" |
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 |
-func defaultOptions(configFile string) *daemonOptions {
|
|
| 14 |
+func defaultOptions(t *testing.T, configFile string) *daemonOptions {
|
|
| 15 | 15 |
opts := newDaemonOptions(&config.Config{})
|
| 16 | 16 |
opts.flags = &pflag.FlagSet{}
|
| 17 | 17 |
opts.InstallFlags(opts.flags) |
| 18 |
- installConfigFlags(opts.daemonConfig, opts.flags) |
|
| 18 |
+ if err := installConfigFlags(opts.daemonConfig, opts.flags); err != nil {
|
|
| 19 |
+ t.Fatal(err) |
|
| 20 |
+ } |
|
| 21 |
+ defaultDaemonConfigFile, err := getDefaultDaemonConfigFile() |
|
| 22 |
+ assert.NilError(t, err) |
|
| 19 | 23 |
opts.flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "") |
| 20 | 24 |
opts.configFile = configFile |
| 21 | 25 |
return opts |
| 22 | 26 |
} |
| 23 | 27 |
|
| 24 | 28 |
func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
|
| 25 |
- opts := defaultOptions("")
|
|
| 29 |
+ opts := defaultOptions(t, "") |
|
| 26 | 30 |
opts.Debug = true |
| 27 | 31 |
|
| 28 | 32 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| ... | ... |
@@ -34,7 +38,7 @@ func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
|
| 34 | 34 |
} |
| 35 | 35 |
|
| 36 | 36 |
func TestLoadDaemonCliConfigWithTLS(t *testing.T) {
|
| 37 |
- opts := defaultOptions("")
|
|
| 37 |
+ opts := defaultOptions(t, "") |
|
| 38 | 38 |
opts.TLSOptions.CAFile = "/tmp/ca.pem" |
| 39 | 39 |
opts.TLS = true |
| 40 | 40 |
|
| ... | ... |
@@ -49,7 +53,7 @@ func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
|
| 49 | 49 |
defer tempFile.Remove() |
| 50 | 50 |
configFile := tempFile.Path() |
| 51 | 51 |
|
| 52 |
- opts := defaultOptions(configFile) |
|
| 52 |
+ opts := defaultOptions(t, configFile) |
|
| 53 | 53 |
flags := opts.flags |
| 54 | 54 |
|
| 55 | 55 |
assert.Check(t, flags.Set("config-file", configFile))
|
| ... | ... |
@@ -65,7 +69,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
|
| 65 | 65 |
defer tempFile.Remove() |
| 66 | 66 |
configFile := tempFile.Path() |
| 67 | 67 |
|
| 68 |
- opts := defaultOptions(configFile) |
|
| 68 |
+ opts := defaultOptions(t, configFile) |
|
| 69 | 69 |
flags := opts.flags |
| 70 | 70 |
|
| 71 | 71 |
assert.Check(t, flags.Set("config-file", configFile))
|
| ... | ... |
@@ -77,7 +81,7 @@ func TestLoadDaemonCliWithConflictingNodeGenericResources(t *testing.T) {
|
| 77 | 77 |
} |
| 78 | 78 |
|
| 79 | 79 |
func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
|
| 80 |
- opts := defaultOptions("")
|
|
| 80 |
+ opts := defaultOptions(t, "") |
|
| 81 | 81 |
flags := opts.flags |
| 82 | 82 |
|
| 83 | 83 |
assert.Check(t, flags.Set("label", "foo=bar"))
|
| ... | ... |
@@ -88,7 +92,7 @@ func TestLoadDaemonCliWithConflictingLabels(t *testing.T) {
|
| 88 | 88 |
} |
| 89 | 89 |
|
| 90 | 90 |
func TestLoadDaemonCliWithDuplicateLabels(t *testing.T) {
|
| 91 |
- opts := defaultOptions("")
|
|
| 91 |
+ opts := defaultOptions(t, "") |
|
| 92 | 92 |
flags := opts.flags |
| 93 | 93 |
|
| 94 | 94 |
assert.Check(t, flags.Set("label", "foo=the-same"))
|
| ... | ... |
@@ -102,7 +106,7 @@ func TestLoadDaemonCliConfigWithTLSVerify(t *testing.T) {
|
| 102 | 102 |
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": true}`))
|
| 103 | 103 |
defer tempFile.Remove() |
| 104 | 104 |
|
| 105 |
- opts := defaultOptions(tempFile.Path()) |
|
| 105 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 106 | 106 |
opts.TLSOptions.CAFile = "/tmp/ca.pem" |
| 107 | 107 |
|
| 108 | 108 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| ... | ... |
@@ -115,7 +119,7 @@ func TestLoadDaemonCliConfigWithExplicitTLSVerifyFalse(t *testing.T) {
|
| 115 | 115 |
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"tlsverify": false}`))
|
| 116 | 116 |
defer tempFile.Remove() |
| 117 | 117 |
|
| 118 |
- opts := defaultOptions(tempFile.Path()) |
|
| 118 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 119 | 119 |
opts.TLSOptions.CAFile = "/tmp/ca.pem" |
| 120 | 120 |
|
| 121 | 121 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| ... | ... |
@@ -128,7 +132,7 @@ func TestLoadDaemonCliConfigWithoutTLSVerify(t *testing.T) {
|
| 128 | 128 |
tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
|
| 129 | 129 |
defer tempFile.Remove() |
| 130 | 130 |
|
| 131 |
- opts := defaultOptions(tempFile.Path()) |
|
| 131 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 132 | 132 |
opts.TLSOptions.CAFile = "/tmp/ca.pem" |
| 133 | 133 |
|
| 134 | 134 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| ... | ... |
@@ -141,7 +145,7 @@ func TestLoadDaemonCliConfigWithLogLevel(t *testing.T) {
|
| 141 | 141 |
tempFile := fs.NewFile(t, "config", fs.WithContent(`{"log-level": "warn"}`))
|
| 142 | 142 |
defer tempFile.Remove() |
| 143 | 143 |
|
| 144 |
- opts := defaultOptions(tempFile.Path()) |
|
| 144 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 145 | 145 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| 146 | 146 |
assert.NilError(t, err) |
| 147 | 147 |
assert.Assert(t, loadedConfig != nil) |
| ... | ... |
@@ -153,7 +157,7 @@ func TestLoadDaemonConfigWithEmbeddedOptions(t *testing.T) {
|
| 153 | 153 |
tempFile := fs.NewFile(t, "config", fs.WithContent(content)) |
| 154 | 154 |
defer tempFile.Remove() |
| 155 | 155 |
|
| 156 |
- opts := defaultOptions(tempFile.Path()) |
|
| 156 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 157 | 157 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| 158 | 158 |
assert.NilError(t, err) |
| 159 | 159 |
assert.Assert(t, loadedConfig != nil) |
| ... | ... |
@@ -170,7 +174,7 @@ func TestLoadDaemonConfigWithRegistryOptions(t *testing.T) {
|
| 170 | 170 |
tempFile := fs.NewFile(t, "config", fs.WithContent(content)) |
| 171 | 171 |
defer tempFile.Remove() |
| 172 | 172 |
|
| 173 |
- opts := defaultOptions(tempFile.Path()) |
|
| 173 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 174 | 174 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| 175 | 175 |
assert.NilError(t, err) |
| 176 | 176 |
assert.Assert(t, loadedConfig != nil) |
| ... | ... |
@@ -15,11 +15,33 @@ import ( |
| 15 | 15 |
"github.com/docker/docker/daemon" |
| 16 | 16 |
"github.com/docker/docker/daemon/config" |
| 17 | 17 |
"github.com/docker/docker/libcontainerd/supervisor" |
| 18 |
+ "github.com/docker/docker/pkg/homedir" |
|
| 19 |
+ "github.com/docker/docker/rootless" |
|
| 18 | 20 |
"github.com/docker/libnetwork/portallocator" |
| 19 | 21 |
"golang.org/x/sys/unix" |
| 20 | 22 |
) |
| 21 | 23 |
|
| 22 |
-const defaultDaemonConfigFile = "/etc/docker/daemon.json" |
|
| 24 |
+func getDefaultDaemonConfigDir() (string, error) {
|
|
| 25 |
+ if !rootless.RunningWithNonRootUsername() {
|
|
| 26 |
+ return "/etc/docker", nil |
|
| 27 |
+ } |
|
| 28 |
+ // NOTE: CLI uses ~/.docker while the daemon uses ~/.config/docker, because |
|
| 29 |
+ // ~/.docker was not designed to store daemon configurations. |
|
| 30 |
+ // In future, the daemon directory may be renamed to ~/.config/moby-engine (?). |
|
| 31 |
+ configHome, err := homedir.GetConfigHome() |
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ return "", nil |
|
| 34 |
+ } |
|
| 35 |
+ return filepath.Join(configHome, "docker"), nil |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+func getDefaultDaemonConfigFile() (string, error) {
|
|
| 39 |
+ dir, err := getDefaultDaemonConfigDir() |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return "", err |
|
| 42 |
+ } |
|
| 43 |
+ return filepath.Join(dir, "daemon.json"), nil |
|
| 44 |
+} |
|
| 23 | 45 |
|
| 24 | 46 |
// setDefaultUmask sets the umask to 0022 to avoid problems |
| 25 | 47 |
// caused by custom umask |
| ... | ... |
@@ -33,8 +55,8 @@ func setDefaultUmask() error {
|
| 33 | 33 |
return nil |
| 34 | 34 |
} |
| 35 | 35 |
|
| 36 |
-func getDaemonConfDir(_ string) string {
|
|
| 37 |
- return "/etc/docker" |
|
| 36 |
+func getDaemonConfDir(_ string) (string, error) {
|
|
| 37 |
+ return getDefaultDaemonConfigDir() |
|
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 | 40 |
func (cli *DaemonCli) getPlatformContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) {
|
| ... | ... |
@@ -16,7 +16,7 @@ func TestLoadDaemonCliConfigWithDaemonFlags(t *testing.T) {
|
| 16 | 16 |
tempFile := fs.NewFile(t, "config", fs.WithContent(content)) |
| 17 | 17 |
defer tempFile.Remove() |
| 18 | 18 |
|
| 19 |
- opts := defaultOptions(tempFile.Path()) |
|
| 19 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 20 | 20 |
opts.Debug = true |
| 21 | 21 |
opts.LogLevel = "info" |
| 22 | 22 |
assert.Check(t, opts.flags.Set("selinux-enabled", "true"))
|
| ... | ... |
@@ -37,7 +37,7 @@ func TestLoadDaemonConfigWithNetwork(t *testing.T) {
|
| 37 | 37 |
tempFile := fs.NewFile(t, "config", fs.WithContent(content)) |
| 38 | 38 |
defer tempFile.Remove() |
| 39 | 39 |
|
| 40 |
- opts := defaultOptions(tempFile.Path()) |
|
| 40 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 41 | 41 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| 42 | 42 |
assert.NilError(t, err) |
| 43 | 43 |
assert.Assert(t, loadedConfig != nil) |
| ... | ... |
@@ -54,7 +54,7 @@ func TestLoadDaemonConfigWithMapOptions(t *testing.T) {
|
| 54 | 54 |
tempFile := fs.NewFile(t, "config", fs.WithContent(content)) |
| 55 | 55 |
defer tempFile.Remove() |
| 56 | 56 |
|
| 57 |
- opts := defaultOptions(tempFile.Path()) |
|
| 57 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 58 | 58 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| 59 | 59 |
assert.NilError(t, err) |
| 60 | 60 |
assert.Assert(t, loadedConfig != nil) |
| ... | ... |
@@ -71,7 +71,7 @@ func TestLoadDaemonConfigWithTrueDefaultValues(t *testing.T) {
|
| 71 | 71 |
tempFile := fs.NewFile(t, "config", fs.WithContent(content)) |
| 72 | 72 |
defer tempFile.Remove() |
| 73 | 73 |
|
| 74 |
- opts := defaultOptions(tempFile.Path()) |
|
| 74 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 75 | 75 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| 76 | 76 |
assert.NilError(t, err) |
| 77 | 77 |
assert.Assert(t, loadedConfig != nil) |
| ... | ... |
@@ -90,7 +90,7 @@ func TestLoadDaemonConfigWithTrueDefaultValuesLeaveDefaults(t *testing.T) {
|
| 90 | 90 |
tempFile := fs.NewFile(t, "config", fs.WithContent(`{}`))
|
| 91 | 91 |
defer tempFile.Remove() |
| 92 | 92 |
|
| 93 |
- opts := defaultOptions(tempFile.Path()) |
|
| 93 |
+ opts := defaultOptions(t, tempFile.Path()) |
|
| 94 | 94 |
loadedConfig, err := loadDaemonCliConfig(opts) |
| 95 | 95 |
assert.NilError(t, err) |
| 96 | 96 |
assert.Assert(t, loadedConfig != nil) |
| ... | ... |
@@ -12,15 +12,17 @@ import ( |
| 12 | 12 |
"golang.org/x/sys/windows" |
| 13 | 13 |
) |
| 14 | 14 |
|
| 15 |
-var defaultDaemonConfigFile = "" |
|
| 15 |
+func getDefaultDaemonConfigFile() (string, error) {
|
|
| 16 |
+ return "", nil |
|
| 17 |
+} |
|
| 16 | 18 |
|
| 17 | 19 |
// setDefaultUmask doesn't do anything on windows |
| 18 | 20 |
func setDefaultUmask() error {
|
| 19 | 21 |
return nil |
| 20 | 22 |
} |
| 21 | 23 |
|
| 22 |
-func getDaemonConfDir(root string) string {
|
|
| 23 |
- return filepath.Join(root, `\config`) |
|
| 24 |
+func getDaemonConfDir(root string) (string, error) {
|
|
| 25 |
+ return filepath.Join(root, `\config`), nil |
|
| 24 | 26 |
} |
| 25 | 27 |
|
| 26 | 28 |
// preNotifySystem sends a message to the host when the API is active, but before the daemon is |
| ... | ... |
@@ -16,7 +16,7 @@ import ( |
| 16 | 16 |
"github.com/spf13/cobra" |
| 17 | 17 |
) |
| 18 | 18 |
|
| 19 |
-func newDaemonCommand() *cobra.Command {
|
|
| 19 |
+func newDaemonCommand() (*cobra.Command, error) {
|
|
| 20 | 20 |
opts := newDaemonOptions(config.New()) |
| 21 | 21 |
|
| 22 | 22 |
cmd := &cobra.Command{
|
| ... | ... |
@@ -36,12 +36,18 @@ func newDaemonCommand() *cobra.Command {
|
| 36 | 36 |
|
| 37 | 37 |
flags := cmd.Flags() |
| 38 | 38 |
flags.BoolP("version", "v", false, "Print version information and quit")
|
| 39 |
+ defaultDaemonConfigFile, err := getDefaultDaemonConfigFile() |
|
| 40 |
+ if err != nil {
|
|
| 41 |
+ return nil, err |
|
| 42 |
+ } |
|
| 39 | 43 |
flags.StringVar(&opts.configFile, "config-file", defaultDaemonConfigFile, "Daemon configuration file") |
| 40 | 44 |
opts.InstallFlags(flags) |
| 41 |
- installConfigFlags(opts.daemonConfig, flags) |
|
| 45 |
+ if err := installConfigFlags(opts.daemonConfig, flags); err != nil {
|
|
| 46 |
+ return nil, err |
|
| 47 |
+ } |
|
| 42 | 48 |
installServiceFlags(flags) |
| 43 | 49 |
|
| 44 |
- return cmd |
|
| 50 |
+ return cmd, nil |
|
| 45 | 51 |
} |
| 46 | 52 |
|
| 47 | 53 |
func init() {
|
| ... | ... |
@@ -72,10 +78,17 @@ func main() {
|
| 72 | 72 |
logrus.SetOutput(stderr) |
| 73 | 73 |
} |
| 74 | 74 |
|
| 75 |
- cmd := newDaemonCommand() |
|
| 76 |
- cmd.SetOutput(stdout) |
|
| 77 |
- if err := cmd.Execute(); err != nil {
|
|
| 75 |
+ onError := func(err error) {
|
|
| 78 | 76 |
fmt.Fprintf(stderr, "%s\n", err) |
| 79 | 77 |
os.Exit(1) |
| 80 | 78 |
} |
| 79 |
+ |
|
| 80 |
+ cmd, err := newDaemonCommand() |
|
| 81 |
+ if err != nil {
|
|
| 82 |
+ onError(err) |
|
| 83 |
+ } |
|
| 84 |
+ cmd.SetOutput(stdout) |
|
| 85 |
+ if err := cmd.Execute(); err != nil {
|
|
| 86 |
+ onError(err) |
|
| 87 |
+ } |
|
| 81 | 88 |
} |
| ... | ... |
@@ -49,6 +49,8 @@ func newDaemonOptions(config *config.Config) *daemonOptions {
|
| 49 | 49 |
// InstallFlags adds flags for the common options on the FlagSet |
| 50 | 50 |
func (o *daemonOptions) InstallFlags(flags *pflag.FlagSet) {
|
| 51 | 51 |
if dockerCertPath == "" {
|
| 52 |
+ // cliconfig.Dir returns $DOCKER_CONFIG or ~/.docker. |
|
| 53 |
+ // cliconfig.Dir does not look up $XDG_CONFIG_HOME |
|
| 52 | 54 |
dockerCertPath = cliconfig.Dir() |
| 53 | 55 |
} |
| 54 | 56 |
|
| 55 | 57 |
new file mode 100755 |
| ... | ... |
@@ -0,0 +1,77 @@ |
| 0 |
+#!/bin/sh |
|
| 1 |
+# dockerd-rootless.sh executes dockerd in rootless mode. |
|
| 2 |
+# |
|
| 3 |
+# Usage: dockerd-rootless.sh --experimental [DOCKERD_OPTIONS] |
|
| 4 |
+# Currently, specifying --experimental is mandatory. |
|
| 5 |
+# |
|
| 6 |
+# External dependencies: |
|
| 7 |
+# * newuidmap and newgidmap needs to be installed. |
|
| 8 |
+# * /etc/subuid and /etc/subgid needs to be configured for the current user. |
|
| 9 |
+# * Either slirp4netns (v0.3+) or VPNKit needs to be installed. |
|
| 10 |
+# |
|
| 11 |
+# See the documentation for the further information. |
|
| 12 |
+ |
|
| 13 |
+set -e -x |
|
| 14 |
+if ! [ -w $XDG_RUNTIME_DIR ]; then |
|
| 15 |
+ echo "XDG_RUNTIME_DIR needs to be set and writable" |
|
| 16 |
+ exit 1 |
|
| 17 |
+fi |
|
| 18 |
+if ! [ -w $HOME ]; then |
|
| 19 |
+ echo "HOME needs to be set and writable" |
|
| 20 |
+ exit 1 |
|
| 21 |
+fi |
|
| 22 |
+ |
|
| 23 |
+rootlesskit="" |
|
| 24 |
+for f in docker-rootlesskit rootlesskit; do |
|
| 25 |
+ if which $f >/dev/null 2>&1; then |
|
| 26 |
+ rootlesskit=$f |
|
| 27 |
+ break |
|
| 28 |
+ fi |
|
| 29 |
+done |
|
| 30 |
+if [ -z $rootlesskit ]; then |
|
| 31 |
+ echo "rootlesskit needs to be installed" |
|
| 32 |
+ exit 1 |
|
| 33 |
+fi |
|
| 34 |
+ |
|
| 35 |
+net="" |
|
| 36 |
+mtu="" |
|
| 37 |
+if which slirp4netns >/dev/null 2>&1; then |
|
| 38 |
+ if slirp4netns --help | grep -- --disable-host-loopback; then |
|
| 39 |
+ net=slirp4netns |
|
| 40 |
+ mtu=65520 |
|
| 41 |
+ else |
|
| 42 |
+ echo "slirp4netns does not support --disable-host-loopback. Falling back to VPNKit." |
|
| 43 |
+ fi |
|
| 44 |
+fi |
|
| 45 |
+if [ -z $net ]; then |
|
| 46 |
+ if which vpnkit >/dev/null 2>&1; then |
|
| 47 |
+ net=vpnkit |
|
| 48 |
+ mtu=1500 |
|
| 49 |
+ else |
|
| 50 |
+ echo "Either slirp4netns (v0.3+) or vpnkit needs to be installed" |
|
| 51 |
+ exit 1 |
|
| 52 |
+ fi |
|
| 53 |
+fi |
|
| 54 |
+ |
|
| 55 |
+if [ -z $_DOCKERD_ROOTLESS_CHILD ]; then |
|
| 56 |
+ _DOCKERD_ROOTLESS_CHILD=1 |
|
| 57 |
+ export _DOCKERD_ROOTLESS_CHILD |
|
| 58 |
+ # Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces.
|
|
| 59 |
+ # |
|
| 60 |
+ # --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks |
|
| 61 |
+ # * /etc: copy-up is required so as to prevent `/etc/resolv.conf` in the |
|
| 62 |
+ # namespace from being unexpectedly unmounted when `/etc/resolv.conf` is recreated on the host |
|
| 63 |
+ # (by either systemd-networkd or NetworkManager) |
|
| 64 |
+ # * /run: copy-up is required so that we can create /run/docker (hardcoded for plugins) in our namespace |
|
| 65 |
+ $rootlesskit \ |
|
| 66 |
+ --net=$net --mtu=$mtu --disable-host-loopback \ |
|
| 67 |
+ --copy-up=/etc --copy-up=/run \ |
|
| 68 |
+ $DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS \ |
|
| 69 |
+ $0 $@ |
|
| 70 |
+else |
|
| 71 |
+ [ $_DOCKERD_ROOTLESS_CHILD = 1 ] |
|
| 72 |
+ # remove the symlinks for the existing files in the parent namespace if any, |
|
| 73 |
+ # so that we can create our own files in our mount namespace. |
|
| 74 |
+ rm -f /run/docker /run/xtables.lock |
|
| 75 |
+ dockerd $@ |
|
| 76 |
+fi |
| ... | ... |
@@ -39,6 +39,7 @@ type Config struct {
|
| 39 | 39 |
IpcMode string `json:"default-ipc-mode,omitempty"` |
| 40 | 40 |
// ResolvConf is the path to the configuration of the host resolver |
| 41 | 41 |
ResolvConf string `json:"resolv-conf,omitempty"` |
| 42 |
+ Rootless bool `json:"rootless,omitempty"` |
|
| 42 | 43 |
} |
| 43 | 44 |
|
| 44 | 45 |
// BridgeConfig stores all the bridge driver specific |
| ... | ... |
@@ -87,3 +88,8 @@ func verifyDefaultIpcMode(mode string) error {
|
| 87 | 87 |
func (conf *Config) ValidatePlatformConfig() error {
|
| 88 | 88 |
return verifyDefaultIpcMode(conf.IpcMode) |
| 89 | 89 |
} |
| 90 |
+ |
|
| 91 |
+// IsRootless returns conf.Rootless |
|
| 92 |
+func (conf *Config) IsRootless() bool {
|
|
| 93 |
+ return conf.Rootless |
|
| 94 |
+} |
| ... | ... |
@@ -800,6 +800,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S |
| 800 | 800 |
logrus.Warnf("Failed to configure golang's threads limit: %v", err)
|
| 801 | 801 |
} |
| 802 | 802 |
|
| 803 |
+ // ensureDefaultAppArmorProfile does nothing if apparmor is disabled |
|
| 803 | 804 |
if err := ensureDefaultAppArmorProfile(); err != nil {
|
| 804 | 805 |
logrus.Errorf(err.Error()) |
| 805 | 806 |
} |
| ... | ... |
@@ -745,9 +745,6 @@ func verifyDaemonSettings(conf *config.Config) error {
|
| 745 | 745 |
|
| 746 | 746 |
// checkSystem validates platform-specific requirements |
| 747 | 747 |
func checkSystem() error {
|
| 748 |
- if os.Geteuid() != 0 {
|
|
| 749 |
- return fmt.Errorf("The Docker daemon needs to be run as root")
|
|
| 750 |
- } |
|
| 751 | 748 |
return checkKernel() |
| 752 | 749 |
} |
| 753 | 750 |
|
| ... | ... |
@@ -175,6 +175,9 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf |
| 175 | 175 |
if rootIDs := daemon.idMapping.RootPair(); rootIDs.UID != 0 || rootIDs.GID != 0 {
|
| 176 | 176 |
securityOptions = append(securityOptions, "name=userns") |
| 177 | 177 |
} |
| 178 |
+ if daemon.configStoreRootless() {
|
|
| 179 |
+ securityOptions = append(securityOptions, "name=rootless") |
|
| 180 |
+ } |
|
| 178 | 181 |
v.SecurityOptions = securityOptions |
| 179 | 182 |
} |
| 180 | 183 |
|
| ... | ... |
@@ -8,6 +8,7 @@ import ( |
| 8 | 8 |
"strconv" |
| 9 | 9 |
|
| 10 | 10 |
"github.com/coreos/go-systemd/activation" |
| 11 |
+ "github.com/docker/docker/pkg/homedir" |
|
| 11 | 12 |
"github.com/docker/go-connections/sockets" |
| 12 | 13 |
"github.com/sirupsen/logrus" |
| 13 | 14 |
) |
| ... | ... |
@@ -45,6 +46,10 @@ func Init(proto, addr, socketGroup string, tlsConfig *tls.Config) ([]net.Listene |
| 45 | 45 |
if err != nil {
|
| 46 | 46 |
return nil, fmt.Errorf("can't create unix socket %s: %v", addr, err)
|
| 47 | 47 |
} |
| 48 |
+ if _, err := homedir.StickRuntimeDirContents([]string{addr}); err != nil {
|
|
| 49 |
+ // StickRuntimeDirContents returns nil error if XDG_RUNTIME_DIR is just unset |
|
| 50 |
+ logrus.WithError(err).Warnf("cannot set sticky bit on socket %s under XDG_RUNTIME_DIR", addr)
|
|
| 51 |
+ } |
|
| 48 | 52 |
ls = append(ls, l) |
| 49 | 53 |
default: |
| 50 | 54 |
return nil, fmt.Errorf("invalid protocol format: %q", proto)
|
| ... | ... |
@@ -17,10 +17,12 @@ import ( |
| 17 | 17 |
"github.com/docker/docker/oci/caps" |
| 18 | 18 |
"github.com/docker/docker/pkg/idtools" |
| 19 | 19 |
"github.com/docker/docker/pkg/mount" |
| 20 |
+ "github.com/docker/docker/rootless/specconv" |
|
| 20 | 21 |
volumemounts "github.com/docker/docker/volume/mounts" |
| 21 | 22 |
"github.com/opencontainers/runc/libcontainer/apparmor" |
| 22 | 23 |
"github.com/opencontainers/runc/libcontainer/cgroups" |
| 23 | 24 |
"github.com/opencontainers/runc/libcontainer/devices" |
| 25 |
+ rsystem "github.com/opencontainers/runc/libcontainer/system" |
|
| 24 | 26 |
"github.com/opencontainers/runc/libcontainer/user" |
| 25 | 27 |
"github.com/opencontainers/runtime-spec/specs-go" |
| 26 | 28 |
"github.com/pkg/errors" |
| ... | ... |
@@ -89,7 +91,7 @@ func setDevices(s *specs.Spec, c *container.Container) error {
|
| 89 | 89 |
// Build lists of devices allowed and created within the container. |
| 90 | 90 |
var devs []specs.LinuxDevice |
| 91 | 91 |
devPermissions := s.Linux.Resources.Devices |
| 92 |
- if c.HostConfig.Privileged {
|
|
| 92 |
+ if c.HostConfig.Privileged && !rsystem.RunningInUserNS() {
|
|
| 93 | 93 |
hostDevices, err := devices.HostDevices() |
| 94 | 94 |
if err != nil {
|
| 95 | 95 |
return err |
| ... | ... |
@@ -867,6 +869,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, e |
| 867 | 867 |
s.Linux.ReadonlyPaths = c.HostConfig.ReadonlyPaths |
| 868 | 868 |
} |
| 869 | 869 |
|
| 870 |
+ if daemon.configStore.Rootless {
|
|
| 871 |
+ if err := specconv.ToRootless(&s); err != nil {
|
|
| 872 |
+ return nil, err |
|
| 873 |
+ } |
|
| 874 |
+ } |
|
| 870 | 875 |
return &s, nil |
| 871 | 876 |
} |
| 872 | 877 |
|
| 873 | 878 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,92 @@ |
| 0 |
+# Rootless mode (Experimental) |
|
| 1 |
+ |
|
| 2 |
+The rootless mode allows running `dockerd` as an unprivileged user, using `user_namespaces(7)`, `mount_namespaces(7)`, `network_namespaces(7)`. |
|
| 3 |
+ |
|
| 4 |
+No SETUID/SETCAP binary is required except `newuidmap` and `newgidmap`. |
|
| 5 |
+ |
|
| 6 |
+## Requirements |
|
| 7 |
+* `newuidmap` and `newgidmap` need to be installed on the host. These commands are provided by the `uidmap` package on most distros. |
|
| 8 |
+ |
|
| 9 |
+* `/etc/subuid` and `/etc/subgid` should contain >= 65536 sub-IDs. e.g. `penguin:231072:65536`. |
|
| 10 |
+ |
|
| 11 |
+```console |
|
| 12 |
+$ id -u |
|
| 13 |
+1001 |
|
| 14 |
+$ whoami |
|
| 15 |
+penguin |
|
| 16 |
+$ grep ^$(whoami): /etc/subuid |
|
| 17 |
+penguin:231072:65536 |
|
| 18 |
+$ grep ^$(whoami): /etc/subgid |
|
| 19 |
+penguin:231072:65536 |
|
| 20 |
+``` |
|
| 21 |
+ |
|
| 22 |
+* 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. |
|
| 23 |
+ |
|
| 24 |
+### Distribution-specific hint |
|
| 25 |
+ |
|
| 26 |
+#### Debian (excluding Ubuntu) |
|
| 27 |
+* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required |
|
| 28 |
+ |
|
| 29 |
+#### Arch Linux |
|
| 30 |
+* `sudo sh -c "echo 1 > /proc/sys/kernel/unprivileged_userns_clone"` is required |
|
| 31 |
+ |
|
| 32 |
+#### openSUSE |
|
| 33 |
+* `sudo modprobe ip_tables iptable_mangle iptable_nat iptable_filter` is required. (This is likely to be required on other distros as well) |
|
| 34 |
+ |
|
| 35 |
+#### RHEL/CentOS 7 |
|
| 36 |
+* `sudo sh -c "echo 28633 > /proc/sys/user/max_user_namespaces"` is required |
|
| 37 |
+* [COPR package `vbatts/shadow-utils-newxidmap`](https://copr.fedorainfracloud.org/coprs/vbatts/shadow-utils-newxidmap/) needs to be installed |
|
| 38 |
+ |
|
| 39 |
+## Restrictions |
|
| 40 |
+ |
|
| 41 |
+* 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. |
|
| 42 |
+* Following features are not supported: |
|
| 43 |
+ * Cgroups (including `docker top`, which depends on the cgroups device controller) |
|
| 44 |
+ * Apparmor |
|
| 45 |
+ * Checkpoint |
|
| 46 |
+ * Overlay network |
|
| 47 |
+ |
|
| 48 |
+## Usage |
|
| 49 |
+ |
|
| 50 |
+### Daemon |
|
| 51 |
+ |
|
| 52 |
+You need to run `dockerd-rootless.sh` instead of `dockerd`. |
|
| 53 |
+ |
|
| 54 |
+```console |
|
| 55 |
+$ dockerd-rootless.sh --experimental" |
|
| 56 |
+``` |
|
| 57 |
+As Rootless mode is experimental per se, currently you always need to run `dockerd-rootless.sh` with `--experimental`. |
|
| 58 |
+ |
|
| 59 |
+Remarks: |
|
| 60 |
+* The socket path is set to `$XDG_RUNTIME_DIR/docker.sock` by default. `$XDG_RUNTIME_DIR` is typically set to `/run/user/$UID`. |
|
| 61 |
+* The data dir is set to `~/.local/share/docker` by default. |
|
| 62 |
+* The exec dir is set to `$XDG_RUNTIME_DIR/docker` by default. |
|
| 63 |
+* The daemon config dir is set to `~/.config/docker` (not `~/.docker`, which is used by the client) by default. |
|
| 64 |
+* 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)`. |
|
| 65 |
+ |
|
| 66 |
+### Client |
|
| 67 |
+ |
|
| 68 |
+You can just use the upstream Docker client but you need to set the socket path explicitly. |
|
| 69 |
+ |
|
| 70 |
+```console |
|
| 71 |
+$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d nginx |
|
| 72 |
+``` |
|
| 73 |
+ |
|
| 74 |
+### Exposing ports |
|
| 75 |
+ |
|
| 76 |
+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. |
|
| 77 |
+ |
|
| 78 |
+```console |
|
| 79 |
+$ docker -H unix://$XDG_RUNTIME_DIR/docker.sock run -d -p 80:80 nginx |
|
| 80 |
+$ 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" |
|
| 81 |
+``` |
|
| 82 |
+ |
|
| 83 |
+In future, `dockerd` will be able to expose the ports automatically. |
|
| 84 |
+ |
|
| 85 |
+### Routing ping packets |
|
| 86 |
+ |
|
| 87 |
+To route ping packets, you need to set up `net.ipv4.ping_group_range` properly as the root. |
|
| 88 |
+ |
|
| 89 |
+```console |
|
| 90 |
+$ sudo sh -c "echo 0 2147483647 > /proc/sys/net/ipv4/ping_group_range" |
|
| 91 |
+``` |
| 0 | 92 |
new file mode 100755 |
| ... | ... |
@@ -0,0 +1,34 @@ |
| 0 |
+#!/bin/sh |
|
| 1 |
+ |
|
| 2 |
+# v0.3.0-alpha.0 |
|
| 3 |
+ROOTLESSKIT_COMMIT=3c4582e950e3a67795c2832179c125b258b78124 |
|
| 4 |
+ |
|
| 5 |
+install_rootlesskit() {
|
|
| 6 |
+ case "$1" in |
|
| 7 |
+ "dynamic") |
|
| 8 |
+ install_rootlesskit_dynamic |
|
| 9 |
+ return |
|
| 10 |
+ ;; |
|
| 11 |
+ "") |
|
| 12 |
+ export CGO_ENABLED=0 |
|
| 13 |
+ _install_rootlesskit |
|
| 14 |
+ ;; |
|
| 15 |
+ *) |
|
| 16 |
+ echo 'Usage: $0 [dynamic]' |
|
| 17 |
+ ;; |
|
| 18 |
+ esac |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+install_rootlesskit_dynamic() {
|
|
| 22 |
+ export ROOTLESSKIT_LDFLAGS="-linkmode=external" install_rootlesskit |
|
| 23 |
+ export BUILD_MODE="-buildmode=pie" |
|
| 24 |
+ _install_rootlesskit |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+_install_rootlesskit() {
|
|
| 28 |
+ echo "Install rootlesskit version $ROOTLESSKIT_COMMIT" |
|
| 29 |
+ git clone https://github.com/rootless-containers/rootlesskit.git "$GOPATH/src/github.com/rootless-containers/rootlesskit" |
|
| 30 |
+ cd "$GOPATH/src/github.com/rootless-containers/rootlesskit" |
|
| 31 |
+ git checkout -q "$ROOTLESSKIT_COMMIT" |
|
| 32 |
+ go build $BUILD_MODE -ldflags="$ROOTLESSKIT_LDFLAGS" -o "${PREFIX}/rootlesskit" github.com/rootless-containers/rootlesskit/cmd/rootlesskit
|
|
| 33 |
+} |
| ... | ... |
@@ -7,3 +7,5 @@ DOCKER_CONTAINERD_CTR_BINARY_NAME='ctr' |
| 7 | 7 |
DOCKER_CONTAINERD_SHIM_BINARY_NAME='containerd-shim' |
| 8 | 8 |
DOCKER_PROXY_BINARY_NAME='docker-proxy' |
| 9 | 9 |
DOCKER_INIT_BINARY_NAME='docker-init' |
| 10 |
+DOCKER_ROOTLESSKIT_BINARY_NAME='rootlesskit' |
|
| 11 |
+DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME='dockerd-rootless.sh' |
| ... | ... |
@@ -14,7 +14,7 @@ copy_binaries() {
|
| 14 | 14 |
return |
| 15 | 15 |
fi |
| 16 | 16 |
echo "Copying nested executables into $dir" |
| 17 |
- for file in containerd containerd-shim ctr runc docker-init docker-proxy; do |
|
| 17 |
+ for file in containerd containerd-shim ctr runc docker-init docker-proxy rootlesskit dockerd-rootless.sh; do |
|
| 18 | 18 |
cp -f `which "$file"` "$dir/" |
| 19 | 19 |
if [ "$hash" == "hash" ]; then |
| 20 | 20 |
hash_files "$dir/$file" |
| ... | ... |
@@ -26,4 +26,6 @@ install_binary() {
|
| 26 | 26 |
install_binary "${DEST}/${DOCKER_CONTAINERD_SHIM_BINARY_NAME}"
|
| 27 | 27 |
install_binary "${DEST}/${DOCKER_PROXY_BINARY_NAME}"
|
| 28 | 28 |
install_binary "${DEST}/${DOCKER_INIT_BINARY_NAME}"
|
| 29 |
+ install_binary "${DEST}/${DOCKER_ROOTLESSKIT_BINARY_NAME}"
|
|
| 30 |
+ install_binary "${DEST}/${DOCKER_DAEMON_ROOTLESS_SH_BINARY_NAME}"
|
|
| 29 | 31 |
) |
| ... | ... |
@@ -4,8 +4,11 @@ import ( |
| 4 | 4 |
"fmt" |
| 5 | 5 |
"net" |
| 6 | 6 |
"net/url" |
| 7 |
+ "path/filepath" |
|
| 7 | 8 |
"strconv" |
| 8 | 9 |
"strings" |
| 10 |
+ |
|
| 11 |
+ "github.com/docker/docker/pkg/homedir" |
|
| 9 | 12 |
) |
| 10 | 13 |
|
| 11 | 14 |
var ( |
| ... | ... |
@@ -41,12 +44,20 @@ func ValidateHost(val string) (string, error) {
|
| 41 | 41 |
return val, nil |
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 |
-// ParseHost and set defaults for a Daemon host string |
|
| 45 |
-func ParseHost(defaultToTLS bool, val string) (string, error) {
|
|
| 44 |
+// ParseHost and set defaults for a Daemon host string. |
|
| 45 |
+// defaultToTLS is preferred over defaultToUnixRootless. |
|
| 46 |
+func ParseHost(defaultToTLS, defaultToUnixRootless bool, val string) (string, error) {
|
|
| 46 | 47 |
host := strings.TrimSpace(val) |
| 47 | 48 |
if host == "" {
|
| 48 | 49 |
if defaultToTLS {
|
| 49 | 50 |
host = DefaultTLSHost |
| 51 |
+ } else if defaultToUnixRootless {
|
|
| 52 |
+ runtimeDir, err := homedir.GetRuntimeDir() |
|
| 53 |
+ if err != nil {
|
|
| 54 |
+ return "", err |
|
| 55 |
+ } |
|
| 56 |
+ socket := filepath.Join(runtimeDir, "docker.sock") |
|
| 57 |
+ host = "unix://" + socket |
|
| 50 | 58 |
} else {
|
| 51 | 59 |
host = DefaultHost |
| 52 | 60 |
} |
| ... | ... |
@@ -38,13 +38,13 @@ func TestParseHost(t *testing.T) {
|
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 | 40 |
for _, value := range invalid {
|
| 41 |
- if _, err := ParseHost(false, value); err == nil {
|
|
| 41 |
+ if _, err := ParseHost(false, false, value); err == nil {
|
|
| 42 | 42 |
t.Errorf("Expected an error for %v, got [nil]", value)
|
| 43 | 43 |
} |
| 44 | 44 |
} |
| 45 | 45 |
|
| 46 | 46 |
for value, expected := range valid {
|
| 47 |
- if actual, err := ParseHost(false, value); err != nil || actual != expected {
|
|
| 47 |
+ if actual, err := ParseHost(false, false, value); err != nil || actual != expected {
|
|
| 48 | 48 |
t.Errorf("Expected for %v [%v], got [%v, %v]", value, expected, actual, err)
|
| 49 | 49 |
} |
| 50 | 50 |
} |
| ... | ... |
@@ -1,7 +1,10 @@ |
| 1 | 1 |
package homedir // import "github.com/docker/docker/pkg/homedir" |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "errors" |
|
| 4 | 5 |
"os" |
| 6 |
+ "path/filepath" |
|
| 7 |
+ "strings" |
|
| 5 | 8 |
|
| 6 | 9 |
"github.com/docker/docker/pkg/idtools" |
| 7 | 10 |
) |
| ... | ... |
@@ -19,3 +22,88 @@ func GetStatic() (string, error) {
|
| 19 | 19 |
} |
| 20 | 20 |
return usr.Home, nil |
| 21 | 21 |
} |
| 22 |
+ |
|
| 23 |
+// GetRuntimeDir returns XDG_RUNTIME_DIR. |
|
| 24 |
+// XDG_RUNTIME_DIR is typically configured via pam_systemd. |
|
| 25 |
+// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set. |
|
| 26 |
+// |
|
| 27 |
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html |
|
| 28 |
+func GetRuntimeDir() (string, error) {
|
|
| 29 |
+ if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
|
|
| 30 |
+ return xdgRuntimeDir, nil |
|
| 31 |
+ } |
|
| 32 |
+ return "", errors.New("could not get XDG_RUNTIME_DIR")
|
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+// StickRuntimeDirContents sets the sticky bit on files that are under |
|
| 36 |
+// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system. |
|
| 37 |
+// |
|
| 38 |
+// StickyRuntimeDir returns slice of sticked files. |
|
| 39 |
+// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set. |
|
| 40 |
+// |
|
| 41 |
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html |
|
| 42 |
+func StickRuntimeDirContents(files []string) ([]string, error) {
|
|
| 43 |
+ runtimeDir, err := GetRuntimeDir() |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ // ignore error if runtimeDir is empty |
|
| 46 |
+ return nil, nil |
|
| 47 |
+ } |
|
| 48 |
+ runtimeDir, err = filepath.Abs(runtimeDir) |
|
| 49 |
+ if err != nil {
|
|
| 50 |
+ return nil, err |
|
| 51 |
+ } |
|
| 52 |
+ var sticked []string |
|
| 53 |
+ for _, f := range files {
|
|
| 54 |
+ f, err = filepath.Abs(f) |
|
| 55 |
+ if err != nil {
|
|
| 56 |
+ return sticked, err |
|
| 57 |
+ } |
|
| 58 |
+ if strings.HasPrefix(f, runtimeDir+"/") {
|
|
| 59 |
+ if err = stick(f); err != nil {
|
|
| 60 |
+ return sticked, err |
|
| 61 |
+ } |
|
| 62 |
+ sticked = append(sticked, f) |
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+ return sticked, nil |
|
| 66 |
+} |
|
| 67 |
+ |
|
| 68 |
+func stick(f string) error {
|
|
| 69 |
+ st, err := os.Stat(f) |
|
| 70 |
+ if err != nil {
|
|
| 71 |
+ return err |
|
| 72 |
+ } |
|
| 73 |
+ m := st.Mode() |
|
| 74 |
+ m |= os.ModeSticky |
|
| 75 |
+ return os.Chmod(f, m) |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+// GetDataHome returns XDG_DATA_HOME. |
|
| 79 |
+// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set. |
|
| 80 |
+// |
|
| 81 |
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html |
|
| 82 |
+func GetDataHome() (string, error) {
|
|
| 83 |
+ if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
|
|
| 84 |
+ return xdgDataHome, nil |
|
| 85 |
+ } |
|
| 86 |
+ home := os.Getenv("HOME")
|
|
| 87 |
+ if home == "" {
|
|
| 88 |
+ return "", errors.New("could not get either XDG_DATA_HOME or HOME")
|
|
| 89 |
+ } |
|
| 90 |
+ return filepath.Join(home, ".local", "share"), nil |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+// GetConfigHome returns XDG_CONFIG_HOME. |
|
| 94 |
+// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set. |
|
| 95 |
+// |
|
| 96 |
+// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html |
|
| 97 |
+func GetConfigHome() (string, error) {
|
|
| 98 |
+ if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
|
|
| 99 |
+ return xdgConfigHome, nil |
|
| 100 |
+ } |
|
| 101 |
+ home := os.Getenv("HOME")
|
|
| 102 |
+ if home == "" {
|
|
| 103 |
+ return "", errors.New("could not get either XDG_CONFIG_HOME or HOME")
|
|
| 104 |
+ } |
|
| 105 |
+ return filepath.Join(home, ".config"), nil |
|
| 106 |
+} |
| ... | ... |
@@ -11,3 +11,23 @@ import ( |
| 11 | 11 |
func GetStatic() (string, error) {
|
| 12 | 12 |
return "", errors.New("homedir.GetStatic() is not supported on this system")
|
| 13 | 13 |
} |
| 14 |
+ |
|
| 15 |
+// GetRuntimeDir is unsupported on non-linux system. |
|
| 16 |
+func GetRuntimeDir() (string, error) {
|
|
| 17 |
+ return "", errors.New("homedir.GetRuntimeDir() is not supported on this system")
|
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+// StickRuntimeDirContents is unsupported on non-linux system. |
|
| 21 |
+func StickRuntimeDirContents(files []string) ([]string, error) {
|
|
| 22 |
+ return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system")
|
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+// GetDataHome is unsupported on non-linux system. |
|
| 26 |
+func GetDataHome() (string, error) {
|
|
| 27 |
+ return "", errors.New("homedir.GetDataHome() is not supported on this system")
|
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// GetConfigHome is unsupported on non-linux system. |
|
| 31 |
+func GetConfigHome() (string, error) {
|
|
| 32 |
+ return "", errors.New("homedir.GetConfigHome() is not supported on this system")
|
|
| 33 |
+} |
| ... | ... |
@@ -51,7 +51,9 @@ func New(quiet bool) *SysInfo {
|
| 51 | 51 |
|
| 52 | 52 |
// Check if AppArmor is supported. |
| 53 | 53 |
if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) {
|
| 54 |
- sysInfo.AppArmor = true |
|
| 54 |
+ if _, err := ioutil.ReadFile("/sys/kernel/security/apparmor/profiles"); err == nil {
|
|
| 55 |
+ sysInfo.AppArmor = true |
|
| 56 |
+ } |
|
| 55 | 57 |
} |
| 56 | 58 |
|
| 57 | 59 |
// Check if Seccomp is supported, via CONFIG_SECCOMP. |
| 58 | 60 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,26 @@ |
| 0 |
+package rootless |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "os" |
|
| 4 |
+ "sync" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+var ( |
|
| 8 |
+ runningWithNonRootUsername bool |
|
| 9 |
+ runningWithNonRootUsernameOnce sync.Once |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// RunningWithNonRootUsername returns true if we $USER is set to a non-root value, |
|
| 13 |
+// regardless to the UID/EUID value. |
|
| 14 |
+// |
|
| 15 |
+// The value of this variable is mostly used for configuring default paths. |
|
| 16 |
+// If the value is true, $HOME and $XDG_RUNTIME_DIR should be honored for setting up the default paths. |
|
| 17 |
+// If false (not only EUID==0 but also $USER==root), $HOME and $XDG_RUNTIME_DIR should be ignored |
|
| 18 |
+// even if we are in a user namespace. |
|
| 19 |
+func RunningWithNonRootUsername() bool {
|
|
| 20 |
+ runningWithNonRootUsernameOnce.Do(func() {
|
|
| 21 |
+ u := os.Getenv("USER")
|
|
| 22 |
+ runningWithNonRootUsername = u != "" && u != "root" |
|
| 23 |
+ }) |
|
| 24 |
+ return runningWithNonRootUsername |
|
| 25 |
+} |
| 0 | 26 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,38 @@ |
| 0 |
+package specconv |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "io/ioutil" |
|
| 4 |
+ "strconv" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/opencontainers/runtime-spec/specs-go" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// ToRootless converts spec to be compatible with "rootless" runc. |
|
| 10 |
+// * Remove cgroups (will be supported in separate PR when delegation permission is configured) |
|
| 11 |
+// * Fix up OOMScoreAdj |
|
| 12 |
+func ToRootless(spec *specs.Spec) error {
|
|
| 13 |
+ return toRootless(spec, getCurrentOOMScoreAdj()) |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+func getCurrentOOMScoreAdj() int {
|
|
| 17 |
+ b, err := ioutil.ReadFile("/proc/self/oom_score_adj")
|
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return 0 |
|
| 20 |
+ } |
|
| 21 |
+ i, err := strconv.Atoi(string(b)) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ return 0 |
|
| 24 |
+ } |
|
| 25 |
+ return i |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+func toRootless(spec *specs.Spec, currentOOMScoreAdj int) error {
|
|
| 29 |
+ // Remove cgroup settings. |
|
| 30 |
+ spec.Linux.Resources = nil |
|
| 31 |
+ spec.Linux.CgroupsPath = "" |
|
| 32 |
+ |
|
| 33 |
+ if spec.Process.OOMScoreAdj != nil && *spec.Process.OOMScoreAdj < currentOOMScoreAdj {
|
|
| 34 |
+ *spec.Process.OOMScoreAdj = currentOOMScoreAdj |
|
| 35 |
+ } |
|
| 36 |
+ return nil |
|
| 37 |
+} |