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