diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go index 0a7d7321f3..135f7cbbfc 100644 --- a/api/types/swarm/container.go +++ b/api/types/swarm/container.go @@ -21,6 +21,28 @@ type DNSConfig struct { Options []string `json:",omitempty"` } +// SELinuxContext contains the SELinux labels of the container. +type SELinuxContext struct { + Disable bool + + User string + Role string + Type string + Level string +} + +// CredentialSpec for managed service account (Windows only) +type CredentialSpec struct { + File string + Registry string +} + +// Privileges defines the security options for the container. +type Privileges struct { + CredentialSpec *CredentialSpec + SELinuxContext *SELinuxContext +} + // ContainerSpec represents the spec of a container. type ContainerSpec struct { Image string `json:",omitempty"` @@ -32,6 +54,7 @@ type ContainerSpec struct { Dir string `json:",omitempty"` User string `json:",omitempty"` Groups []string `json:",omitempty"` + Privileges *Privileges `json:",omitempty"` StopSignal string `json:",omitempty"` TTY bool `json:",omitempty"` OpenStdin bool `json:",omitempty"` diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 7e16957102..47d417fb25 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -238,6 +238,38 @@ func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy { } } +type credentialSpecOpt struct { + value *swarm.CredentialSpec + source string +} + +func (c *credentialSpecOpt) Set(value string) error { + c.source = value + c.value = &swarm.CredentialSpec{} + switch { + case strings.HasPrefix(value, "file://"): + c.value.File = strings.TrimPrefix(value, "file://") + case strings.HasPrefix(value, "registry://"): + c.value.Registry = strings.TrimPrefix(value, "registry://") + default: + return errors.New("Invalid credential spec - value must be prefixed file:// or registry:// followed by a value") + } + + return nil +} + +func (c *credentialSpecOpt) Type() string { + return "credential-spec" +} + +func (c *credentialSpecOpt) String() string { + return c.source +} + +func (c *credentialSpecOpt) Value() *swarm.CredentialSpec { + return c.value +} + func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { nets := []swarm.NetworkAttachmentConfig{} for _, network := range networks { @@ -355,6 +387,7 @@ type serviceOptions struct { workdir string user string groups opts.ListOpts + credentialSpec credentialSpecOpt stopSignal string tty bool readOnly bool @@ -500,6 +533,12 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { EndpointSpec: opts.endpoint.ToEndpointSpec(), } + if opts.credentialSpec.Value() != nil { + service.TaskTemplate.ContainerSpec.Privileges = &swarm.Privileges{ + CredentialSpec: opts.credentialSpec.Value(), + } + } + return service, nil } @@ -511,6 +550,8 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions) { flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") + flags.Var(&opts.credentialSpec, flagCredentialSpec, "Credential spec for managed service account (Windows only)") + flags.SetAnnotation(flagCredentialSpec, "version", []string{"1.29"}) flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") flags.SetAnnotation(flagHostname, "version", []string{"1.25"}) flags.Var(&opts.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image") @@ -582,6 +623,7 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions) { } const ( + flagCredentialSpec = "credential-spec" flagPlacementPref = "placement-pref" flagPlacementPrefAdd = "placement-pref-add" flagPlacementPrefRemove = "placement-pref-rm" diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index 6bf1f9939e..b2ccc9f6b1 100644 --- a/daemon/cluster/convert/container.go +++ b/daemon/cluster/convert/container.go @@ -1,6 +1,7 @@ package convert import ( + "errors" "fmt" "strings" @@ -39,6 +40,31 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec { } } + // Privileges + if c.Privileges != nil { + containerSpec.Privileges = &types.Privileges{} + + if c.Privileges.CredentialSpec != nil { + containerSpec.Privileges.CredentialSpec = &types.CredentialSpec{} + switch c.Privileges.CredentialSpec.Source.(type) { + case *swarmapi.Privileges_CredentialSpec_File: + containerSpec.Privileges.CredentialSpec.File = c.Privileges.CredentialSpec.GetFile() + case *swarmapi.Privileges_CredentialSpec_Registry: + containerSpec.Privileges.CredentialSpec.Registry = c.Privileges.CredentialSpec.GetRegistry() + } + } + + if c.Privileges.SELinuxContext != nil { + containerSpec.Privileges.SELinuxContext = &types.SELinuxContext{ + Disable: c.Privileges.SELinuxContext.Disable, + User: c.Privileges.SELinuxContext.User, + Type: c.Privileges.SELinuxContext.Type, + Role: c.Privileges.SELinuxContext.Role, + Level: c.Privileges.SELinuxContext.Level, + } + } + } + // Mounts for _, m := range c.Mounts { mount := mounttypes.Mount{ @@ -166,6 +192,40 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) { containerSpec.StopGracePeriod = gogotypes.DurationProto(*c.StopGracePeriod) } + // Privileges + if c.Privileges != nil { + containerSpec.Privileges = &swarmapi.Privileges{} + + if c.Privileges.CredentialSpec != nil { + containerSpec.Privileges.CredentialSpec = &swarmapi.Privileges_CredentialSpec{} + + if c.Privileges.CredentialSpec.File != "" && c.Privileges.CredentialSpec.Registry != "" { + return nil, errors.New("cannot specify both \"file\" and \"registry\" credential specs") + } + if c.Privileges.CredentialSpec.File != "" { + containerSpec.Privileges.CredentialSpec.Source = &swarmapi.Privileges_CredentialSpec_File{ + File: c.Privileges.CredentialSpec.File, + } + } else if c.Privileges.CredentialSpec.Registry != "" { + containerSpec.Privileges.CredentialSpec.Source = &swarmapi.Privileges_CredentialSpec_Registry{ + Registry: c.Privileges.CredentialSpec.Registry, + } + } else { + return nil, errors.New("must either provide \"file\" or \"registry\" for credential spec") + } + } + + if c.Privileges.SELinuxContext != nil { + containerSpec.Privileges.SELinuxContext = &swarmapi.Privileges_SELinuxContext{ + Disable: c.Privileges.SELinuxContext.Disable, + User: c.Privileges.SELinuxContext.User, + Type: c.Privileges.SELinuxContext.Type, + Role: c.Privileges.SELinuxContext.Role, + Level: c.Privileges.SELinuxContext.Level, + } + } + } + // Mounts for _, m := range c.Mounts { mount := swarmapi.Mount{ diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index c73bb2dbdf..dcac7281b2 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -351,6 +351,8 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig { hc.DNSOptions = c.spec().DNSConfig.Options } + c.applyPrivileges(hc) + // The format of extra hosts on swarmkit is specified in: // http://man7.org/linux/man-pages/man5/hosts.5.html // IP_address canonical_hostname [aliases...] @@ -600,6 +602,42 @@ func (c *containerConfig) networkCreateRequest(name string) (clustertypes.Networ }, nil } +func (c *containerConfig) applyPrivileges(hc *enginecontainer.HostConfig) { + privileges := c.spec().Privileges + if privileges == nil { + return + } + + credentials := privileges.CredentialSpec + if credentials != nil { + switch credentials.Source.(type) { + case *api.Privileges_CredentialSpec_File: + hc.SecurityOpt = append(hc.SecurityOpt, "credentialspec=file://"+credentials.GetFile()) + case *api.Privileges_CredentialSpec_Registry: + hc.SecurityOpt = append(hc.SecurityOpt, "credentialspec=registry://"+credentials.GetRegistry()) + } + } + + selinux := privileges.SELinuxContext + if selinux != nil { + if selinux.Disable { + hc.SecurityOpt = append(hc.SecurityOpt, "label=disable") + } + if selinux.User != "" { + hc.SecurityOpt = append(hc.SecurityOpt, "label=user:"+selinux.User) + } + if selinux.Role != "" { + hc.SecurityOpt = append(hc.SecurityOpt, "label=role:"+selinux.Role) + } + if selinux.Level != "" { + hc.SecurityOpt = append(hc.SecurityOpt, "label=level:"+selinux.Level) + } + if selinux.Type != "" { + hc.SecurityOpt = append(hc.SecurityOpt, "label=type:"+selinux.Type) + } + } +} + func (c containerConfig) eventFilter() filters.Args { filter := filters.NewArgs() filter.Add("type", events.ContainerEventType)