1package command
2
3import (
4	"fmt"
5	"net/url"
6	"runtime"
7	"strings"
8
9	"github.com/hashicorp/vault/api"
10	"github.com/hashicorp/vault/helper/pgpkeys"
11	"github.com/mitchellh/cli"
12	"github.com/posener/complete"
13
14	consulapi "github.com/hashicorp/consul/api"
15)
16
17var _ cli.Command = (*OperatorInitCommand)(nil)
18var _ cli.CommandAutocomplete = (*OperatorInitCommand)(nil)
19
20type OperatorInitCommand struct {
21	*BaseCommand
22
23	flagStatus          bool
24	flagKeyShares       int
25	flagKeyThreshold    int
26	flagPGPKeys         []string
27	flagRootTokenPGPKey string
28
29	// Auto Unseal
30	flagRecoveryShares    int
31	flagRecoveryThreshold int
32	flagRecoveryPGPKeys   []string
33	flagStoredShares      int
34
35	// Consul
36	flagConsulAuto    bool
37	flagConsulService string
38}
39
40const (
41	defKeyShares    = 5
42	defKeyThreshold = 3
43)
44
45func (c *OperatorInitCommand) Synopsis() string {
46	return "Initializes a server"
47}
48
49func (c *OperatorInitCommand) Help() string {
50	helpText := `
51Usage: vault operator init [options]
52
53  Initializes a Vault server. Initialization is the process by which Vault's
54  storage backend is prepared to receive data. Since Vault servers share the
55  same storage backend in HA mode, you only need to initialize one Vault to
56  initialize the storage backend.
57
58  During initialization, Vault generates an in-memory master key and applies
59  Shamir's secret sharing algorithm to disassemble that master key into a
60  configuration number of key shares such that a configurable subset of those
61  key shares must come together to regenerate the master key. These keys are
62  often called "unseal keys" in Vault's documentation.
63
64  This command cannot be run against an already-initialized Vault cluster.
65
66  Start initialization with the default options:
67
68      $ vault operator init
69
70  Initialize, but encrypt the unseal keys with pgp keys:
71
72      $ vault operator init \
73          -key-shares=3 \
74          -key-threshold=2 \
75          -pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo"
76
77  Encrypt the initial root token using a pgp key:
78
79      $ vault operator init -root-token-pgp-key="keybase:hashicorp"
80
81` + c.Flags().Help()
82	return strings.TrimSpace(helpText)
83}
84
85func (c *OperatorInitCommand) Flags() *FlagSets {
86	set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
87
88	// Common Options
89	f := set.NewFlagSet("Common Options")
90
91	f.BoolVar(&BoolVar{
92		Name:    "status",
93		Target:  &c.flagStatus,
94		Default: false,
95		Usage: "Print the current initialization status. An exit code of 0 means " +
96			"the Vault is already initialized. An exit code of 1 means an error " +
97			"occurred. An exit code of 2 means the mean is not initialized.",
98	})
99
100	f.IntVar(&IntVar{
101		Name:       "key-shares",
102		Aliases:    []string{"n"},
103		Target:     &c.flagKeyShares,
104		Default:    defKeyShares,
105		Completion: complete.PredictAnything,
106		Usage: "Number of key shares to split the generated master key into. " +
107			"This is the number of \"unseal keys\" to generate.",
108	})
109
110	f.IntVar(&IntVar{
111		Name:       "key-threshold",
112		Aliases:    []string{"t"},
113		Target:     &c.flagKeyThreshold,
114		Default:    defKeyThreshold,
115		Completion: complete.PredictAnything,
116		Usage: "Number of key shares required to reconstruct the master key. " +
117			"This must be less than or equal to -key-shares.",
118	})
119
120	f.VarFlag(&VarFlag{
121		Name:       "pgp-keys",
122		Value:      (*pgpkeys.PubKeyFilesFlag)(&c.flagPGPKeys),
123		Completion: complete.PredictAnything,
124		Usage: "Comma-separated list of paths to files on disk containing " +
125			"public GPG keys OR a comma-separated list of Keybase usernames using " +
126			"the format \"keybase:<username>\". When supplied, the generated " +
127			"unseal keys will be encrypted and base64-encoded in the order " +
128			"specified in this list. The number of entries must match -key-shares, " +
129			"unless -store-shares are used.",
130	})
131
132	f.VarFlag(&VarFlag{
133		Name:       "root-token-pgp-key",
134		Value:      (*pgpkeys.PubKeyFileFlag)(&c.flagRootTokenPGPKey),
135		Completion: complete.PredictAnything,
136		Usage: "Path to a file on disk containing a binary or base64-encoded " +
137			"public GPG key. This can also be specified as a Keybase username " +
138			"using the format \"keybase:<username>\". When supplied, the generated " +
139			"root token will be encrypted and base64-encoded with the given public " +
140			"key.",
141	})
142
143	f.IntVar(&IntVar{
144		Name:    "stored-shares",
145		Target:  &c.flagStoredShares,
146		Default: -1,
147		Usage:   "DEPRECATED: This flag does nothing. It will be removed in Vault 1.3.",
148	})
149
150	// Consul Options
151	f = set.NewFlagSet("Consul Options")
152
153	f.BoolVar(&BoolVar{
154		Name:    "consul-auto",
155		Target:  &c.flagConsulAuto,
156		Default: false,
157		Usage: "Perform automatic service discovery using Consul in HA mode. " +
158			"When all nodes in a Vault HA cluster are registered with Consul, " +
159			"enabling this option will trigger automatic service discovery based " +
160			"on the provided -consul-service value. When Consul is Vault's HA " +
161			"backend, this functionality is automatically enabled. Ensure the " +
162			"proper Consul environment variables are set (CONSUL_HTTP_ADDR, etc). " +
163			"When only one Vault server is discovered, it will be initialized " +
164			"automatically. When more than one Vault server is discovered, they " +
165			"will each be output for selection.",
166	})
167
168	f.StringVar(&StringVar{
169		Name:       "consul-service",
170		Target:     &c.flagConsulService,
171		Default:    "vault",
172		Completion: complete.PredictAnything,
173		Usage: "Name of the service in Consul under which the Vault servers are " +
174			"registered.",
175	})
176
177	// Auto Unseal Options
178	f = set.NewFlagSet("Auto Unseal Options")
179
180	f.IntVar(&IntVar{
181		Name:       "recovery-shares",
182		Target:     &c.flagRecoveryShares,
183		Default:    5,
184		Completion: complete.PredictAnything,
185		Usage: "Number of key shares to split the recovery key into. " +
186			"This is only used in auto-unseal mode.",
187	})
188
189	f.IntVar(&IntVar{
190		Name:       "recovery-threshold",
191		Target:     &c.flagRecoveryThreshold,
192		Default:    3,
193		Completion: complete.PredictAnything,
194		Usage: "Number of key shares required to reconstruct the recovery key. " +
195			"This is only used in Auto Unseal mode.",
196	})
197
198	f.VarFlag(&VarFlag{
199		Name:       "recovery-pgp-keys",
200		Value:      (*pgpkeys.PubKeyFilesFlag)(&c.flagRecoveryPGPKeys),
201		Completion: complete.PredictAnything,
202		Usage: "Behaves like -pgp-keys, but for the recovery key shares. This " +
203			"is only used in Auto Unseal mode.",
204	})
205
206	return set
207}
208
209func (c *OperatorInitCommand) AutocompleteArgs() complete.Predictor {
210	return nil
211}
212
213func (c *OperatorInitCommand) AutocompleteFlags() complete.Flags {
214	return c.Flags().Completions()
215}
216
217func (c *OperatorInitCommand) Run(args []string) int {
218	f := c.Flags()
219
220	if err := f.Parse(args); err != nil {
221		c.UI.Error(err.Error())
222		return 1
223	}
224
225	args = f.Args()
226	if len(args) > 0 {
227		c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
228		return 1
229	}
230
231	if c.flagStoredShares != -1 {
232		c.UI.Warn("-stored-shares has no effect and will be removed in Vault 1.3.\n")
233	}
234
235	// Build the initial init request
236	initReq := &api.InitRequest{
237		SecretShares:    c.flagKeyShares,
238		SecretThreshold: c.flagKeyThreshold,
239		PGPKeys:         c.flagPGPKeys,
240		RootTokenPGPKey: c.flagRootTokenPGPKey,
241
242		RecoveryShares:    c.flagRecoveryShares,
243		RecoveryThreshold: c.flagRecoveryThreshold,
244		RecoveryPGPKeys:   c.flagRecoveryPGPKeys,
245	}
246
247	client, err := c.Client()
248	if err != nil {
249		c.UI.Error(err.Error())
250		return 2
251	}
252
253	// Check auto mode
254	switch {
255	case c.flagStatus:
256		return c.status(client)
257	case c.flagConsulAuto:
258		return c.consulAuto(client, initReq)
259	default:
260		return c.init(client, initReq)
261	}
262}
263
264// consulAuto enables auto-joining via Consul.
265func (c *OperatorInitCommand) consulAuto(client *api.Client, req *api.InitRequest) int {
266	// Capture the client original address and reset it
267	originalAddr := client.Address()
268	defer client.SetAddress(originalAddr)
269
270	// Create a client to communicate with Consul
271	consulClient, err := consulapi.NewClient(consulapi.DefaultConfig())
272	if err != nil {
273		c.UI.Error(fmt.Sprintf("Failed to create Consul client:%v", err))
274		return 1
275	}
276
277	// Pull the scheme from the Vault client to determine if the Consul agent
278	// should talk via HTTP or HTTPS.
279	addr := client.Address()
280	clientURL, err := url.Parse(addr)
281	if err != nil || clientURL == nil {
282		c.UI.Error(fmt.Sprintf("Failed to parse Vault address %s: %s", addr, err))
283		return 1
284	}
285
286	var uninitedVaults []string
287	var initedVault string
288
289	// Query the nodes belonging to the cluster
290	services, _, err := consulClient.Catalog().Service(c.flagConsulService, "", &consulapi.QueryOptions{
291		AllowStale: true,
292	})
293	if err == nil {
294		for _, service := range services {
295			// Set the address on the client temporarily
296			vaultAddr := (&url.URL{
297				Scheme: clientURL.Scheme,
298				Host:   fmt.Sprintf("%s:%d", service.ServiceAddress, service.ServicePort),
299			}).String()
300			client.SetAddress(vaultAddr)
301
302			// Check the initialization status of the discovered node
303			inited, err := client.Sys().InitStatus()
304			if err != nil {
305				c.UI.Error(fmt.Sprintf("Error checking init status of %q: %s", vaultAddr, err))
306			}
307			if inited {
308				initedVault = vaultAddr
309				break
310			}
311
312			// If we got this far, we communicated successfully with Vault, but it
313			// was not initialized.
314			uninitedVaults = append(uninitedVaults, vaultAddr)
315		}
316	}
317
318	// Get the correct export keywords and quotes for *nix vs Windows
319	export := "export"
320	quote := "\""
321	if runtime.GOOS == "windows" {
322		export = "set"
323		quote = ""
324	}
325
326	if initedVault != "" {
327		vaultURL, err := url.Parse(initedVault)
328		if err != nil {
329			c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
330			return 2
331		}
332		vaultAddr := vaultURL.String()
333
334		c.UI.Output(wrapAtLength(fmt.Sprintf(
335			"Discovered an initialized Vault node at %q with Consul service name "+
336				"%q. Set the following environment variable to target the discovered "+
337				"Vault server:",
338			vaultURL.String(), c.flagConsulService)))
339		c.UI.Output("")
340		c.UI.Output(fmt.Sprintf("    $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
341		c.UI.Output("")
342		return 0
343	}
344
345	switch len(uninitedVaults) {
346	case 0:
347		c.UI.Error(fmt.Sprintf("No Vault nodes registered as %q in Consul", c.flagConsulService))
348		return 2
349	case 1:
350		// There was only one node found in the Vault cluster and it was
351		// uninitialized.
352		vaultURL, err := url.Parse(uninitedVaults[0])
353		if err != nil {
354			c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
355			return 2
356		}
357		vaultAddr := vaultURL.String()
358
359		// Update the client to connect to this Vault server
360		client.SetAddress(vaultAddr)
361
362		// Let the client know that initialization is performed on the
363		// discovered node.
364		c.UI.Output(wrapAtLength(fmt.Sprintf(
365			"Discovered an initialized Vault node at %q with Consul service name "+
366				"%q. Set the following environment variable to target the discovered "+
367				"Vault server:",
368			vaultURL.String(), c.flagConsulService)))
369		c.UI.Output("")
370		c.UI.Output(fmt.Sprintf("    $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
371		c.UI.Output("")
372		c.UI.Output("Attempting to initialize it...")
373		c.UI.Output("")
374
375		// Attempt to initialize it
376		return c.init(client, req)
377	default:
378		// If more than one Vault node were discovered, print out all of them,
379		// requiring the client to update VAULT_ADDR and to run init again.
380		c.UI.Output(wrapAtLength(fmt.Sprintf(
381			"Discovered %d uninitialized Vault servers with Consul service name "+
382				"%q. To initialize these Vaults, set any one of the following "+
383				"environment variables and run \"vault operator init\":",
384			len(uninitedVaults), c.flagConsulService)))
385		c.UI.Output("")
386
387		// Print valid commands to make setting the variables easier
388		for _, node := range uninitedVaults {
389			vaultURL, err := url.Parse(node)
390			if err != nil {
391				c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
392				return 2
393			}
394			vaultAddr := vaultURL.String()
395
396			c.UI.Output(fmt.Sprintf("    $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
397		}
398
399		c.UI.Output("")
400		return 0
401	}
402}
403
404func (c *OperatorInitCommand) init(client *api.Client, req *api.InitRequest) int {
405	resp, err := client.Sys().Init(req)
406	if err != nil {
407		c.UI.Error(fmt.Sprintf("Error initializing: %s", err))
408		return 2
409	}
410
411	switch Format(c.UI) {
412	case "table":
413	default:
414		return OutputData(c.UI, newMachineInit(req, resp))
415	}
416
417	for i, key := range resp.Keys {
418		if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) {
419			c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, resp.KeysB64[i]))
420		} else {
421			c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, key))
422		}
423	}
424	for i, key := range resp.RecoveryKeys {
425		if resp.RecoveryKeysB64 != nil && len(resp.RecoveryKeysB64) == len(resp.RecoveryKeys) {
426			c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, resp.RecoveryKeysB64[i]))
427		} else {
428			c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, key))
429		}
430	}
431
432	c.UI.Output("")
433	c.UI.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken))
434
435	if len(resp.Keys) > 0 {
436		c.UI.Output("")
437		c.UI.Output(wrapAtLength(fmt.Sprintf(
438			"Vault initialized with %d key shares and a key threshold of %d. Please "+
439				"securely distribute the key shares printed above. When the Vault is "+
440				"re-sealed, restarted, or stopped, you must supply at least %d of "+
441				"these keys to unseal it before it can start servicing requests.",
442			req.SecretShares,
443			req.SecretThreshold,
444			req.SecretThreshold)))
445
446		c.UI.Output("")
447		c.UI.Output(wrapAtLength(fmt.Sprintf(
448			"Vault does not store the generated master key. Without at least %d "+
449				"key to reconstruct the master key, Vault will remain permanently "+
450				"sealed!",
451			req.SecretThreshold)))
452
453		c.UI.Output("")
454		c.UI.Output(wrapAtLength(
455			"It is possible to generate new unseal keys, provided you have a quorum " +
456				"of existing unseal keys shares. See \"vault operator rekey\" for " +
457				"more information."))
458	} else {
459		c.UI.Output("")
460		c.UI.Output("Success! Vault is initialized")
461	}
462
463	if len(resp.RecoveryKeys) > 0 {
464		c.UI.Output("")
465		c.UI.Output(wrapAtLength(fmt.Sprintf(
466			"Recovery key initialized with %d key shares and a key threshold of %d. "+
467				"Please securely distribute the key shares printed above.",
468			req.RecoveryShares,
469			req.RecoveryThreshold)))
470	}
471
472	if len(resp.RecoveryKeys) > 0 && (req.SecretShares != defKeyShares || req.SecretThreshold != defKeyThreshold) {
473		c.UI.Output("")
474		c.UI.Warn(wrapAtLength(
475			"WARNING! -key-shares and -key-threshold is ignored when " +
476				"Auto Unseal is used. Use -recovery-shares and -recovery-threshold instead.",
477		))
478	}
479
480	return 0
481}
482
483// status inspects the init status of vault and returns an appropriate error
484// code and message.
485func (c *OperatorInitCommand) status(client *api.Client) int {
486	inited, err := client.Sys().InitStatus()
487	if err != nil {
488		c.UI.Error(fmt.Sprintf("Error checking init status: %s", err))
489		return 1 // Normally we'd return 2, but 2 means something special here
490	}
491
492	if inited {
493		c.UI.Output("Vault is initialized")
494		return 0
495	}
496
497	c.UI.Output("Vault is not initialized")
498	return 2
499}
500
501// machineInit is used to output information about the init command.
502type machineInit struct {
503	UnsealKeysB64     []string `json:"unseal_keys_b64"`
504	UnsealKeysHex     []string `json:"unseal_keys_hex"`
505	UnsealShares      int      `json:"unseal_shares"`
506	UnsealThreshold   int      `json:"unseal_threshold"`
507	RecoveryKeysB64   []string `json:"recovery_keys_b64"`
508	RecoveryKeysHex   []string `json:"recovery_keys_hex"`
509	RecoveryShares    int      `json:"recovery_keys_shares"`
510	RecoveryThreshold int      `json:"recovery_keys_threshold"`
511	RootToken         string   `json:"root_token"`
512}
513
514func newMachineInit(req *api.InitRequest, resp *api.InitResponse) *machineInit {
515	init := &machineInit{}
516
517	init.UnsealKeysHex = make([]string, len(resp.Keys))
518	for i, v := range resp.Keys {
519		init.UnsealKeysHex[i] = v
520	}
521
522	init.UnsealKeysB64 = make([]string, len(resp.KeysB64))
523	for i, v := range resp.KeysB64 {
524		init.UnsealKeysB64[i] = v
525	}
526
527	// If we don't get a set of keys back, it means that we are storing the keys,
528	// so the key shares and threshold has been set to 1.
529	if len(resp.Keys) == 0 {
530		init.UnsealShares = 1
531		init.UnsealThreshold = 1
532	} else {
533		init.UnsealShares = req.SecretShares
534		init.UnsealThreshold = req.SecretThreshold
535	}
536
537	init.RecoveryKeysHex = make([]string, len(resp.RecoveryKeys))
538	for i, v := range resp.RecoveryKeys {
539		init.RecoveryKeysHex[i] = v
540	}
541
542	init.RecoveryKeysB64 = make([]string, len(resp.RecoveryKeysB64))
543	for i, v := range resp.RecoveryKeysB64 {
544		init.RecoveryKeysB64[i] = v
545	}
546
547	init.RecoveryShares = req.RecoveryShares
548	init.RecoveryThreshold = req.RecoveryThreshold
549
550	init.RootToken = resp.RootToken
551
552	return init
553}
554