Browse code

Revise swarm init/update flags, add unlocking capability

- Neither swarm init or swarm update should take an unlock key
- Add an autolock flag to turn on autolock
- Make the necessary docker api changes
- Add SwarmGetUnlockKey API call and use it when turning on autolock
- Add swarm unlock-key subcommand

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2016/10/28 10:50:49
Showing 16 changed files
... ...
@@ -64,7 +64,7 @@ func maskSecretKeys(inp interface{}) {
64 64
 	if form, ok := inp.(map[string]interface{}); ok {
65 65
 	loop0:
66 66
 		for k, v := range form {
67
-			for _, m := range []string{"password", "secret", "jointoken", "lockkey"} {
67
+			for _, m := range []string{"password", "secret", "jointoken", "unlockkey"} {
68 68
 				if strings.EqualFold(m, k) {
69 69
 					form[k] = "*****"
70 70
 					continue loop0
... ...
@@ -12,6 +12,7 @@ type Backend interface {
12 12
 	Leave(force bool) error
13 13
 	Inspect() (types.Swarm, error)
14 14
 	Update(uint64, types.Spec, types.UpdateFlags) error
15
+	GetUnlockKey() (string, error)
15 16
 	UnlockSwarm(req types.UnlockRequest) error
16 17
 	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
17 18
 	GetService(string) (types.Service, error)
... ...
@@ -28,6 +28,7 @@ func (sr *swarmRouter) initRoutes() {
28 28
 		router.NewPostRoute("/swarm/join", sr.joinCluster),
29 29
 		router.NewPostRoute("/swarm/leave", sr.leaveCluster),
30 30
 		router.NewGetRoute("/swarm", sr.inspectCluster),
31
+		router.NewGetRoute("/swarm/unlockkey", sr.getUnlockKey),
31 32
 		router.NewPostRoute("/swarm/update", sr.updateCluster),
32 33
 		router.NewPostRoute("/swarm/unlock", sr.unlockCluster),
33 34
 		router.NewGetRoute("/services", sr.getServices),
... ...
@@ -101,12 +101,24 @@ func (sr *swarmRouter) unlockCluster(ctx context.Context, w http.ResponseWriter,
101 101
 	}
102 102
 
103 103
 	if err := sr.backend.UnlockSwarm(req); err != nil {
104
-		logrus.Errorf("Error unlocking swarm: %+v", err)
104
+		logrus.Errorf("Error unlocking swarm: %v", err)
105 105
 		return err
106 106
 	}
107 107
 	return nil
108 108
 }
109 109
 
110
+func (sr *swarmRouter) getUnlockKey(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
111
+	unlockKey, err := sr.backend.GetUnlockKey()
112
+	if err != nil {
113
+		logrus.WithError(err).Errorf("Error retrieving swarm unlock key")
114
+		return err
115
+	}
116
+
117
+	return httputils.WriteJSON(w, http.StatusOK, &basictypes.SwarmUnlockKeyResponse{
118
+		UnlockKey: unlockKey,
119
+	})
120
+}
121
+
110 122
 func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
111 123
 	if err := httputils.ParseForm(r); err != nil {
112 124
 		return err
... ...
@@ -349,3 +349,10 @@ type SecretRequestOption struct {
349 349
 	GID    string
350 350
 	Mode   os.FileMode
351 351
 }
352
+
353
+// SwarmUnlockKeyResponse contains the response for Remote API:
354
+// GET /swarm/unlockkey
355
+type SwarmUnlockKeyResponse struct {
356
+	// UnlockKey is the unlock key in ASCII-armored format.
357
+	UnlockKey string
358
+}
... ...
@@ -28,11 +28,12 @@ type JoinTokens struct {
28 28
 type Spec struct {
29 29
 	Annotations
30 30
 
31
-	Orchestration OrchestrationConfig `json:",omitempty"`
32
-	Raft          RaftConfig          `json:",omitempty"`
33
-	Dispatcher    DispatcherConfig    `json:",omitempty"`
34
-	CAConfig      CAConfig            `json:",omitempty"`
35
-	TaskDefaults  TaskDefaults        `json:",omitempty"`
31
+	Orchestration    OrchestrationConfig `json:",omitempty"`
32
+	Raft             RaftConfig          `json:",omitempty"`
33
+	Dispatcher       DispatcherConfig    `json:",omitempty"`
34
+	CAConfig         CAConfig            `json:",omitempty"`
35
+	TaskDefaults     TaskDefaults        `json:",omitempty"`
36
+	EncryptionConfig EncryptionConfig    `json:",omitempty"`
36 37
 }
37 38
 
38 39
 // OrchestrationConfig represents orchestration configuration.
... ...
@@ -53,6 +54,14 @@ type TaskDefaults struct {
53 53
 	LogDriver *Driver `json:",omitempty"`
54 54
 }
55 55
 
56
+// EncryptionConfig controls at-rest encryption of data and keys.
57
+type EncryptionConfig struct {
58
+	// AutoLockManagers specifies whether or not managers TLS keys and raft data
59
+	// should be encrypted at rest in such a way that they must be unlocked
60
+	// before the manager node starts up again.
61
+	AutoLockManagers bool
62
+}
63
+
56 64
 // RaftConfig represents raft configuration.
57 65
 type RaftConfig struct {
58 66
 	// SnapshotInterval is the number of log entries between snapshots.
... ...
@@ -121,11 +130,11 @@ type ExternalCA struct {
121 121
 
122 122
 // InitRequest is the request used to init a swarm.
123 123
 type InitRequest struct {
124
-	ListenAddr      string
125
-	AdvertiseAddr   string
126
-	ForceNewCluster bool
127
-	Spec            Spec
128
-	LockKey         string
124
+	ListenAddr       string
125
+	AdvertiseAddr    string
126
+	ForceNewCluster  bool
127
+	Spec             Spec
128
+	AutoLockManagers bool
129 129
 }
130 130
 
131 131
 // JoinRequest is the request used to join a swarm.
... ...
@@ -138,7 +147,8 @@ type JoinRequest struct {
138 138
 
139 139
 // UnlockRequest is the request used to unlock a swarm.
140 140
 type UnlockRequest struct {
141
-	LockKey string
141
+	// UnlockKey is the unlock key in ASCII-armored format.
142
+	UnlockKey string
142 143
 }
143 144
 
144 145
 // LocalNodeState represents the state of the local node.
... ...
@@ -181,6 +191,7 @@ type Peer struct {
181 181
 
182 182
 // UpdateFlags contains flags for SwarmUpdate.
183 183
 type UpdateFlags struct {
184
-	RotateWorkerToken  bool
185
-	RotateManagerToken bool
184
+	RotateWorkerToken      bool
185
+	RotateManagerToken     bool
186
+	RotateManagerUnlockKey bool
186 187
 }
... ...
@@ -22,6 +22,7 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command {
22 22
 		newInitCommand(dockerCli),
23 23
 		newJoinCommand(dockerCli),
24 24
 		newJoinTokenCommand(dockerCli),
25
+		newUnlockKeyCommand(dockerCli),
25 26
 		newUpdateCommand(dockerCli),
26 27
 		newLeaveCommand(dockerCli),
27 28
 		newUnlockCommand(dockerCli),
... ...
@@ -1,20 +1,15 @@
1 1
 package swarm
2 2
 
3 3
 import (
4
-	"bufio"
5
-	"crypto/rand"
6
-	"errors"
7 4
 	"fmt"
8
-	"io"
9
-	"math/big"
10 5
 	"strings"
11 6
 
12
-	"golang.org/x/crypto/ssh/terminal"
13 7
 	"golang.org/x/net/context"
14 8
 
15 9
 	"github.com/docker/docker/api/types/swarm"
16 10
 	"github.com/docker/docker/cli"
17 11
 	"github.com/docker/docker/cli/command"
12
+	"github.com/pkg/errors"
18 13
 	"github.com/spf13/cobra"
19 14
 	"github.com/spf13/pflag"
20 15
 )
... ...
@@ -25,7 +20,6 @@ type initOptions struct {
25 25
 	// Not a NodeAddrOption because it has no default port.
26 26
 	advertiseAddr   string
27 27
 	forceNewCluster bool
28
-	lockKey         bool
29 28
 }
30 29
 
31 30
 func newInitCommand(dockerCli *command.DockerCli) *cobra.Command {
... ...
@@ -45,7 +39,6 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command {
45 45
 	flags := cmd.Flags()
46 46
 	flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: <ip|interface>[:port])")
47 47
 	flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: <ip|interface>[:port])")
48
-	flags.BoolVar(&opts.lockKey, flagLockKey, false, "Encrypt swarm with optionally provided key from stdin")
49 48
 	flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state")
50 49
 	addSwarmFlags(flags, &opts.swarmOptions)
51 50
 	return cmd
... ...
@@ -55,31 +48,12 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption
55 55
 	client := dockerCli.Client()
56 56
 	ctx := context.Background()
57 57
 
58
-	var lockKey string
59
-	if opts.lockKey {
60
-		var err error
61
-		lockKey, err = readKey(dockerCli.In(), "Please enter key for encrypting swarm(leave empty to generate): ")
62
-		if err != nil {
63
-			return err
64
-		}
65
-		if len(lockKey) == 0 {
66
-			randBytes := make([]byte, 16)
67
-			if _, err := rand.Read(randBytes[:]); err != nil {
68
-				panic(fmt.Errorf("failed to general random lock key: %v", err))
69
-			}
70
-
71
-			var n big.Int
72
-			n.SetBytes(randBytes[:])
73
-			lockKey = n.Text(36)
74
-		}
75
-	}
76
-
77 58
 	req := swarm.InitRequest{
78
-		ListenAddr:      opts.listenAddr.String(),
79
-		AdvertiseAddr:   opts.advertiseAddr,
80
-		ForceNewCluster: opts.forceNewCluster,
81
-		Spec:            opts.swarmOptions.ToSpec(flags),
82
-		LockKey:         lockKey,
59
+		ListenAddr:       opts.listenAddr.String(),
60
+		AdvertiseAddr:    opts.advertiseAddr,
61
+		ForceNewCluster:  opts.forceNewCluster,
62
+		Spec:             opts.swarmOptions.ToSpec(flags),
63
+		AutoLockManagers: opts.swarmOptions.autolock,
83 64
 	}
84 65
 
85 66
 	nodeID, err := client.SwarmInit(ctx, req)
... ...
@@ -92,29 +66,19 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption
92 92
 
93 93
 	fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID)
94 94
 
95
-	if len(lockKey) > 0 {
96
-		fmt.Fprintf(dockerCli.Out(), "Swarm is encrypted. When a node is restarted it needs to be unlocked by running command:\n\n    echo '%s' | docker swarm unlock\n\n", lockKey)
97
-	}
98
-
99 95
 	if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil {
100 96
 		return err
101 97
 	}
102 98
 
103 99
 	fmt.Fprint(dockerCli.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n\n")
104
-	return nil
105
-}
106 100
 
107
-func readKey(in *command.InStream, prompt string) (string, error) {
108
-	if in.IsTerminal() {
109
-		fmt.Print(prompt)
110
-		dt, err := terminal.ReadPassword(int(in.FD()))
111
-		fmt.Println()
112
-		return string(dt), err
113
-	} else {
114
-		key, err := bufio.NewReader(in).ReadString('\n')
115
-		if err == io.EOF {
116
-			err = nil
101
+	if req.AutoLockManagers {
102
+		unlockKeyResp, err := client.SwarmGetUnlockKey(ctx)
103
+		if err != nil {
104
+			return errors.Wrap(err, "could not fetch unlock key")
117 105
 		}
118
-		return strings.TrimSpace(key), err
106
+		printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey)
119 107
 	}
108
+
109
+	return nil
120 110
 }
... ...
@@ -27,6 +27,7 @@ const (
27 27
 	flagMaxSnapshots        = "max-snapshots"
28 28
 	flagSnapshotInterval    = "snapshot-interval"
29 29
 	flagLockKey             = "lock-key"
30
+	flagAutolock            = "autolock"
30 31
 )
31 32
 
32 33
 type swarmOptions struct {
... ...
@@ -36,6 +37,7 @@ type swarmOptions struct {
36 36
 	externalCA          ExternalCAOption
37 37
 	maxSnapshots        uint64
38 38
 	snapshotInterval    uint64
39
+	autolock            bool
39 40
 }
40 41
 
41 42
 // NodeAddrOption is a pflag.Value for listening addresses
... ...
@@ -174,6 +176,7 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
174 174
 	flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints")
175 175
 	flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain")
176 176
 	flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots")
177
+	flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable or disable manager autolocking (requiring an unlock key to start a stopped manager)")
177 178
 }
178 179
 
179 180
 func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) {
... ...
@@ -195,6 +198,9 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet)
195 195
 	if flags.Changed(flagSnapshotInterval) {
196 196
 		spec.Raft.SnapshotInterval = opts.snapshotInterval
197 197
 	}
198
+	if flags.Changed(flagAutolock) {
199
+		spec.EncryptionConfig.AutoLockManagers = opts.autolock
200
+	}
198 201
 }
199 202
 
200 203
 func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec {
... ...
@@ -1,9 +1,14 @@
1 1
 package swarm
2 2
 
3 3
 import (
4
+	"bufio"
4 5
 	"context"
6
+	"fmt"
7
+	"io"
8
+	"strings"
5 9
 
6 10
 	"github.com/spf13/cobra"
11
+	"golang.org/x/crypto/ssh/terminal"
7 12
 
8 13
 	"github.com/docker/docker/api/types/swarm"
9 14
 	"github.com/docker/docker/cli"
... ...
@@ -24,7 +29,7 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command {
24 24
 				return err
25 25
 			}
26 26
 			req := swarm.UnlockRequest{
27
-				LockKey: string(key),
27
+				UnlockKey: key,
28 28
 			}
29 29
 
30 30
 			return client.SwarmUnlock(ctx, req)
... ...
@@ -33,3 +38,17 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command {
33 33
 
34 34
 	return cmd
35 35
 }
36
+
37
+func readKey(in *command.InStream, prompt string) (string, error) {
38
+	if in.IsTerminal() {
39
+		fmt.Print(prompt)
40
+		dt, err := terminal.ReadPassword(int(in.FD()))
41
+		fmt.Println()
42
+		return string(dt), err
43
+	}
44
+	key, err := bufio.NewReader(in).ReadString('\n')
45
+	if err == io.EOF {
46
+		err = nil
47
+	}
48
+	return strings.TrimSpace(key), err
49
+}
36 50
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+package swarm
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/spf13/cobra"
6
+
7
+	"github.com/docker/docker/cli"
8
+	"github.com/docker/docker/cli/command"
9
+	"github.com/pkg/errors"
10
+	"golang.org/x/net/context"
11
+)
12
+
13
+func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command {
14
+	var rotate, quiet bool
15
+
16
+	cmd := &cobra.Command{
17
+		Use:   "unlock-key [OPTIONS]",
18
+		Short: "Manage the unlock key",
19
+		Args:  cli.NoArgs,
20
+		RunE: func(cmd *cobra.Command, args []string) error {
21
+			client := dockerCli.Client()
22
+			ctx := context.Background()
23
+
24
+			if rotate {
25
+				// FIXME(aaronl)
26
+			}
27
+
28
+			unlockKeyResp, err := client.SwarmGetUnlockKey(ctx)
29
+			if err != nil {
30
+				return errors.Wrap(err, "could not fetch unlock key")
31
+			}
32
+
33
+			if quiet {
34
+				fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey)
35
+			} else {
36
+				printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey)
37
+			}
38
+			return nil
39
+		},
40
+	}
41
+
42
+	flags := cmd.Flags()
43
+	flags.BoolVar(&rotate, flagRotate, false, "Rotate unlock key")
44
+	flags.BoolVarP(&quiet, flagQuiet, "q", false, "Only display token")
45
+
46
+	return cmd
47
+}
48
+
49
+func printUnlockCommand(ctx context.Context, dockerCli *command.DockerCli, unlockKey string) {
50
+	if len(unlockKey) == 0 {
51
+		return
52
+	}
53
+
54
+	fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n    %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey)
55
+	return
56
+}
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"github.com/docker/docker/api/types/swarm"
9 9
 	"github.com/docker/docker/cli"
10 10
 	"github.com/docker/docker/cli/command"
11
+	"github.com/pkg/errors"
11 12
 	"github.com/spf13/cobra"
12 13
 	"github.com/spf13/pflag"
13 14
 )
... ...
@@ -39,8 +40,12 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt
39 39
 		return err
40 40
 	}
41 41
 
42
+	prevAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers
43
+
42 44
 	opts.mergeSwarmSpec(&swarm.Spec, flags)
43 45
 
46
+	curAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers
47
+
44 48
 	err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, updateFlags)
45 49
 	if err != nil {
46 50
 		return err
... ...
@@ -48,5 +53,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt
48 48
 
49 49
 	fmt.Fprintln(dockerCli.Out(), "Swarm updated.")
50 50
 
51
+	if curAutoLock && !prevAutoLock {
52
+		unlockKeyResp, err := client.SwarmGetUnlockKey(ctx)
53
+		if err != nil {
54
+			return errors.Wrap(err, "could not fetch unlock key")
55
+		}
56
+		printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey)
57
+	}
58
+
51 59
 	return nil
52 60
 }
... ...
@@ -119,6 +119,7 @@ type ServiceAPIClient interface {
119 119
 type SwarmAPIClient interface {
120 120
 	SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error)
121 121
 	SwarmJoin(ctx context.Context, req swarm.JoinRequest) error
122
+	SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error)
122 123
 	SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error
123 124
 	SwarmLeave(ctx context.Context, force bool) error
124 125
 	SwarmInspect(ctx context.Context) (swarm.Swarm, error)
125 126
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+package client
1
+
2
+import (
3
+	"encoding/json"
4
+
5
+	"github.com/docker/docker/api/types"
6
+	"golang.org/x/net/context"
7
+)
8
+
9
+// SwarmGetUnlockKey retrieves the swarm's unlock key.
10
+func (cli *Client) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) {
11
+	serverResp, err := cli.get(ctx, "/swarm/unlockkey", nil, nil)
12
+	if err != nil {
13
+		return types.SwarmUnlockKeyResponse{}, err
14
+	}
15
+
16
+	var response types.SwarmUnlockKeyResponse
17
+	err = json.NewDecoder(serverResp.body).Decode(&response)
18
+	ensureReaderClosed(serverResp)
19
+	return response, err
20
+}
... ...
@@ -1,7 +1,6 @@
1 1
 package cluster
2 2
 
3 3
 import (
4
-	"crypto/x509"
5 4
 	"encoding/json"
6 5
 	"fmt"
7 6
 	"io/ioutil"
... ...
@@ -27,6 +26,7 @@ import (
27 27
 	"github.com/docker/docker/pkg/signal"
28 28
 	"github.com/docker/docker/runconfig"
29 29
 	swarmapi "github.com/docker/swarmkit/api"
30
+	"github.com/docker/swarmkit/manager/encryption"
30 31
 	swarmnode "github.com/docker/swarmkit/node"
31 32
 	"github.com/pkg/errors"
32 33
 	"golang.org/x/net/context"
... ...
@@ -140,6 +140,7 @@ type nodeStartConfig struct {
140 140
 	forceNewCluster bool
141 141
 	joinToken       string
142 142
 	lockKey         []byte
143
+	autolock        bool
143 144
 }
144 145
 
145 146
 // New creates a new Cluster instance using provided config.
... ...
@@ -172,12 +173,6 @@ func New(config Config) (*Cluster, error) {
172 172
 
173 173
 	n, err := c.startNewNode(*nodeConfig)
174 174
 	if err != nil {
175
-		if errors.Cause(err) == ErrSwarmLocked {
176
-			logrus.Warnf("swarm component could not be started: %v", err)
177
-			c.locked = true
178
-			c.lastNodeConfig = nodeConfig
179
-			return c, nil
180
-		}
181 175
 		return nil, err
182 176
 	}
183 177
 
... ...
@@ -186,6 +181,10 @@ func New(config Config) (*Cluster, error) {
186 186
 		logrus.Error("swarm component could not be started before timeout was reached")
187 187
 	case <-n.Ready():
188 188
 	case <-n.done:
189
+		if errors.Cause(c.err) == ErrSwarmLocked {
190
+			return c, nil
191
+		}
192
+
189 193
 		return nil, fmt.Errorf("swarm component could not be started: %v", c.err)
190 194
 	}
191 195
 	go c.reconnectOnFailure(n)
... ...
@@ -314,15 +313,10 @@ func (c *Cluster) startNewNode(conf nodeStartConfig) (*node, error) {
314 314
 		HeartbeatTick:      1,
315 315
 		ElectionTick:       3,
316 316
 		UnlockKey:          conf.lockKey,
317
+		AutoLockManagers:   conf.autolock,
317 318
 	})
318 319
 
319 320
 	if err != nil {
320
-		err = detectLockedError(err)
321
-		if errors.Cause(err) == ErrSwarmLocked {
322
-			c.locked = true
323
-			confClone := conf
324
-			c.lastNodeConfig = &confClone
325
-		}
326 321
 		return nil, err
327 322
 	}
328 323
 	ctx := context.Background()
... ...
@@ -341,13 +335,18 @@ func (c *Cluster) startNewNode(conf nodeStartConfig) (*node, error) {
341 341
 
342 342
 	c.config.Backend.SetClusterProvider(c)
343 343
 	go func() {
344
-		err := n.Err(ctx)
344
+		err := detectLockedError(n.Err(ctx))
345 345
 		if err != nil {
346 346
 			logrus.Errorf("cluster exited with error: %v", err)
347 347
 		}
348 348
 		c.Lock()
349 349
 		c.node = nil
350 350
 		c.err = err
351
+		if errors.Cause(err) == ErrSwarmLocked {
352
+			c.locked = true
353
+			confClone := conf
354
+			c.lastNodeConfig = &confClone
355
+		}
351 356
 		c.Unlock()
352 357
 		close(node.done)
353 358
 	}()
... ...
@@ -443,18 +442,13 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
443 443
 		localAddr = advertiseIP.String()
444 444
 	}
445 445
 
446
-	var key []byte
447
-	if len(req.LockKey) > 0 {
448
-		key = []byte(req.LockKey)
449
-	}
450
-
451 446
 	// todo: check current state existing
452 447
 	n, err := c.startNewNode(nodeStartConfig{
453 448
 		forceNewCluster: req.ForceNewCluster,
449
+		autolock:        req.AutoLockManagers,
454 450
 		LocalAddr:       localAddr,
455 451
 		ListenAddr:      net.JoinHostPort(listenHost, listenPort),
456 452
 		AdvertiseAddr:   net.JoinHostPort(advertiseHost, advertisePort),
457
-		lockKey:         key,
458 453
 	})
459 454
 	if err != nil {
460 455
 		c.Unlock()
... ...
@@ -569,8 +563,9 @@ func (c *Cluster) GetUnlockKey() (string, error) {
569 569
 
570 570
 // UnlockSwarm provides a key to decrypt data that is encrypted at rest.
571 571
 func (c *Cluster) UnlockSwarm(req types.UnlockRequest) error {
572
-	if len(req.LockKey) == 0 {
573
-		return errors.New("unlock key can't be empty")
572
+	key, err := encryption.ParseHumanReadableKey(req.UnlockKey)
573
+	if err != nil {
574
+		return err
574 575
 	}
575 576
 
576 577
 	c.Lock()
... ...
@@ -580,7 +575,7 @@ func (c *Cluster) UnlockSwarm(req types.UnlockRequest) error {
580 580
 	}
581 581
 
582 582
 	config := *c.lastNodeConfig
583
-	config.lockKey = []byte(req.LockKey)
583
+	config.lockKey = key
584 584
 	n, err := c.startNewNode(config)
585 585
 	if err != nil {
586 586
 		c.Unlock()
... ...
@@ -779,9 +774,10 @@ func (c *Cluster) Update(version uint64, spec types.Spec, flags types.UpdateFlag
779 779
 			ClusterVersion: &swarmapi.Version{
780 780
 				Index: version,
781 781
 			},
782
-			Rotation: swarmapi.JoinTokenRotation{
783
-				RotateWorkerToken:  flags.RotateWorkerToken,
784
-				RotateManagerToken: flags.RotateManagerToken,
782
+			Rotation: swarmapi.KeyRotation{
783
+				WorkerJoinToken:  flags.RotateWorkerToken,
784
+				ManagerJoinToken: flags.RotateManagerToken,
785
+				ManagerUnlockKey: flags.RotateManagerUnlockKey,
785 786
 			},
786 787
 		},
787 788
 	)
... ...
@@ -1708,7 +1704,7 @@ func initClusterSpec(node *node, spec types.Spec) error {
1708 1708
 }
1709 1709
 
1710 1710
 func detectLockedError(err error) error {
1711
-	if errors.Cause(err) == x509.IncorrectPasswordError || errors.Cause(err).Error() == "tls: failed to parse private key" { // todo: better to export typed error
1711
+	if err == swarmnode.ErrInvalidUnlockKey {
1712 1712
 		return errors.WithStack(ErrSwarmLocked)
1713 1713
 	}
1714 1714
 	return err
... ...
@@ -26,6 +26,9 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm {
26 26
 					HeartbeatTick:              int(c.Spec.Raft.HeartbeatTick),
27 27
 					ElectionTick:               int(c.Spec.Raft.ElectionTick),
28 28
 				},
29
+				EncryptionConfig: types.EncryptionConfig{
30
+					AutoLockManagers: c.Spec.EncryptionConfig.AutoLockManagers,
31
+				},
29 32
 			},
30 33
 		},
31 34
 		JoinTokens: types.JoinTokens{
... ...
@@ -113,5 +116,7 @@ func MergeSwarmSpecToGRPC(s types.Spec, spec swarmapi.ClusterSpec) (swarmapi.Clu
113 113
 		})
114 114
 	}
115 115
 
116
+	spec.EncryptionConfig.AutoLockManagers = s.EncryptionConfig.AutoLockManagers
117
+
116 118
 	return spec, nil
117 119
 }