From 3716ec25b423d8ff7dfa231a7b3cf0154726ed37 Mon Sep 17 00:00:00 2001
From: Evan Hazlett <ejhazlett@gmail.com>
Date: Wed, 19 Oct 2016 12:22:02 -0400
Subject: [PATCH] secrets: secret management for swarm

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

wip: use tmpfs for swarm secrets

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

wip: inject secrets from swarm secret store

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

secrets: use secret names in cli for service create

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

switch to use mounts instead of volumes

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

vendor: use ehazlett swarmkit

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>

secrets: finish secret update

Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
---
 api/server/router/swarm/backend.go            |   5 +
 api/server/router/swarm/cluster.go            |   5 +
 api/server/router/swarm/cluster_routes.go     |  74 ++++++++++
 api/types/container/secret.go                 |  12 ++
 api/types/swarm/container.go                  |   1 +
 api/types/swarm/secret.go                     |  30 ++++
 api/types/types.go                            |  13 ++
 cli/command/commands/commands.go              |   2 +
 cli/command/secret/cmd.go                     |  29 ++++
 cli/command/secret/create.go                  |  57 ++++++++
 cli/command/secret/inspect.go                 |  42 ++++++
 cli/command/secret/ls.go                      |  62 +++++++++
 cli/command/secret/remove.go                  |  43 ++++++
 cli/command/service/create.go                 |   7 +
 cli/command/service/opts.go                   |  17 +++
 cli/command/service/parse.go                  |  92 ++++++++++++
 client/errors.go                              |  22 +++
 client/interface.go                           |   9 ++
 client/secret_create.go                       |  24 ++++
 client/secret_create_test.go                  |  57 ++++++++
 client/secret_inspect.go                      |  34 +++++
 client/secret_inspect_test.go                 |  65 +++++++++
 client/secret_list.go                         |  35 +++++
 client/secret_list_test.go                    |  94 +++++++++++++
 client/secret_remove.go                       |  10 ++
 client/secret_remove_test.go                  |  47 +++++++
 container/container.go                        |   5 +-
 container/container_unix.go                   |  29 +++-
 daemon/cluster/convert/container.go           |  43 ++++++
 daemon/cluster/convert/secret.go              |  46 ++++++
 daemon/cluster/executor/backend.go            |   1 +
 daemon/cluster/executor/container/adapter.go  |  34 ++++-
 .../cluster/executor/container/attachment.go  |   5 +-
 .../cluster/executor/container/controller.go  |   4 +-
 daemon/cluster/executor/container/executor.go |  10 +-
 .../cluster/executor/container/health_test.go |   2 +-
 .../executor/container/validate_test.go       |   2 +-
 daemon/cluster/filters.go                     |  18 +++
 daemon/cluster/secrets.go                     | 131 ++++++++++++++++++
 daemon/container_operations_unix.go           |  39 ++++++
 daemon/daemon.go                              |   1 +
 daemon/oci_linux.go                           |   7 +
 daemon/secrets.go                             |  22 +++
 daemon/secrets_linux.go                       |   7 +
 daemon/secrets_unsupported.go                 |   7 +
 daemon/start.go                               |   4 +
 46 files changed, 1292 insertions(+), 13 deletions(-)
 create mode 100644 api/types/container/secret.go
 create mode 100644 api/types/swarm/secret.go
 create mode 100644 cli/command/secret/cmd.go
 create mode 100644 cli/command/secret/create.go
 create mode 100644 cli/command/secret/inspect.go
 create mode 100644 cli/command/secret/ls.go
 create mode 100644 cli/command/secret/remove.go
 create mode 100644 cli/command/service/parse.go
 create mode 100644 client/secret_create.go
 create mode 100644 client/secret_create_test.go
 create mode 100644 client/secret_inspect.go
 create mode 100644 client/secret_inspect_test.go
 create mode 100644 client/secret_list.go
 create mode 100644 client/secret_list_test.go
 create mode 100644 client/secret_remove.go
 create mode 100644 client/secret_remove_test.go
 create mode 100644 daemon/cluster/convert/secret.go
 create mode 100644 daemon/cluster/secrets.go
 create mode 100644 daemon/secrets.go
 create mode 100644 daemon/secrets_linux.go
 create mode 100644 daemon/secrets_unsupported.go

diff --git a/api/server/router/swarm/backend.go b/api/server/router/swarm/backend.go
index a7cc9eef40..1ab09a8f8b 100644
--- a/api/server/router/swarm/backend.go
+++ b/api/server/router/swarm/backend.go
@@ -23,4 +23,9 @@ type Backend interface {
 	RemoveNode(string, bool) error
 	GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
 	GetTask(string) (types.Task, error)
+	GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error)
+	CreateSecret(s types.SecretSpec) (string, error)
+	RemoveSecret(id string) error
+	GetSecret(id string) (types.Secret, error)
+	UpdateSecret(id string, version uint64, spec types.SecretSpec) error
 }
diff --git a/api/server/router/swarm/cluster.go b/api/server/router/swarm/cluster.go
index a67ffa9632..ec92e7aaa0 100644
--- a/api/server/router/swarm/cluster.go
+++ b/api/server/router/swarm/cluster.go
@@ -40,5 +40,10 @@ func (sr *swarmRouter) initRoutes() {
 		router.NewPostRoute("/nodes/{id:.*}/update", sr.updateNode),
 		router.NewGetRoute("/tasks", sr.getTasks),
 		router.NewGetRoute("/tasks/{id:.*}", sr.getTask),
+		router.NewGetRoute("/secrets", sr.getSecrets),
+		router.NewPostRoute("/secrets/create", sr.createSecret),
+		router.NewDeleteRoute("/secrets/{id:.*}", sr.removeSecret),
+		router.NewGetRoute("/secrets/{id:.*}", sr.getSecret),
+		router.NewPostRoute("/secrets/{id:.*}/update", sr.updateSecret),
 	}
 }
diff --git a/api/server/router/swarm/cluster_routes.go b/api/server/router/swarm/cluster_routes.go
index bc78601692..fc33d27746 100644
--- a/api/server/router/swarm/cluster_routes.go
+++ b/api/server/router/swarm/cluster_routes.go
@@ -261,3 +261,77 @@ func (sr *swarmRouter) getTask(ctx context.Context, w http.ResponseWriter, r *ht
 
 	return httputils.WriteJSON(w, http.StatusOK, task)
 }
