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