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