+
+func (sr *swarmRouter) getSecrets(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := httputils.ParseForm(r); err != nil {
+		return err
+	}
+	filter, err := filters.FromParam(r.Form.Get("filters"))
+	if err != nil {
+		return err
+	}
+
+	secrets, err := sr.backend.GetSecrets(basictypes.SecretListOptions{Filter: filter})
+	if err != nil {
+		logrus.Errorf("Error getting secrets: %v", err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, secrets)
+}
+
+func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var secret types.SecretSpec
+	if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
+		return err
+	}
+
+	id, err := sr.backend.CreateSecret(secret)
+	if err != nil {
+		logrus.Errorf("Error creating secret %s: %v", id, err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusCreated, &basictypes.SecretCreateResponse{
+		ID: id,
+	})
+}
+
+func (sr *swarmRouter) removeSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	if err := sr.backend.RemoveSecret(vars["id"]); err != nil {
+		logrus.Errorf("Error removing secret %s: %v", vars["id"], err)
+		return err
+	}
+
+	return nil
+}
+
+func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	secret, err := sr.backend.GetSecret(vars["id"])
+	if err != nil {
+		logrus.Errorf("Error getting secret %s: %v", vars["id"], err)
+		return err
+	}
+
+	return httputils.WriteJSON(w, http.StatusOK, secret)
+}
+
+func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
+	var secret types.SecretSpec
+	if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
+		return err
+	}
+
+	rawVersion := r.URL.Query().Get("version")
+	version, err := strconv.ParseUint(rawVersion, 10, 64)
+	if err != nil {
+		return fmt.Errorf("Invalid secret version '%s': %s", rawVersion, err.Error())
+	}
+
+	id := vars["id"]
+	if err := sr.backend.UpdateSecret(id, version, secret); err != nil {
+		return fmt.Errorf("Error updating secret: %s", err)
+	}
+
+	return nil
+}
diff --git a/api/types/container/secret.go b/api/types/container/secret.go
new file mode 100644
index 0000000000..eee5bf89d2
--- /dev/null
+++ b/api/types/container/secret.go
@@ -0,0 +1,12 @@
+package container
+
+import "os"
+
+type ContainerSecret struct {
+	Name   string
+	Target string
+	Data   []byte
+	Uid    int
+	Gid    int
+	Mode   os.FileMode
+}
diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go
index e1ab81c5f9..39f1d3987c 100644
--- a/api/types/swarm/container.go
+++ b/api/types/swarm/container.go
@@ -37,4 +37,5 @@ type ContainerSpec struct {
 	StopGracePeriod *time.Duration          `json:",omitempty"`
 	Healthcheck     *container.HealthConfig `json:",omitempty"`
 	DNSConfig       *DNSConfig              `json:",omitempty"`
+	Secrets         []*SecretReference      `json:",omitempty"`
 }
diff --git a/api/types/swarm/secret.go b/api/types/swarm/secret.go
new file mode 100644
index 0000000000..3323ba212d
--- /dev/null
+++ b/api/types/swarm/secret.go
@@ -0,0 +1,30 @@
+package swarm
+
+// Secret represents a secret.
+type Secret struct {
+	ID string
+	Meta
+	Spec       *SecretSpec `json:",omitempty"`
+	Digest     string      `json:",omitempty"`
+	SecretSize int64       `json:",omitempty"`
+}
+
+type SecretSpec struct {
+	Annotations
+	Data []byte `json",omitempty"`
+}
+
+type SecretReferenceMode int
+
+const (
+	SecretReferenceSystem SecretReferenceMode = 0
+	SecretReferenceFile   SecretReferenceMode = 1
+	SecretReferenceEnv    SecretReferenceMode = 2
+)
+
+type SecretReference struct {
+	SecretID   string              `json:",omitempty"`
+	Mode       SecretReferenceMode `json:",omitempty"`
+	Target     string              `json:",omitempty"`
+	SecretName string              `json:",omitempty"`
+}
diff --git a/api/types/types.go b/api/types/types.go
index 5591646b69..fe60755c84 100644
--- a/api/types/types.go
+++ b/api/types/types.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/mount"
 	"github.com/docker/docker/api/types/network"
 	"github.com/docker/docker/api/types/registry"
