diff --git a/api/server/router/build/build_routes.go b/api/server/router/build/build_routes.go index 17aa6226e4..892b8fca50 100644 --- a/api/server/router/build/build_routes.go +++ b/api/server/router/build/build_routes.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "runtime" "strconv" "strings" "sync" @@ -51,6 +52,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui options.CPUSetMems = r.FormValue("cpusetmems") options.CgroupParent = r.FormValue("cgroupparent") options.Tags = r.Form["t"] + options.SecurityOpt = r.Form["securityopt"] if r.Form.Get("shmsize") != "" { shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) @@ -67,6 +69,10 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui options.Isolation = i } + if runtime.GOOS != "windows" && options.SecurityOpt != nil { + return nil, fmt.Errorf("the daemon on this platform does not support --security-opt to build") + } + var buildUlimits = []*units.Ulimit{} ulimitsJSON := r.FormValue("ulimits") if ulimitsJSON != "" { diff --git a/api/types/client.go b/api/types/client.go index 553055e566..8cc9b05c0e 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -153,7 +153,8 @@ type ImageBuildOptions struct { Squash bool // CacheFrom specifies images that are used for matching cache. Images // specified here do not need to have a valid parent chain to match cache. - CacheFrom []string + CacheFrom []string + SecurityOpt []string } // ImageBuildResponse holds information diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go index f31d863012..df45920698 100644 --- a/builder/dockerfile/internals.go +++ b/builder/dockerfile/internals.go @@ -484,9 +484,10 @@ func (b *Builder) create() (string, error) { // TODO: why not embed a hostconfig in builder? hostConfig := &container.HostConfig{ - Isolation: b.options.Isolation, - ShmSize: b.options.ShmSize, - Resources: resources, + SecurityOpt: b.options.SecurityOpt, + Isolation: b.options.Isolation, + ShmSize: b.options.ShmSize, + Resources: resources, } config := *b.runConfig diff --git a/cli/command/image/build.go b/cli/command/image/build.go index ccfebb9834..19fd4aa709 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -57,6 +57,7 @@ type buildOptions struct { pull bool cacheFrom []string compress bool + securityOpt []string } // NewBuildCommand creates a new `docker build` command @@ -103,6 +104,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") + flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") command.AddTrustedFlags(flags, true) @@ -299,6 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { AuthConfigs: authConfig, Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, + SecurityOpt: options.securityOpt, } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) diff --git a/client/image_build.go b/client/image_build.go index 0094602a6e..3abd87025e 100644 --- a/client/image_build.go +++ b/client/image_build.go @@ -49,7 +49,8 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) { query := url.Values{ - "t": options.Tags, + "t": options.Tags, + "securityopt": options.SecurityOpt, } if options.SuppressOutput { query.Set("q", "1") diff --git a/daemon/daemon.go b/daemon/daemon.go index 64f961b12b..2d294ada62 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -547,6 +547,12 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot return nil, err } + if runtime.GOOS == "windows" { + if err := idtools.MkdirAllAs(filepath.Join(config.Root, "credentialspecs"), 0700, rootUID, rootGID); err != nil && !os.IsExist(err) { + return nil, err + } + } + driverName := os.Getenv("DOCKER_DRIVER") if driverName == "" { driverName = config.GraphDriver diff --git a/daemon/start_windows.go b/daemon/start_windows.go index 8b9cae7629..dc73f09692 100644 --- a/daemon/start_windows.go +++ b/daemon/start_windows.go @@ -2,11 +2,19 @@ package daemon import ( "fmt" + "io/ioutil" "path/filepath" + "strings" "github.com/docker/docker/container" "github.com/docker/docker/layer" "github.com/docker/docker/libcontainerd" + "golang.org/x/sys/windows/registry" +) + +const ( + credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs` + credentialSpecFileLocation = "CredentialSpecs" ) func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Container) (*[]libcontainerd.CreateOption, error) { @@ -80,7 +88,50 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain } } - // Now build the full set of options + // Read and add credentials from the security options if a credential spec has been provided. + if container.HostConfig.SecurityOpt != nil { + for _, sOpt := range container.HostConfig.SecurityOpt { + sOpt = strings.ToLower(sOpt) + if !strings.Contains(sOpt, "=") { + return nil, fmt.Errorf("invalid security option: no equals sign in supplied value %s", sOpt) + } + var splitsOpt []string + splitsOpt = strings.SplitN(sOpt, "=", 2) + if len(splitsOpt) != 2 { + return nil, fmt.Errorf("invalid security option: %s", sOpt) + } + if splitsOpt[0] != "credentialspec" { + return nil, fmt.Errorf("security option not supported: %s", splitsOpt[0]) + } + + credentialsOpts := &libcontainerd.CredentialsOption{} + var ( + match bool + csValue string + err error + ) + if match, csValue = getCredentialSpec("file://", splitsOpt[1]); match { + if csValue == "" { + return nil, fmt.Errorf("no value supplied for file:// credential spec security option") + } + if credentialsOpts.Credentials, err = readCredentialSpecFile(container.ID, daemon.root, filepath.Clean(csValue)); err != nil { + return nil, err + } + } else if match, csValue = getCredentialSpec("registry://", splitsOpt[1]); match { + if csValue == "" { + return nil, fmt.Errorf("no value supplied for registry:// credential spec security option") + } + if credentialsOpts.Credentials, err = readCredentialSpecRegistry(container.ID, csValue); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value") + } + createOptions = append(createOptions, credentialsOpts) + } + } + + // Now add the remaining options. createOptions = append(createOptions, &libcontainerd.FlushOption{IgnoreFlushesDuringBoot: !container.HasBeenStartedBefore}) createOptions = append(createOptions, hvOpts) createOptions = append(createOptions, layerOpts) @@ -90,3 +141,52 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain return &createOptions, nil } + +// getCredentialSpec is a helper function to get the value of a credential spec supplied +// on the CLI, stripping the prefix +func getCredentialSpec(prefix, value string) (bool, string) { + if strings.HasPrefix(value, prefix) { + return true, strings.TrimPrefix(value, prefix) + } + return false, "" +} + +// readCredentialSpecRegistry is a helper function to read a credential spec from +// the registry. If not found, we return an empty string and warn in the log. +// This allows for staging on machines which do not have the necessary components. +func readCredentialSpecRegistry(id, name string) (string, error) { + var ( + k registry.Key + err error + val string + ) + if k, err = registry.OpenKey(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE); err != nil { + return "", fmt.Errorf("failed handling spec %q for container %s - %s could not be opened", name, id, credentialSpecRegistryLocation) + } + if val, _, err = k.GetStringValue(name); err != nil { + if err == registry.ErrNotExist { + return "", fmt.Errorf("credential spec %q for container %s as it was not found", name, id) + } + return "", fmt.Errorf("error %v reading credential spec %q from registry for container %s", err, name, id) + } + return val, nil +} + +// readCredentialSpecFile is a helper function to read a credential spec from +// a file. If not found, we return an empty string and warn in the log. +// This allows for staging on machines which do not have the necessary components. +func readCredentialSpecFile(id, root, location string) (string, error) { + if filepath.IsAbs(location) { + return "", fmt.Errorf("invalid credential spec - file:// path cannot be absolute") + } + base := filepath.Join(root, credentialSpecFileLocation) + full := filepath.Join(base, location) + if !strings.HasPrefix(full, base) { + return "", fmt.Errorf("invalid credential spec - file:// path must be under %s", base) + } + bcontents, err := ioutil.ReadFile(full) + if err != nil { + return "", fmt.Errorf("credential spec '%s' for container %s as the file could not be read: %q", full, id, err) + } + return string(bcontents[:]), nil +} diff --git a/docs/reference/commandline/build.md b/docs/reference/commandline/build.md index 95c278f761..65f54cc81f 100644 --- a/docs/reference/commandline/build.md +++ b/docs/reference/commandline/build.md @@ -37,6 +37,7 @@ Options: --pull Always attempt to pull a newer version of the image -q, --quiet Suppress the build output and print image ID on success --rm Remove intermediate containers after a successful build (default true) + --security-opt value Security Options (default []) --shm-size string Size of /dev/shm, default value is 64MB. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), @@ -397,6 +398,12 @@ Dockerfile are echoed during the build process. For detailed information on using `ARG` and `ENV` instructions, see the [Dockerfile reference](../builder.md). +### Optional security options (--security-opt) + +This flag is only supported on a daemon running on Windows, and only supports +the `credentialspec` option. The `credentialspec` must be in the format +`file://spec.txt` or `registry://keyname`. + ### Specify isolation technology for container (--isolation) This option is useful in situations where you are running Docker containers on diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index b0cde6eb1d..b169f9c476 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -614,6 +614,11 @@ The `--stop-signal` flag sets the system call signal that will be sent to the co This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9, or a signal name in the format SIGNAME, for instance SIGKILL. +### Optional security options (--security-opt) + +On Windows, this flag can be used to specify the `credentialspec` option. +The `credentialspec` must be in the format `file://spec.txt` or `registry://keyname`. + ### Specify isolation technology for container (--isolation) This option is useful in situations where you are running Docker containers on diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 515d73c354..2112cf9d08 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -4519,3 +4519,22 @@ func (s *DockerSuite) TestRunStoppedLoggingDriverNoLeak(c *check.C) { // NGoroutines is not updated right away, so we need to wait before failing c.Assert(waitForGoroutines(nroutines), checker.IsNil) } + +// Handles error conditions for --credentialspec. Validating E2E success cases +// requires additional infrastructure (AD for example) on CI servers. +func (s *DockerSuite) TestRunCredentialSpecFailures(c *check.C) { + testRequires(c, DaemonIsWindows) + attempts := []struct{ value, expectedError string }{ + {"rubbish", "invalid credential spec security option - value must be prefixed file:// or registry://"}, + {"rubbish://", "invalid credential spec security option - value must be prefixed file:// or registry://"}, + {"file://", "no value supplied for file:// credential spec security option"}, + {"registry://", "no value supplied for registry:// credential spec security option"}, + {`file://c:\blah.txt`, "path cannot be absolute"}, + {`file://doesnotexist.txt`, "The system cannot find the file specified"}, + } + for _, attempt := range attempts { + _, _, err := dockerCmdWithError("run", "--security-opt=credentialspec="+attempt.value, "busybox", "true") + c.Assert(err, checker.NotNil, check.Commentf("%s expected non-nil err", attempt.value)) + c.Assert(err.Error(), checker.Contains, attempt.expectedError, check.Commentf("%s expected %s got %s", attempt.value, attempt.expectedError, err)) + } +} diff --git a/libcontainerd/client_windows.go b/libcontainerd/client_windows.go index de67af6b90..da30ced86b 100644 --- a/libcontainerd/client_windows.go +++ b/libcontainerd/client_windows.go @@ -154,6 +154,10 @@ func (clnt *client) Create(containerID string, checkpoint string, checkpointDir configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery continue } + if c, ok := option.(*CredentialsOption); ok { + configuration.Credentials = c.Credentials + continue + } } // We must have a layer option with at least one path diff --git a/libcontainerd/types_windows.go b/libcontainerd/types_windows.go index 10c6e61623..24a9a96440 100644 --- a/libcontainerd/types_windows.go +++ b/libcontainerd/types_windows.go @@ -62,6 +62,12 @@ type NetworkEndpointsOption struct { AllowUnqualifiedDNSQuery bool } +// CredentialsOption is a CreateOption that indicates the credentials from +// a credential spec to be used to the runtime +type CredentialsOption struct { + Credentials string +} + // Checkpoint holds the details of a checkpoint (not supported in windows) type Checkpoint struct { Name string diff --git a/libcontainerd/utils_windows.go b/libcontainerd/utils_windows.go index 77fd5dacf2..41ac40d2c2 100644 --- a/libcontainerd/utils_windows.go +++ b/libcontainerd/utils_windows.go @@ -39,3 +39,8 @@ func (h *LayerOption) Apply(interface{}) error { func (s *NetworkEndpointsOption) Apply(interface{}) error { return nil } + +// Apply for the credentials option is a no-op. +func (s *CredentialsOption) Apply(interface{}) error { + return nil +} diff --git a/runconfig/config.go b/runconfig/config.go index a663a47a8e..4596f048ae 100644 --- a/runconfig/config.go +++ b/runconfig/config.go @@ -67,6 +67,7 @@ func DecodeContainerConfig(src io.Reader) (*container.Config, *container.HostCon if err := ValidateQoS(hc); err != nil { return nil, nil, nil, err } + return w.Config, hc, w.NetworkingConfig, nil } diff --git a/runconfig/opts/parse.go b/runconfig/opts/parse.go index 0fd48bbb9b..b7097c3ec8 100644 --- a/runconfig/opts/parse.go +++ b/runconfig/opts/parse.go @@ -105,6 +105,7 @@ type ContainerOptions struct { autoRemove bool init bool initPath string + credentialSpec string Image string Args []string @@ -173,6 +174,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions { flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container") flags.Var(&copts.securityOpt, "security-opt", "Security Options") flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") + flags.StringVar(&copts.credentialSpec, "credentialspec", "", "Credential spec for managed service account (Windows only)") // Network and port publishing flag flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")