- 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>
... | ... |
@@ -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 |
} |