@@ -509,3 +510,15 @@ type ImagesPruneReport struct {
 type NetworksPruneReport struct {
 	NetworksDeleted []string
 }
+
+// SecretCreateResponse contains the information returned to a client
+// on the creation of a new secret.
+type SecretCreateResponse struct {
+	// ID is the id of the created secret.
+	ID string
+}
+
+// SecretListOptions holds parameters to list secrets
+type SecretListOptions struct {
+	Filter filters.Args
+}
diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go
index fad709bca1..d64d5680cc 100644
--- a/cli/command/commands/commands.go
+++ b/cli/command/commands/commands.go
@@ -11,6 +11,7 @@ import (
 	"github.com/docker/docker/cli/command/node"
 	"github.com/docker/docker/cli/command/plugin"
 	"github.com/docker/docker/cli/command/registry"
+	"github.com/docker/docker/cli/command/secret"
 	"github.com/docker/docker/cli/command/service"
 	"github.com/docker/docker/cli/command/stack"
 	"github.com/docker/docker/cli/command/swarm"
@@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
 		node.NewNodeCommand(dockerCli),
 		service.NewServiceCommand(dockerCli),
 		swarm.NewSwarmCommand(dockerCli),
+		secret.NewSecretCommand(dockerCli),
 		container.NewContainerCommand(dockerCli),
 		image.NewImageCommand(dockerCli),
 		system.NewSystemCommand(dockerCli),
diff --git a/cli/command/secret/cmd.go b/cli/command/secret/cmd.go
new file mode 100644
index 0000000000..995300ad77
--- /dev/null
+++ b/cli/command/secret/cmd.go
@@ -0,0 +1,29 @@
+package secret
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+)
+
+// NewSecretCommand returns a cobra command for `secret` subcommands
+func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "secret",
+		Short: "Manage Docker secrets",
+		Args:  cli.NoArgs,
+		Run: func(cmd *cobra.Command, args []string) {
+			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
+		},
+	}
+	cmd.AddCommand(
+		newSecretListCommand(dockerCli),
+		newSecretCreateCommand(dockerCli),
+		newSecretInspectCommand(dockerCli),
+		newSecretRemoveCommand(dockerCli),
+	)
+	return cmd
+}
diff --git a/cli/command/secret/create.go b/cli/command/secret/create.go
new file mode 100644
index 0000000000..1c0e933f57
--- /dev/null
+++ b/cli/command/secret/create.go
@@ -0,0 +1,57 @@
+package secret
+
+import (
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/spf13/cobra"
+)
+
+type createOptions struct {
+	name string
+}
+
+func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
+	return &cobra.Command{
+		Use:   "create [name]",
+		Short: "Create a secret using stdin as content",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts := createOptions{
+				name: args[0],
+			}
+
+			return runSecretCreate(dockerCli, opts)
+		},
+	}
+}
+
+func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	secretData, err := ioutil.ReadAll(os.Stdin)
+	if err != nil {
+		return fmt.Errorf("Error reading content from STDIN: %v", err)
+	}
+
+	spec := swarm.SecretSpec{
+		Annotations: swarm.Annotations{
+			Name: opts.name,
+		},
+		Data: secretData,
+	}
+
+	r, err := client.SecretCreate(ctx, spec)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintln(dockerCli.Out(), r.ID)
+	return nil
+}
diff --git a/cli/command/secret/inspect.go b/cli/command/secret/inspect.go
new file mode 100644
index 0000000000..c8d5cd8f79
--- /dev/null
+++ b/cli/command/secret/inspect.go
@@ -0,0 +1,42 @@
+package secret
+
+import (
+	"context"
+
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/command/inspect"
+	"github.com/spf13/cobra"
+)
+
+type inspectOptions struct {
+	name   string
+	format string
+}
+
+func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
+	opts := inspectOptions{}
+	cmd := &cobra.Command{
+		Use:   "inspect [name]",
+		Short: "Inspect a secret",
+		Args:  cli.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts.name = args[0]
+			return runSecretInspect(dockerCli, opts)
+		},
+	}
+
+	cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
+	return cmd
+}
+
+func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	getRef := func(name string) (interface{}, []byte, error) {
+		return client.SecretInspectWithRaw(ctx, name)
+	}
+
+	return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef)
+}
diff --git a/cli/command/secret/ls.go b/cli/command/secret/ls.go
new file mode 100644
index 0000000000..1befdad9d0
--- /dev/null
+++ b/cli/command/secret/ls.go
@@ -0,0 +1,62 @@
+package secret
+
+import (
+	"context"
+	"fmt"
+	"text/tabwriter"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/spf13/cobra"
+)
+
+type listOptions struct {
+	quiet bool
+}
+
+func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command {
+	opts := listOptions{}
+
+	cmd := &cobra.Command{
+		Use:   "ls",
+		Short: "List secrets",
+		Args:  cli.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runSecretList(dockerCli, opts)
+		},
+	}
+
+	flags := cmd.Flags()
+	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
+
+	return cmd
+}
+
+func runSecretList(dockerCli *command.DockerCli, opts listOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	secrets, err := client.SecretList(ctx, types.SecretListOptions{})
+	if err != nil {
+		return err
+	}
+
+	w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0)
+	if opts.quiet {
+		for _, s := range secrets {
+			fmt.Fprintf(w, "%s\n", s.ID)
+		}
+	} else {
+		fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE")
+		fmt.Fprintf(w, "\n")
+
+		for _, s := range secrets {
+			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", s.ID, s.Spec.Annotations.Name, s.Meta.CreatedAt, s.Meta.UpdatedAt, s.SecretSize)
+		}
+	}
+
+	w.Flush()
+
+	return nil
+}
diff --git a/cli/command/secret/remove.go b/cli/command/secret/remove.go
new file mode 100644
index 0000000000..f336c6161a
--- /dev/null
+++ b/cli/command/secret/remove.go
@@ -0,0 +1,43 @@
+package secret
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/docker/docker/cli"
+	"github.com/docker/docker/cli/command"
+	"github.com/spf13/cobra"
+)
+
+type removeOptions struct {
+	ids []string
+}
+
+func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
+	return &cobra.Command{
+		Use:   "rm [id]",
+		Short: "Remove a secret",
+		Args:  cli.RequiresMinArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			opts := removeOptions{
+				ids: args,
+			}
+			return runSecretRemove(dockerCli, opts)
+		},
+	}
+}
+
+func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error {
+	client := dockerCli.Client()
+	ctx := context.Background()
+
+	for _, id := range opts.ids {
+		if err := client.SecretRemove(ctx, id); err != nil {
+			return err
+		}
+
+		fmt.Fprintln(dockerCli.Out(), id)
+	}
+
+	return nil
+}
diff --git a/cli/command/service/create.go b/cli/command/service/create.go
index d6c3ebdb9c..8fb9070e67 100644
--- a/cli/command/service/create.go
+++ b/cli/command/service/create.go
@@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
 		return err
 	}
 
+	// parse and validate secrets
+	secrets, err := parseSecrets(apiClient, opts.secrets)
+	if err != nil {
+		return err
+	}
+	service.TaskTemplate.ContainerSpec.Secrets = secrets
+
 	ctx := context.Background()
 
 	// only send auth if flag was set
diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go
index 827c4e5cdc..a4fd08881c 100644
--- a/cli/command/service/opts.go
+++ b/cli/command/service/opts.go
@@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
 	return nets
 }
 
+func convertSecrets(secrets []string) []*swarm.SecretReference {
+	sec := []*swarm.SecretReference{}
+	for _, s := range secrets {
+		sec = append(sec, &swarm.SecretReference{
+			SecretID: s,
+			Mode:     swarm.SecretReferenceFile,
+			Target:   "",
+		})
+	}
+
+	return sec
+}
+
 type endpointOptions struct {
 	mode  string
 	ports opts.ListOpts
@@ -337,6 +350,7 @@ type serviceOptions struct {
 	logDriver logDriverOptions
 
 	healthcheck healthCheckOptions
+	secrets     []string
 }
 
 func newServiceOptions() *serviceOptions {
@@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
 					Options:     opts.dnsOptions.GetAll(),
 				},
 				StopGracePeriod: opts.stopGrace.Value(),
+				Secrets:         convertSecrets(opts.secrets),
 			},
 			Networks:      convertNetworks(opts.networks.GetAll()),
 			Resources:     opts.resources.ToResourceRequirements(),
@@ -488,6 +503,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
 	flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK")
 
 	flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY")
