Signed-off-by: John Howard <jhoward@microsoft.com>
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
"fmt" |
| 8 | 8 |
"io" |
| 9 | 9 |
"net/http" |
| 10 |
+ "runtime" |
|
| 10 | 11 |
"strconv" |
| 11 | 12 |
"strings" |
| 12 | 13 |
"sync" |
| ... | ... |
@@ -51,6 +52,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui |
| 51 | 51 |
options.CPUSetMems = r.FormValue("cpusetmems")
|
| 52 | 52 |
options.CgroupParent = r.FormValue("cgroupparent")
|
| 53 | 53 |
options.Tags = r.Form["t"] |
| 54 |
+ options.SecurityOpt = r.Form["securityopt"] |
|
| 54 | 55 |
|
| 55 | 56 |
if r.Form.Get("shmsize") != "" {
|
| 56 | 57 |
shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64)
|
| ... | ... |
@@ -67,6 +69,10 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui |
| 67 | 67 |
options.Isolation = i |
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 |
+ if runtime.GOOS != "windows" && options.SecurityOpt != nil {
|
|
| 71 |
+ return nil, fmt.Errorf("the daemon on this platform does not support --security-opt to build")
|
|
| 72 |
+ } |
|
| 73 |
+ |
|
| 70 | 74 |
var buildUlimits = []*units.Ulimit{}
|
| 71 | 75 |
ulimitsJSON := r.FormValue("ulimits")
|
| 72 | 76 |
if ulimitsJSON != "" {
|
| ... | ... |
@@ -153,7 +153,8 @@ type ImageBuildOptions struct {
|
| 153 | 153 |
Squash bool |
| 154 | 154 |
// CacheFrom specifies images that are used for matching cache. Images |
| 155 | 155 |
// specified here do not need to have a valid parent chain to match cache. |
| 156 |
- CacheFrom []string |
|
| 156 |
+ CacheFrom []string |
|
| 157 |
+ SecurityOpt []string |
|
| 157 | 158 |
} |
| 158 | 159 |
|
| 159 | 160 |
// ImageBuildResponse holds information |
| ... | ... |
@@ -484,9 +484,10 @@ func (b *Builder) create() (string, error) {
|
| 484 | 484 |
|
| 485 | 485 |
// TODO: why not embed a hostconfig in builder? |
| 486 | 486 |
hostConfig := &container.HostConfig{
|
| 487 |
- Isolation: b.options.Isolation, |
|
| 488 |
- ShmSize: b.options.ShmSize, |
|
| 489 |
- Resources: resources, |
|
| 487 |
+ SecurityOpt: b.options.SecurityOpt, |
|
| 488 |
+ Isolation: b.options.Isolation, |
|
| 489 |
+ ShmSize: b.options.ShmSize, |
|
| 490 |
+ Resources: resources, |
|
| 490 | 491 |
} |
| 491 | 492 |
|
| 492 | 493 |
config := *b.runConfig |
| ... | ... |
@@ -57,6 +57,7 @@ type buildOptions struct {
|
| 57 | 57 |
pull bool |
| 58 | 58 |
cacheFrom []string |
| 59 | 59 |
compress bool |
| 60 |
+ securityOpt []string |
|
| 60 | 61 |
} |
| 61 | 62 |
|
| 62 | 63 |
// NewBuildCommand creates a new `docker build` command |
| ... | ... |
@@ -103,6 +104,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 103 | 103 |
flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") |
| 104 | 104 |
flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
|
| 105 | 105 |
flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") |
| 106 |
+ flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options")
|
|
| 106 | 107 |
|
| 107 | 108 |
command.AddTrustedFlags(flags, true) |
| 108 | 109 |
|
| ... | ... |
@@ -299,6 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
| 299 | 299 |
AuthConfigs: authConfig, |
| 300 | 300 |
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), |
| 301 | 301 |
CacheFrom: options.cacheFrom, |
| 302 |
+ SecurityOpt: options.securityOpt, |
|
| 302 | 303 |
} |
| 303 | 304 |
|
| 304 | 305 |
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) |
| ... | ... |
@@ -49,7 +49,8 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio |
| 49 | 49 |
|
| 50 | 50 |
func imageBuildOptionsToQuery(options types.ImageBuildOptions) (url.Values, error) {
|
| 51 | 51 |
query := url.Values{
|
| 52 |
- "t": options.Tags, |
|
| 52 |
+ "t": options.Tags, |
|
| 53 |
+ "securityopt": options.SecurityOpt, |
|
| 53 | 54 |
} |
| 54 | 55 |
if options.SuppressOutput {
|
| 55 | 56 |
query.Set("q", "1")
|
| ... | ... |
@@ -547,6 +547,12 @@ func NewDaemon(config *Config, registryService registry.Service, containerdRemot |
| 547 | 547 |
return nil, err |
| 548 | 548 |
} |
| 549 | 549 |
|
| 550 |
+ if runtime.GOOS == "windows" {
|
|
| 551 |
+ if err := idtools.MkdirAllAs(filepath.Join(config.Root, "credentialspecs"), 0700, rootUID, rootGID); err != nil && !os.IsExist(err) {
|
|
| 552 |
+ return nil, err |
|
| 553 |
+ } |
|
| 554 |
+ } |
|
| 555 |
+ |
|
| 550 | 556 |
driverName := os.Getenv("DOCKER_DRIVER")
|
| 551 | 557 |
if driverName == "" {
|
| 552 | 558 |
driverName = config.GraphDriver |
| ... | ... |
@@ -2,11 +2,19 @@ package daemon |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "io/ioutil" |
|
| 5 | 6 |
"path/filepath" |
| 7 |
+ "strings" |
|
| 6 | 8 |
|
| 7 | 9 |
"github.com/docker/docker/container" |
| 8 | 10 |
"github.com/docker/docker/layer" |
| 9 | 11 |
"github.com/docker/docker/libcontainerd" |
| 12 |
+ "golang.org/x/sys/windows/registry" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+const ( |
|
| 16 |
+ credentialSpecRegistryLocation = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs` |
|
| 17 |
+ credentialSpecFileLocation = "CredentialSpecs" |
|
| 10 | 18 |
) |
| 11 | 19 |
|
| 12 | 20 |
func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Container) (*[]libcontainerd.CreateOption, error) {
|
| ... | ... |
@@ -80,7 +88,50 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain |
| 80 | 80 |
} |
| 81 | 81 |
} |
| 82 | 82 |
|
| 83 |
- // Now build the full set of options |
|
| 83 |
+ // Read and add credentials from the security options if a credential spec has been provided. |
|
| 84 |
+ if container.HostConfig.SecurityOpt != nil {
|
|
| 85 |
+ for _, sOpt := range container.HostConfig.SecurityOpt {
|
|
| 86 |
+ sOpt = strings.ToLower(sOpt) |
|
| 87 |
+ if !strings.Contains(sOpt, "=") {
|
|
| 88 |
+ return nil, fmt.Errorf("invalid security option: no equals sign in supplied value %s", sOpt)
|
|
| 89 |
+ } |
|
| 90 |
+ var splitsOpt []string |
|
| 91 |
+ splitsOpt = strings.SplitN(sOpt, "=", 2) |
|
| 92 |
+ if len(splitsOpt) != 2 {
|
|
| 93 |
+ return nil, fmt.Errorf("invalid security option: %s", sOpt)
|
|
| 94 |
+ } |
|
| 95 |
+ if splitsOpt[0] != "credentialspec" {
|
|
| 96 |
+ return nil, fmt.Errorf("security option not supported: %s", splitsOpt[0])
|
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ credentialsOpts := &libcontainerd.CredentialsOption{}
|
|
| 100 |
+ var ( |
|
| 101 |
+ match bool |
|
| 102 |
+ csValue string |
|
| 103 |
+ err error |
|
| 104 |
+ ) |
|
| 105 |
+ if match, csValue = getCredentialSpec("file://", splitsOpt[1]); match {
|
|
| 106 |
+ if csValue == "" {
|
|
| 107 |
+ return nil, fmt.Errorf("no value supplied for file:// credential spec security option")
|
|
| 108 |
+ } |
|
| 109 |
+ if credentialsOpts.Credentials, err = readCredentialSpecFile(container.ID, daemon.root, filepath.Clean(csValue)); err != nil {
|
|
| 110 |
+ return nil, err |
|
| 111 |
+ } |
|
| 112 |
+ } else if match, csValue = getCredentialSpec("registry://", splitsOpt[1]); match {
|
|
| 113 |
+ if csValue == "" {
|
|
| 114 |
+ return nil, fmt.Errorf("no value supplied for registry:// credential spec security option")
|
|
| 115 |
+ } |
|
| 116 |
+ if credentialsOpts.Credentials, err = readCredentialSpecRegistry(container.ID, csValue); err != nil {
|
|
| 117 |
+ return nil, err |
|
| 118 |
+ } |
|
| 119 |
+ } else {
|
|
| 120 |
+ return nil, fmt.Errorf("invalid credential spec security option - value must be prefixed file:// or registry:// followed by a value")
|
|
| 121 |
+ } |
|
| 122 |
+ createOptions = append(createOptions, credentialsOpts) |
|
| 123 |
+ } |
|
| 124 |
+ } |
|
| 125 |
+ |
|
| 126 |
+ // Now add the remaining options. |
|
| 84 | 127 |
createOptions = append(createOptions, &libcontainerd.FlushOption{IgnoreFlushesDuringBoot: !container.HasBeenStartedBefore})
|
| 85 | 128 |
createOptions = append(createOptions, hvOpts) |
| 86 | 129 |
createOptions = append(createOptions, layerOpts) |
| ... | ... |
@@ -90,3 +141,52 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain |
| 90 | 90 |
|
| 91 | 91 |
return &createOptions, nil |
| 92 | 92 |
} |
| 93 |
+ |
|
| 94 |
+// getCredentialSpec is a helper function to get the value of a credential spec supplied |
|
| 95 |
+// on the CLI, stripping the prefix |
|
| 96 |
+func getCredentialSpec(prefix, value string) (bool, string) {
|
|
| 97 |
+ if strings.HasPrefix(value, prefix) {
|
|
| 98 |
+ return true, strings.TrimPrefix(value, prefix) |
|
| 99 |
+ } |
|
| 100 |
+ return false, "" |
|
| 101 |
+} |
|
| 102 |
+ |
|
| 103 |
+// readCredentialSpecRegistry is a helper function to read a credential spec from |
|
| 104 |
+// the registry. If not found, we return an empty string and warn in the log. |
|
| 105 |
+// This allows for staging on machines which do not have the necessary components. |
|
| 106 |
+func readCredentialSpecRegistry(id, name string) (string, error) {
|
|
| 107 |
+ var ( |
|
| 108 |
+ k registry.Key |
|
| 109 |
+ err error |
|
| 110 |
+ val string |
|
| 111 |
+ ) |
|
| 112 |
+ if k, err = registry.OpenKey(registry.LOCAL_MACHINE, credentialSpecRegistryLocation, registry.QUERY_VALUE); err != nil {
|
|
| 113 |
+ return "", fmt.Errorf("failed handling spec %q for container %s - %s could not be opened", name, id, credentialSpecRegistryLocation)
|
|
| 114 |
+ } |
|
| 115 |
+ if val, _, err = k.GetStringValue(name); err != nil {
|
|
| 116 |
+ if err == registry.ErrNotExist {
|
|
| 117 |
+ return "", fmt.Errorf("credential spec %q for container %s as it was not found", name, id)
|
|
| 118 |
+ } |
|
| 119 |
+ return "", fmt.Errorf("error %v reading credential spec %q from registry for container %s", err, name, id)
|
|
| 120 |
+ } |
|
| 121 |
+ return val, nil |
|
| 122 |
+} |
|
| 123 |
+ |
|
| 124 |
+// readCredentialSpecFile is a helper function to read a credential spec from |
|
| 125 |
+// a file. If not found, we return an empty string and warn in the log. |
|
| 126 |
+// This allows for staging on machines which do not have the necessary components. |
|
| 127 |
+func readCredentialSpecFile(id, root, location string) (string, error) {
|
|
| 128 |
+ if filepath.IsAbs(location) {
|
|
| 129 |
+ return "", fmt.Errorf("invalid credential spec - file:// path cannot be absolute")
|
|
| 130 |
+ } |
|
| 131 |
+ base := filepath.Join(root, credentialSpecFileLocation) |
|
| 132 |
+ full := filepath.Join(base, location) |
|
| 133 |
+ if !strings.HasPrefix(full, base) {
|
|
| 134 |
+ return "", fmt.Errorf("invalid credential spec - file:// path must be under %s", base)
|
|
| 135 |
+ } |
|
| 136 |
+ bcontents, err := ioutil.ReadFile(full) |
|
| 137 |
+ if err != nil {
|
|
| 138 |
+ return "", fmt.Errorf("credential spec '%s' for container %s as the file could not be read: %q", full, id, err)
|
|
| 139 |
+ } |
|
| 140 |
+ return string(bcontents[:]), nil |
|
| 141 |
+} |
| ... | ... |
@@ -37,6 +37,7 @@ Options: |
| 37 | 37 |
--pull Always attempt to pull a newer version of the image |
| 38 | 38 |
-q, --quiet Suppress the build output and print image ID on success |
| 39 | 39 |
--rm Remove intermediate containers after a successful build (default true) |
| 40 |
+ --security-opt value Security Options (default []) |
|
| 40 | 41 |
--shm-size string Size of /dev/shm, default value is 64MB. |
| 41 | 42 |
The format is `<number><unit>`. `number` must be greater than `0`. |
| 42 | 43 |
Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), |
| ... | ... |
@@ -397,6 +398,12 @@ Dockerfile are echoed during the build process. |
| 397 | 397 |
For detailed information on using `ARG` and `ENV` instructions, see the |
| 398 | 398 |
[Dockerfile reference](../builder.md). |
| 399 | 399 |
|
| 400 |
+### Optional security options (--security-opt) |
|
| 401 |
+ |
|
| 402 |
+This flag is only supported on a daemon running on Windows, and only supports |
|
| 403 |
+the `credentialspec` option. The `credentialspec` must be in the format |
|
| 404 |
+`file://spec.txt` or `registry://keyname`. |
|
| 405 |
+ |
|
| 400 | 406 |
### Specify isolation technology for container (--isolation) |
| 401 | 407 |
|
| 402 | 408 |
This option is useful in situations where you are running Docker containers on |
| ... | ... |
@@ -614,6 +614,11 @@ The `--stop-signal` flag sets the system call signal that will be sent to the co |
| 614 | 614 |
This signal can be a valid unsigned number that matches a position in the kernel's syscall table, for instance 9, |
| 615 | 615 |
or a signal name in the format SIGNAME, for instance SIGKILL. |
| 616 | 616 |
|
| 617 |
+### Optional security options (--security-opt) |
|
| 618 |
+ |
|
| 619 |
+On Windows, this flag can be used to specify the `credentialspec` option. |
|
| 620 |
+The `credentialspec` must be in the format `file://spec.txt` or `registry://keyname`. |
|
| 621 |
+ |
|
| 617 | 622 |
### Specify isolation technology for container (--isolation) |
| 618 | 623 |
|
| 619 | 624 |
This option is useful in situations where you are running Docker containers on |
| ... | ... |
@@ -4519,3 +4519,22 @@ func (s *DockerSuite) TestRunStoppedLoggingDriverNoLeak(c *check.C) {
|
| 4519 | 4519 |
// NGoroutines is not updated right away, so we need to wait before failing |
| 4520 | 4520 |
c.Assert(waitForGoroutines(nroutines), checker.IsNil) |
| 4521 | 4521 |
} |
| 4522 |
+ |
|
| 4523 |
+// Handles error conditions for --credentialspec. Validating E2E success cases |
|
| 4524 |
+// requires additional infrastructure (AD for example) on CI servers. |
|
| 4525 |
+func (s *DockerSuite) TestRunCredentialSpecFailures(c *check.C) {
|
|
| 4526 |
+ testRequires(c, DaemonIsWindows) |
|
| 4527 |
+ attempts := []struct{ value, expectedError string }{
|
|
| 4528 |
+ {"rubbish", "invalid credential spec security option - value must be prefixed file:// or registry://"},
|
|
| 4529 |
+ {"rubbish://", "invalid credential spec security option - value must be prefixed file:// or registry://"},
|
|
| 4530 |
+ {"file://", "no value supplied for file:// credential spec security option"},
|
|
| 4531 |
+ {"registry://", "no value supplied for registry:// credential spec security option"},
|
|
| 4532 |
+ {`file://c:\blah.txt`, "path cannot be absolute"},
|
|
| 4533 |
+ {`file://doesnotexist.txt`, "The system cannot find the file specified"},
|
|
| 4534 |
+ } |
|
| 4535 |
+ for _, attempt := range attempts {
|
|
| 4536 |
+ _, _, err := dockerCmdWithError("run", "--security-opt=credentialspec="+attempt.value, "busybox", "true")
|
|
| 4537 |
+ c.Assert(err, checker.NotNil, check.Commentf("%s expected non-nil err", attempt.value))
|
|
| 4538 |
+ c.Assert(err.Error(), checker.Contains, attempt.expectedError, check.Commentf("%s expected %s got %s", attempt.value, attempt.expectedError, err))
|
|
| 4539 |
+ } |
|
| 4540 |
+} |
| ... | ... |
@@ -154,6 +154,10 @@ func (clnt *client) Create(containerID string, checkpoint string, checkpointDir |
| 154 | 154 |
configuration.AllowUnqualifiedDNSQuery = n.AllowUnqualifiedDNSQuery |
| 155 | 155 |
continue |
| 156 | 156 |
} |
| 157 |
+ if c, ok := option.(*CredentialsOption); ok {
|
|
| 158 |
+ configuration.Credentials = c.Credentials |
|
| 159 |
+ continue |
|
| 160 |
+ } |
|
| 157 | 161 |
} |
| 158 | 162 |
|
| 159 | 163 |
// We must have a layer option with at least one path |
| ... | ... |
@@ -62,6 +62,12 @@ type NetworkEndpointsOption struct {
|
| 62 | 62 |
AllowUnqualifiedDNSQuery bool |
| 63 | 63 |
} |
| 64 | 64 |
|
| 65 |
+// CredentialsOption is a CreateOption that indicates the credentials from |
|
| 66 |
+// a credential spec to be used to the runtime |
|
| 67 |
+type CredentialsOption struct {
|
|
| 68 |
+ Credentials string |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 65 | 71 |
// Checkpoint holds the details of a checkpoint (not supported in windows) |
| 66 | 72 |
type Checkpoint struct {
|
| 67 | 73 |
Name string |
| ... | ... |
@@ -39,3 +39,8 @@ func (h *LayerOption) Apply(interface{}) error {
|
| 39 | 39 |
func (s *NetworkEndpointsOption) Apply(interface{}) error {
|
| 40 | 40 |
return nil |
| 41 | 41 |
} |
| 42 |
+ |
|
| 43 |
+// Apply for the credentials option is a no-op. |
|
| 44 |
+func (s *CredentialsOption) Apply(interface{}) error {
|
|
| 45 |
+ return nil |
|
| 46 |
+} |
| ... | ... |
@@ -105,6 +105,7 @@ type ContainerOptions struct {
|
| 105 | 105 |
autoRemove bool |
| 106 | 106 |
init bool |
| 107 | 107 |
initPath string |
| 108 |
+ credentialSpec string |
|
| 108 | 109 |
|
| 109 | 110 |
Image string |
| 110 | 111 |
Args []string |
| ... | ... |
@@ -173,6 +174,7 @@ func AddFlags(flags *pflag.FlagSet) *ContainerOptions {
|
| 173 | 173 |
flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container") |
| 174 | 174 |
flags.Var(&copts.securityOpt, "security-opt", "Security Options") |
| 175 | 175 |
flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") |
| 176 |
+ flags.StringVar(&copts.credentialSpec, "credentialspec", "", "Credential spec for managed service account (Windows only)") |
|
| 176 | 177 |
|
| 177 | 178 |
// Network and port publishing flag |
| 178 | 179 |
flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") |