+	flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service")
 }
 
 const (
@@ -553,4 +569,5 @@ const (
 	flagHealthRetries         = "health-retries"
 	flagHealthTimeout         = "health-timeout"
 	flagNoHealthcheck         = "no-healthcheck"
+	flagSecret                = "secret"
 )
diff --git a/cli/command/service/parse.go b/cli/command/service/parse.go
new file mode 100644
index 0000000000..41883fb445
--- /dev/null
+++ b/cli/command/service/parse.go
@@ -0,0 +1,92 @@
+package service
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	swarmtypes "github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/client"
+)
+
+// parseSecretString parses the requested secret and returns the secret name
+// and target.  Expects format SECRET_NAME:TARGET
+func parseSecretString(secretString string) (string, string, error) {
+	tokens := strings.Split(secretString, ":")
+
+	secretName := strings.TrimSpace(tokens[0])
+	targetName := ""
+
+	if secretName == "" {
+		return "", "", fmt.Errorf("invalid secret name provided")
+	}
+
+	if len(tokens) > 1 {
+		targetName = strings.TrimSpace(tokens[1])
+		if targetName == "" {
+			return "", "", fmt.Errorf("invalid presentation name provided")
+		}
+	} else {
+		targetName = secretName
+	}
+	return secretName, targetName, nil
+}
+
+// parseSecrets retrieves the secrets from the requested names and converts
+// them to secret references to use with the spec
+func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) {
+	lookupSecretNames := []string{}
+	needSecrets := make(map[string]*swarmtypes.SecretReference)
+	ctx := context.Background()
+
+	for _, secret := range requestedSecrets {
+		n, t, err := parseSecretString(secret)
+		if err != nil {
+			return nil, err
+		}
+
+		secretRef := &swarmtypes.SecretReference{
+			SecretName: n,
+			Mode:       swarmtypes.SecretReferenceFile,
+			Target:     t,
+		}
+
+		lookupSecretNames = append(lookupSecretNames, n)
+		needSecrets[n] = secretRef
+	}
+
+	args := filters.NewArgs()
+	for _, s := range lookupSecretNames {
+		args.Add("names", s)
+	}
+
+	secrets, err := client.SecretList(ctx, types.SecretListOptions{
+		Filter: args,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	foundSecrets := make(map[string]*swarmtypes.Secret)
+	for _, secret := range secrets {
+		foundSecrets[secret.Spec.Annotations.Name] = &secret
+	}
+
+	addedSecrets := []*swarmtypes.SecretReference{}
+
+	for secretName, secretRef := range needSecrets {
+		s, ok := foundSecrets[secretName]
+		if !ok {
+			return nil, fmt.Errorf("secret not found: %s", secretName)
+		}
+
+		// set the id for the ref to properly assign in swarm
+		// since swarm needs the ID instead of the name
+		secretRef.SecretID = s.ID
+		addedSecrets = append(addedSecrets, secretRef)
+	}
+
+	return addedSecrets, nil
+}
diff --git a/client/errors.go b/client/errors.go
index 53e2065332..db7294daa8 100644
--- a/client/errors.go
+++ b/client/errors.go
@@ -217,3 +217,25 @@ func (cli *Client) NewVersionError(APIrequired, feature string) error {
 	}
 	return nil
 }
+
+// secretNotFoundError implements an error returned when a secret is not found.
+type secretNotFoundError struct {
+	name string
+}
+
+// Error returns a string representation of a secretNotFoundError
+func (e secretNotFoundError) Error() string {
+	return fmt.Sprintf("Error: No such secret: %s", e.name)
+}
+
+// NoFound indicates that this error type is of NotFound
+func (e secretNotFoundError) NotFound() bool {
+	return true
+}
+
+// IsErrSecretNotFound returns true if the error is caused
+// when a secret is not found.
+func IsErrSecretNotFound(err error) bool {
+	_, ok := err.(secretNotFoundError)
+	return ok
+}
diff --git a/client/interface.go b/client/interface.go
index 99b06709b5..49b66b1d17 100644
--- a/client/interface.go
+++ b/client/interface.go
@@ -23,6 +23,7 @@ type CommonAPIClient interface {
 	NetworkAPIClient
 	ServiceAPIClient
 	SwarmAPIClient
+	SecretAPIClient
 	SystemAPIClient
 	VolumeAPIClient
 	ClientVersion() string
@@ -141,3 +142,11 @@ type VolumeAPIClient interface {
 	VolumeRemove(ctx context.Context, volumeID string, force bool) error
 	VolumesPrune(ctx context.Context, cfg types.VolumesPruneConfig) (types.VolumesPruneReport, error)
 }
+
+// SecretAPIClient defines API client methods for secrets
+type SecretAPIClient interface {
+	SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error)
+	SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error)
+	SecretRemove(ctx context.Context, id string) error
+	SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error)
+}
diff --git a/client/secret_create.go b/client/secret_create.go
new file mode 100644
index 0000000000..de8b041567
--- /dev/null
+++ b/client/secret_create.go
@@ -0,0 +1,24 @@
+package client
+
+import (
+	"encoding/json"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+// SecretCreate creates a new Secret.
+func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
+	var headers map[string][]string
+
+	var response types.SecretCreateResponse
+	resp, err := cli.post(ctx, "/secrets/create", nil, secret, headers)
+	if err != nil {
+		return response, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&response)
+	ensureReaderClosed(resp)
+	return response, err
+}
diff --git a/client/secret_create_test.go b/client/secret_create_test.go
new file mode 100644
index 0000000000..d264eb6692
--- /dev/null
+++ b/client/secret_create_test.go
@@ -0,0 +1,57 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+func TestSecretCreateError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+	_, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestSecretCreate(t *testing.T) {
+	expectedURL := "/secrets/create"
+	client := &Client{
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "POST" {
+				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
+			}
+			b, err := json.Marshal(types.SecretCreateResponse{
+				ID: "test_secret",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(b)),
+			}, nil
+		}),
+	}
+
+	r, err := client.SecretCreate(context.Background(), swarm.SecretSpec{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if r.ID != "test_secret" {
+		t.Fatalf("expected `test_secret`, got %s", r.ID)
+	}
+}
diff --git a/client/secret_inspect.go b/client/secret_inspect.go
new file mode 100644
index 0000000000..f774576118
--- /dev/null
+++ b/client/secret_inspect.go
@@ -0,0 +1,34 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+// SecretInspectWithRaw returns the secret information with raw data
+func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
+	resp, err := cli.get(ctx, "/secrets/"+id, nil, nil)
+	if err != nil {
+		if resp.statusCode == http.StatusNotFound {
+			return swarm.Secret{}, nil, secretNotFoundError{id}
+		}
+		return swarm.Secret{}, nil, err
+	}
+	defer ensureReaderClosed(resp)
+
+	body, err := ioutil.ReadAll(resp.body)
+	if err != nil {
+		return swarm.Secret{}, nil, err
+	}
+
+	var secret swarm.Secret
+	rdr := bytes.NewReader(body)
+	err = json.NewDecoder(rdr).Decode(&secret)
+
+	return secret, body, err
+}
diff --git a/client/secret_inspect_test.go b/client/secret_inspect_test.go
new file mode 100644
index 0000000000..423d986968
--- /dev/null
+++ b/client/secret_inspect_test.go
@@ -0,0 +1,65 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+func TestSecretInspectError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	_, _, err := client.SecretInspectWithRaw(context.Background(), "nothing")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestSecretInspectSecretNotFound(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
+	}
+
+	_, _, err := client.SecretInspectWithRaw(context.Background(), "unknown")
+	if err == nil || !IsErrSecretNotFound(err) {
+		t.Fatalf("expected an secretNotFoundError error, got %v", err)
+	}
+}
+
+func TestSecretInspect(t *testing.T) {
+	expectedURL := "/secrets/secret_id"
+	client := &Client{
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			content, err := json.Marshal(swarm.Secret{
+				ID: "secret_id",
+			})
+			if err != nil {
+				return nil, err
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader(content)),
+			}, nil
+		}),
+	}
+
+	secretInspect, _, err := client.SecretInspectWithRaw(context.Background(), "secret_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if secretInspect.ID != "secret_id" {
+		t.Fatalf("expected `secret_id`, got %s", secretInspect.ID)
+	}
+}
diff --git a/client/secret_list.go b/client/secret_list.go
new file mode 100644
index 0000000000..5e9d2b5098
--- /dev/null
+++ b/client/secret_list.go
@@ -0,0 +1,35 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+// SecretList returns the list of secrets.
+func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
+	query := url.Values{}
+
+	if options.Filter.Len() > 0 {
+		filterJSON, err := filters.ToParam(options.Filter)
+		if err != nil {
+			return nil, err
+		}
+
+		query.Set("filters", filterJSON)
+	}
+
+	resp, err := cli.get(ctx, "/secrets", query, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var secrets []swarm.Secret
+	err = json.NewDecoder(resp.body).Decode(&secrets)
+	ensureReaderClosed(resp)
+	return secrets, err
+}
diff --git a/client/secret_list_test.go b/client/secret_list_test.go
new file mode 100644
index 0000000000..174963c7ee
--- /dev/null
+++ b/client/secret_list_test.go
@@ -0,0 +1,94 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/swarm"
+	"golang.org/x/net/context"
+)
+
+func TestSecretListError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	_, err := client.SecretList(context.Background(), types.SecretListOptions{})
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestSecretList(t *testing.T) {
+	expectedURL := "/secrets"
+
+	filters := filters.NewArgs()
+	filters.Add("label", "label1")
+	filters.Add("label", "label2")
+
+	listCases := []struct {
+		options             types.SecretListOptions
+		expectedQueryParams map[string]string
+	}{
+		{
+			options: types.SecretListOptions{},
+			expectedQueryParams: map[string]string{
+				"filters": "",
+			},
+		},
+		{
+			options: types.SecretListOptions{
+				Filter: filters,
+			},
+			expectedQueryParams: map[string]string{
+				"filters": `{"label":{"label1":true,"label2":true}}`,
+			},
+		},
+	}
+	for _, listCase := range listCases {
+		client := &Client{
+			client: newMockClient(func(req *http.Request) (*http.Response, error) {
+				if !strings.HasPrefix(req.URL.Path, expectedURL) {
+					return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+				}
+				query := req.URL.Query()
+				for key, expected := range listCase.expectedQueryParams {
+					actual := query.Get(key)
+					if actual != expected {
+						return nil, fmt.Errorf("%s not set in URL query properly. Expected '%s', got %s", key, expected, actual)
+					}
+				}
+				content, err := json.Marshal([]swarm.Secret{
+					{
+						ID: "secret_id1",
+					},
+					{
+						ID: "secret_id2",
+					},
+				})
+				if err != nil {
+					return nil, err
+				}
+				return &http.Response{
+					StatusCode: http.StatusOK,
+					Body:       ioutil.NopCloser(bytes.NewReader(content)),
+				}, nil
+			}),
+		}
+
+		secrets, err := client.SecretList(context.Background(), listCase.options)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(secrets) != 2 {
+			t.Fatalf("expected 2 secrets, got %v", secrets)
+		}
+	}
+}
diff --git a/client/secret_remove.go b/client/secret_remove.go
new file mode 100644
index 0000000000..1955b988a9
--- /dev/null
+++ b/client/secret_remove.go
@@ -0,0 +1,10 @@
+package client
+
+import "golang.org/x/net/context"
+
+// SecretRemove removes a Secret.
+func (cli *Client) SecretRemove(ctx context.Context, id string) error {
+	resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil)
+	ensureReaderClosed(resp)
+	return err
+}
diff --git a/client/secret_remove_test.go b/client/secret_remove_test.go
new file mode 100644
index 0000000000..f269f787d2
--- /dev/null
+++ b/client/secret_remove_test.go
@@ -0,0 +1,47 @@
+package client
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"golang.org/x/net/context"
+)
+
+func TestSecretRemoveError(t *testing.T) {
+	client := &Client{
+		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
+	}
+
+	err := client.SecretRemove(context.Background(), "secret_id")
+	if err == nil || err.Error() != "Error response from daemon: Server error" {
+		t.Fatalf("expected a Server Error, got %v", err)
+	}
+}
+
+func TestSecretRemove(t *testing.T) {
+	expectedURL := "/secrets/secret_id"
+
+	client := &Client{
+		client: newMockClient(func(req *http.Request) (*http.Response, error) {
+			if !strings.HasPrefix(req.URL.Path, expectedURL) {
+				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
+			}
+			if req.Method != "DELETE" {
+				return nil, fmt.Errorf("expected DELETE method, got %s", req.Method)
+			}
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
+			}, nil
+		}),
+	}
+
+	err := client.SecretRemove(context.Background(), "secret_id")
+	if err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/container/container.go b/container/container.go
index 722271be96..74d080d46c 100644
--- a/container/container.go
+++ b/container/container.go
@@ -89,8 +89,9 @@ type CommonContainer struct {
 	HasBeenStartedBefore   bool
 	HasBeenManuallyStopped bool // used for unless-stopped restart policy
 	MountPoints            map[string]*volume.MountPoint
-	HostConfig             *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
-	ExecCommands           *exec.Store                `json:"-"`
+	HostConfig             *containertypes.HostConfig        `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable
+	ExecCommands           *exec.Store                       `json:"-"`
+	Secrets                []*containertypes.ContainerSecret `json:"-"` // do not serialize
 	// logDriver for closing
 	LogDriver      logger.Logger  `json:"-"`
 	LogCopier      *logger.Copier `json:"-"`
diff --git a/container/container_unix.go b/container/container_unix.go
index c38f750667..099073b83e 100644
--- a/container/container_unix.go
+++ b/container/container_unix.go
@@ -23,7 +23,10 @@ import (
 )
 
 // DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
-const DefaultSHMSize int64 = 67108864
+const (
+	DefaultSHMSize           int64 = 67108864
+	containerSecretMountPath       = "/run/secrets"
+)
 
 // Container holds the fields specific to unixen implementations.
 // See CommonContainer for standard fields common to all containers.
@@ -175,6 +178,10 @@ func (container *Container) NetworkMounts() []Mount {
 	return mounts
 }
 
+func (container *Container) SecretMountPath() string {
+	return filepath.Join(container.Root, "secrets")
+}
+
 // CopyImagePathContent copies files in destination to the volume.
 func (container *Container) CopyImagePathContent(v volume.Volume, destination string) error {
 	rootfs, err := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, destination), container.BaseFS)
@@ -260,6 +267,26 @@ func (container *Container) IpcMounts() []Mount {
 	return mounts
 }
 
+// SecretMounts returns the list of Secret mounts
+func (container *Container) SecretMounts() []Mount {
+	var mounts []Mount
+
+	if len(container.Secrets) > 0 {
+		mounts = append(mounts, Mount{
+			Source:      container.SecretMountPath(),
+			Destination: containerSecretMountPath,
+			Writable:    false,
+		})
+	}
+
+	return mounts
+}
+
+// UnmountSecrets unmounts the local tmpfs for secrets
+func (container *Container) UnmountSecrets() error {
+	return detachMounted(container.SecretMountPath())
+}
+
 // UpdateContainer updates configuration of a container.
 func (container *Container) UpdateContainer(hostConfig *containertypes.HostConfig) error {
 	container.Lock()
diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go
index 38749dd8b2..38143c6861 100644
--- a/daemon/cluster/convert/container.go
+++ b/daemon/cluster/convert/container.go
@@ -23,6 +23,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
 		User:     c.User,
 		Groups:   c.Groups,
 		TTY:      c.TTY,
+		Secrets:  secretReferencesFromGRPC(c.Secrets),
 	}
 
 	if c.DNSConfig != nil {
@@ -75,6 +76,47 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
 	return containerSpec
 }
 
+func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretReference {
+	refs := []*swarmapi.SecretReference{}
+	for _, s := range sr {
+		var mode swarmapi.SecretReference_Mode
+		switch s.Mode {
+		case types.SecretReferenceSystem:
+			mode = swarmapi.SecretReference_SYSTEM
+		default:
+			mode = swarmapi.SecretReference_FILE
+		}
+		refs = append(refs, &swarmapi.SecretReference{
+			SecretID:   s.SecretID,
+			SecretName: s.SecretName,
+			Target:     s.Target,
+			Mode:       mode,
+		})
+	}
+
+	return refs
+}
+func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
+	refs := []*types.SecretReference{}
+	for _, s := range sr {
+		var mode types.SecretReferenceMode
+		switch s.Mode {
+		case swarmapi.SecretReference_SYSTEM:
+			mode = types.SecretReferenceSystem
+		default:
+			mode = types.SecretReferenceFile
+		}
+		refs = append(refs, &types.SecretReference{
+			SecretID:   s.SecretID,
+			SecretName: s.SecretName,
+			Target:     s.Target,
+			Mode:       mode,
+		})
+	}
+
+	return refs
+}
+
 func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 	containerSpec := &swarmapi.ContainerSpec{
 		Image:    c.Image,
@@ -87,6 +129,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		User:     c.User,
 		Groups:   c.Groups,
 		TTY:      c.TTY,
+		Secrets:  secretReferencesToGRPC(c.Secrets),
 	}
 
 	if c.DNSConfig != nil {
diff --git a/daemon/cluster/convert/secret.go b/daemon/cluster/convert/secret.go
new file mode 100644
index 0000000000..e26fe84ce4
--- /dev/null
+++ b/daemon/cluster/convert/secret.go
@@ -0,0 +1,46 @@
+package convert
+
+import (
+	"github.com/Sirupsen/logrus"
+	swarmtypes "github.com/docker/docker/api/types/swarm"
+	swarmapi "github.com/docker/swarmkit/api"
+	"github.com/docker/swarmkit/protobuf/ptypes"
+)
+
+// SecretFromGRPC converts a grpc Secret to a Secret.
+func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
+	logrus.Debugf("%+v", s)
+	secret := swarmtypes.Secret{
+		ID:         s.ID,
+		Digest:     s.Digest,
+		SecretSize: s.SecretSize,
+	}
+
+	// Meta
+	secret.Version.Index = s.Meta.Version.Index
+	secret.CreatedAt, _ = ptypes.Timestamp(s.Meta.CreatedAt)
+	secret.UpdatedAt, _ = ptypes.Timestamp(s.Meta.UpdatedAt)
+
+	secret.Spec = &swarmtypes.SecretSpec{
+		Annotations: swarmtypes.Annotations{
+			Name:   s.Spec.Annotations.Name,
+			Labels: s.Spec.Annotations.Labels,
+		},
+		Data: s.Spec.Data,
+	}
+
+	return secret
+}
+
+// SecretSpecToGRPC converts Secret to a grpc Secret.
+func SecretSpecToGRPC(s swarmtypes.SecretSpec) (swarmapi.SecretSpec, error) {
+	spec := swarmapi.SecretSpec{
+		Annotations: swarmapi.Annotations{
+			Name:   s.Name,
+			Labels: s.Labels,
+		},
+		Data: s.Data,
+	}
+
+	return spec, nil
+}
diff --git a/daemon/cluster/executor/backend.go b/daemon/cluster/executor/backend.go
index fb88613c1d..76b9d8888f 100644
--- a/daemon/cluster/executor/backend.go
+++ b/daemon/cluster/executor/backend.go
@@ -34,6 +34,7 @@ type Backend interface {
 	ContainerWaitWithContext(ctx context.Context, name string) error
 	ContainerRm(name string, config *types.ContainerRmConfig) error
 	ContainerKill(name string, sig uint64) error
+	SetContainerSecrets(name string, secrets []*container.ContainerSecret) error
 	SystemInfo() (*types.Info, error)
 	VolumeCreate(name, driverName string, opts, labels map[string]string) (*types.Volume, error)
 	Containers(config *types.ContainerListOptions) ([]*types.Container, error)
diff --git a/daemon/cluster/executor/container/adapter.go b/daemon/cluster/executor/container/adapter.go
index 618f4b22b4..40a08401f4 100644
--- a/daemon/cluster/executor/container/adapter.go
+++ b/daemon/cluster/executor/container/adapter.go
@@ -17,6 +17,7 @@ import (
 	"github.com/docker/docker/api/types/versions"
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
 	"github.com/docker/libnetwork"
+	"github.com/docker/swarmkit/agent/exec"
 	"github.com/docker/swarmkit/api"
 	"github.com/docker/swarmkit/log"
 	"golang.org/x/net/context"
@@ -29,9 +30,10 @@ import (
 type containerAdapter struct {
 	backend   executorpkg.Backend
 	container *containerConfig
+	secrets   exec.SecretProvider
 }
 
-func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapter, error) {
+func newContainerAdapter(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*containerAdapter, error) {
 	ctnr, err := newContainerConfig(task)
 	if err != nil {
 		return nil, err
@@ -40,6 +42,7 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt
 	return &containerAdapter{
 		container: ctnr,
 		backend:   b,
+		secrets:   secrets,
 	}, nil
 }
 
@@ -215,6 +218,35 @@ func (c *containerAdapter) create(ctx context.Context) error {
 		}
 	}
 
+	secrets := []*containertypes.ContainerSecret{}
+	for _, s := range c.container.task.Spec.GetContainer().Secrets {
+		sec := c.secrets.Get(s.SecretID)
+		if sec == nil {
+			logrus.Warnf("unable to get secret %s from provider", s.SecretID)
+			continue
+		}
+
+		name := sec.Spec.Annotations.Name
+		target := s.Target
+		if target == "" {
+			target = name
+		}
+		secrets = append(secrets, &containertypes.ContainerSecret{
+			Name:   name,
+			Target: target,
+			Data:   sec.Spec.Data,
+			// TODO (ehazlett): enable configurable uid, gid, mode
+			Uid:  0,
+			Gid:  0,
+			Mode: 0444,
+		})
+	}
+
+	// configure secrets
+	if err := c.backend.SetContainerSecrets(cr.ID, secrets); err != nil {
+		return err
+	}
+
 	if err := c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig()); err != nil {
 		return err
 	}
diff --git a/daemon/cluster/executor/container/attachment.go b/daemon/cluster/executor/container/attachment.go
index f3b738f70b..a897268b26 100644
--- a/daemon/cluster/executor/container/attachment.go
+++ b/daemon/cluster/executor/container/attachment.go
@@ -4,6 +4,7 @@ import (
 	executorpkg "github.com/docker/docker/daemon/cluster/executor"
 	"github.com/docker/swarmkit/api"
 	"golang.org/x/net/context"
+	"src/github.com/docker/swarmkit/agent/exec"
 )
 
 // networkAttacherController implements agent.Controller against docker's API.
@@ -19,8 +20,8 @@ type networkAttacherController struct {
 	closed  chan struct{}
 }
 
-func newNetworkAttacherController(b executorpkg.Backend, task *api.Task) (*networkAttacherController, error) {
-	adapter, err := newContainerAdapter(b, task)
+func newNetworkAttacherController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*networkAttacherController, error) {
+	adapter, err := newContainerAdapter(b, task, secrets)
 	if err != nil {
 		return nil, err
 	}
diff --git a/daemon/cluster/executor/container/controller.go b/daemon/cluster/executor/container/controller.go
index 0185e415b5..d3d80075a4 100644
--- a/daemon/cluster/executor/container/controller.go
+++ b/daemon/cluster/executor/container/controller.go
@@ -33,8 +33,8 @@ type controller struct {
 var _ exec.Controller = &controller{}
 
 // NewController returns a docker exec runner for the provided task.
-func newController(b executorpkg.Backend, task *api.Task) (*controller, error) {
-	adapter, err := newContainerAdapter(b, task)
+func newController(b executorpkg.Backend, task *api.Task, secrets exec.SecretProvider) (*controller, error) {
+	adapter, err := newContainerAdapter(b, task, secrets)
 	if err != nil {
 		return nil, err
 	}
diff --git a/daemon/cluster/executor/container/executor.go b/daemon/cluster/executor/container/executor.go
index 844821b83e..fdd270006d 100644
--- a/daemon/cluster/executor/container/executor.go
+++ b/daemon/cluster/executor/container/executor.go
@@ -18,6 +18,10 @@ type executor struct {
 	backend executorpkg.Backend
 }
 
+type secretProvider interface {
+	Get(secretID string) *api.Secret
+}
+
 // NewExecutor returns an executor from the docker client.
 func NewExecutor(b executorpkg.Backend) exec.Executor {
 	return &executor{
@@ -120,12 +124,12 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
 }
 
 // Controller returns a docker container runner.
-func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
+func (e *executor) Controller(t *api.Task, secrets exec.SecretProvider) (exec.Controller, error) {
 	if t.Spec.GetAttachment() != nil {
-		return newNetworkAttacherController(e.backend, t)
+		return newNetworkAttacherController(e.backend, t, secrets)
 	}
 
-	ctlr, err := newController(e.backend, t)
+	ctlr, err := newController(e.backend, t, secrets)
 	if err != nil {
 		return nil, err
 	}
diff --git a/daemon/cluster/executor/container/health_test.go b/daemon/cluster/executor/container/health_test.go
index 16a1e0c096..99cf7502af 100644
--- a/daemon/cluster/executor/container/health_test.go
+++ b/daemon/cluster/executor/container/health_test.go
@@ -54,7 +54,7 @@ func TestHealthStates(t *testing.T) {
 		EventsService: e,
 	}
 
-	controller, err := newController(daemon, task)
+	controller, err := newController(daemon, task, nil)
 	if err != nil {
 		t.Fatalf("create controller fail %v", err)
 	}
diff --git a/daemon/cluster/executor/container/validate_test.go b/daemon/cluster/executor/container/validate_test.go
index d911c1ebec..5f202d5859 100644
--- a/daemon/cluster/executor/container/validate_test.go
+++ b/daemon/cluster/executor/container/validate_test.go
@@ -26,7 +26,7 @@ func newTestControllerWithMount(m api.Mount) (*controller, error) {
 				},
 			},
 		},
-	})
+	}, nil)
 }
 
 func TestControllerValidateMountBind(t *testing.T) {
diff --git a/daemon/cluster/filters.go b/daemon/cluster/filters.go
index a1d800e56c..88668edaac 100644
--- a/daemon/cluster/filters.go
+++ b/daemon/cluster/filters.go
@@ -96,3 +96,21 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e
 
 	return f, nil
 }
+
+func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Filters, error) {
+	accepted := map[string]bool{
+		"names": true,
+		"name":  true,
+		"id":    true,
+		"label": true,
+	}
+	if err := filter.Validate(accepted); err != nil {
+		return nil, err
+	}
+	return &swarmapi.ListSecretsRequest_Filters{
+		Names:        filter.Get("names"),
+		NamePrefixes: filter.Get("name"),
+		IDPrefixes:   filter.Get("id"),
+		Labels:       runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
+	}, nil
+}
diff --git a/daemon/cluster/secrets.go b/daemon/cluster/secrets.go
new file mode 100644
index 0000000000..305f2afab5
--- /dev/null
+++ b/daemon/cluster/secrets.go
@@ -0,0 +1,131 @@
+package cluster
+
+import (
+	apitypes "github.com/docker/docker/api/types"
+	types "github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/daemon/cluster/convert"
+	swarmapi "github.com/docker/swarmkit/api"
+)
+
+// GetSecret returns a secret from a managed swarm cluster
+func (c *Cluster) GetSecret(id string) (types.Secret, error) {
+	ctx, cancel := c.getRequestContext()
+	defer cancel()
+
+	r, err := c.node.client.GetSecret(ctx, &swarmapi.GetSecretRequest{SecretID: id})
+	if err != nil {
+		return types.Secret{}, err
+	}
+
+	return convert.SecretFromGRPC(r.Secret), nil
+}
+
+// GetSecrets returns all secrets of a managed swarm cluster.
+func (c *Cluster) GetSecrets(options apitypes.SecretListOptions) ([]types.Secret, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return nil, c.errNoManager()
+	}
+
+	filters, err := newListSecretsFilters(options.Filter)
+	if err != nil {
+		return nil, err
+	}
+	ctx, cancel := c.getRequestContext()
+	defer cancel()
+
+	r, err := c.node.client.ListSecrets(ctx,
+		&swarmapi.ListSecretsRequest{Filters: filters})
+	if err != nil {
+		return nil, err
+	}
+
+	secrets := []types.Secret{}
+
+	for _, secret := range r.Secrets {
+		secrets = append(secrets, convert.SecretFromGRPC(secret))
+	}
+
+	return secrets, nil
+}
+
+// CreateSecret creates a new secret in a managed swarm cluster.
+func (c *Cluster) CreateSecret(s types.SecretSpec) (string, error) {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return "", c.errNoManager()
+	}
+
+	ctx, cancel := c.getRequestContext()
+	defer cancel()
+
+	secretSpec, err := convert.SecretSpecToGRPC(s)
+	if err != nil {
+		return "", err
+	}
+
+	r, err := c.node.client.CreateSecret(ctx,
+		&swarmapi.CreateSecretRequest{Spec: &secretSpec})
+	if err != nil {
+		return "", err
+	}
+
+	return r.Secret.ID, nil
+}
+
+// RemoveSecret removes a secret from a managed swarm cluster.
+func (c *Cluster) RemoveSecret(id string) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return c.errNoManager()
+	}
+
+	ctx, cancel := c.getRequestContext()
+	defer cancel()
+
+	req := &swarmapi.RemoveSecretRequest{
+		SecretID: id,
+	}
+
+	if _, err := c.node.client.RemoveSecret(ctx, req); err != nil {
+		return err
+	}
+	return nil
+}
+
+// UpdateSecret updates a secret in a managed swarm cluster.
+func (c *Cluster) UpdateSecret(id string, version uint64, spec types.SecretSpec) error {
+	c.RLock()
+	defer c.RUnlock()
+
+	if !c.isActiveManager() {
+		return c.errNoManager()
+	}
+
+	ctx, cancel := c.getRequestContext()
+	defer cancel()
+
+	secretSpec, err := convert.SecretSpecToGRPC(spec)
+	if err != nil {
+		return err
+	}
+
+	if _, err := c.client.UpdateSecret(ctx,
+		&swarmapi.UpdateSecretRequest{
+			SecretID: id,
+			SecretVersion: &swarmapi.Version{
+				Index: version,
+			},
+			Spec: &secretSpec,
+		}); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/daemon/container_operations_unix.go b/daemon/container_operations_unix.go
index 66b11bb288..a3031ba3e6 100644
--- a/daemon/container_operations_unix.go
+++ b/daemon/container_operations_unix.go
@@ -4,6 +4,7 @@ package daemon
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -18,6 +19,7 @@ import (
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/docker/runconfig"
+	"github.com/docker/engine-api/types/mount"
 	"github.com/docker/libnetwork"
 	"github.com/opencontainers/runc/libcontainer/configs"
 	"github.com/opencontainers/runc/libcontainer/devices"
@@ -139,6 +141,43 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
 
 	return nil
 }
+
+func (daemon *Daemon) setupSecretDir(c *container.Container) error {
+	localMountPath := c.SecretMountPath()
+	logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
+
+	// create tmpfs
+	if err := os.MkdirAll(localMountPath, 0700); err != nil {
+		return fmt.Errorf("error creating secret local mount path: %s", err)
+	}
+	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev"); err != nil {
+		return fmt.Errorf("unable to setup secret mount: %s", err)
+	}
+
+	for _, s := range c.Secrets {
+		fPath := filepath.Join(localMountPath, s.Target)
+		if err := os.MkdirAll(filepath.Dir(fPath), 0700); err != nil {
+			return fmt.Errorf("error creating secret mount path: %s", err)
+		}
+
+		logrus.Debugf("injecting secret: name=%s path=%s", s.Name, fPath)
+		if err := ioutil.WriteFile(fPath, s.Data, s.Mode); err != nil {
+			return fmt.Errorf("error injecting secret: %s", err)
+		}
+
+		if err := os.Chown(fPath, s.Uid, s.Gid); err != nil {
+			return fmt.Errorf("error setting ownership for secret: %s", err)
+		}
+	}
+
+	// remount secrets ro
+	if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro"); err != nil {
+		return fmt.Errorf("unable to remount secret dir as readonly: %s", err)
+	}
+
+	return nil
+}
+
 func killProcessDirectly(container *container.Container) error {
 	if _, err := container.WaitStop(10 * time.Second); err != nil {
 		// Ensure that we don't kill ourselves
diff --git a/daemon/daemon.go b/daemon/daemon.go
index 8bbc8f42d4..c3efbac2bd 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -854,6 +854,7 @@ func (daemon *Daemon) Unmount(container *container.Container) error {
 		logrus.Errorf("Error unmounting container %s: %s", container.ID, err)
 		return err
 	}
+
 	return nil
 }
 
diff --git a/daemon/oci_linux.go b/daemon/oci_linux.go
index 5cac76843d..d74ddd053b 100644
--- a/daemon/oci_linux.go
+++ b/daemon/oci_linux.go
@@ -702,16 +702,23 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
 		return nil, err
 	}
 
+	if err := daemon.setupSecretDir(c); err != nil {
+		return nil, err
+	}
+
 	ms, err := daemon.setupMounts(c)
 	if err != nil {
 		return nil, err
 	}
 	ms = append(ms, c.IpcMounts()...)
+
 	tmpfsMounts, err := c.TmpfsMounts()
 	if err != nil {
 		return nil, err
 	}
 	ms = append(ms, tmpfsMounts...)
+
+	ms = append(ms, c.SecretMounts()...)
 	sort.Sort(mounts(ms))
 	if err := setMounts(daemon, &s, c, ms); err != nil {
 		return nil, fmt.Errorf("linux mounts: %v", err)
diff --git a/daemon/secrets.go b/daemon/secrets.go
new file mode 100644
index 0000000000..342b33c2a3
--- /dev/null
+++ b/daemon/secrets.go
@@ -0,0 +1,22 @@
+package daemon
+
+import (
+	"github.com/Sirupsen/logrus"
+	containertypes "github.com/docker/docker/api/types/container"
+)
+
+func (daemon *Daemon) SetContainerSecrets(name string, secrets []*containertypes.ContainerSecret) error {
+	if !secretsSupported() {
+		logrus.Warn("secrets are not supported on this platform")
+		return nil
+	}
+
+	c, err := daemon.GetContainer(name)
+	if err != nil {
+		return err
+	}
+
+	c.Secrets = secrets
+
+	return nil
+}
diff --git a/daemon/secrets_linux.go b/daemon/secrets_linux.go
new file mode 100644
index 0000000000..fca4e12598
--- /dev/null
+++ b/daemon/secrets_linux.go
@@ -0,0 +1,7 @@
+// +build linux
+
+package daemon
+
+func secretsSupported() bool {
+	return true
+}
diff --git a/daemon/secrets_unsupported.go b/daemon/secrets_unsupported.go
new file mode 100644
index 0000000000..d6f36fda1e
--- /dev/null
+++ b/daemon/secrets_unsupported.go
@@ -0,0 +1,7 @@
+// +build !linux
+
+package daemon
+
+func secretsSupported() bool {
+	return false
+}
diff --git a/daemon/start.go b/daemon/start.go
index c642ce22a8..af08ccdf39 100644
--- a/daemon/start.go
+++ b/daemon/start.go
@@ -212,6 +212,10 @@ func (daemon *Daemon) Cleanup(container *container.Container) {
 		}
 	}
 
+	if err := container.UnmountSecrets(); err != nil {
+		logrus.Warnf("%s cleanup: failed to unmount secrets: %s", container.ID, err)
+	}
+
 	for _, eConfig := range container.ExecCommands.Commands() {
 		daemon.unregisterExecCommand(container, eConfig)
 	}