open-vault/command/operator_init.go

563 lines
17 KiB
Go
Raw Normal View History

2015-03-13 17:32:39 +00:00
package command
import (
"fmt"
2016-07-21 23:04:43 +00:00
"net/url"
2016-07-20 19:38:53 +00:00
"runtime"
2015-03-13 17:32:39 +00:00
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/pgpkeys"
2017-09-05 04:02:02 +00:00
"github.com/mitchellh/cli"
"github.com/posener/complete"
2017-09-05 04:02:02 +00:00
consulapi "github.com/hashicorp/consul/api"
2015-03-13 17:32:39 +00:00
)
2017-09-08 02:03:12 +00:00
var _ cli.Command = (*OperatorInitCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorInitCommand)(nil)
2017-09-05 04:02:02 +00:00
2017-09-08 02:03:12 +00:00
type OperatorInitCommand struct {
2017-09-05 04:02:02 +00:00
*BaseCommand
flagStatus bool
flagKeyShares int
flagKeyThreshold int
flagPGPKeys []string
flagRootTokenPGPKey string
// HSM
flagStoredShares int
flagRecoveryShares int
flagRecoveryThreshold int
flagRecoveryPGPKeys []string
// Consul
flagConsulAuto bool
flagConsulService string
// Deprecations
// TODO: remove in 0.9.0
flagAuto bool
flagCheck bool
2015-03-13 17:32:39 +00:00
}
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) Synopsis() string {
2017-09-05 04:02:02 +00:00
return "Initializes a server"
}
2015-03-13 17:32:39 +00:00
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) Help() string {
2017-09-05 04:02:02 +00:00
helpText := `
2017-09-08 02:03:12 +00:00
Usage: vault operator init [options]
2016-07-20 19:38:53 +00:00
2017-09-05 04:02:02 +00:00
Initializes a Vault server. Initialization is the process by which Vault's
storage backend is prepared to receive data. Since Vault servers share the
2017-09-05 04:02:02 +00:00
same storage backend in HA mode, you only need to initialize one Vault to
initialize the storage backend.
2017-09-05 04:02:02 +00:00
During initialization, Vault generates an in-memory master key and applies
Shamir's secret sharing algorithm to disassemble that master key into a
configuration number of key shares such that a configurable subset of those
key shares must come together to regenerate the master key. These keys are
often called "unseal keys" in Vault's documentation.
2017-09-05 04:02:02 +00:00
This command cannot be run against already-initialized Vault cluster.
2016-07-20 19:38:53 +00:00
2017-09-05 04:02:02 +00:00
Start initialization with the default options:
2016-07-20 19:38:53 +00:00
2017-09-08 02:03:12 +00:00
$ vault operator init
2017-09-05 04:02:02 +00:00
Initialize, but encrypt the unseal keys with pgp keys:
2017-09-08 02:03:12 +00:00
$ vault operator init \
2017-09-05 04:02:02 +00:00
-key-shares=3 \
-key-threshold=2 \
-pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo"
2017-09-05 04:02:02 +00:00
Encrypt the initial root token using a pgp key:
2017-09-08 02:03:12 +00:00
$ vault operator init -root-token-pgp-key="keybase:hashicorp"
2016-07-20 19:38:53 +00:00
2017-09-05 04:02:02 +00:00
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
2016-07-20 19:38:53 +00:00
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) Flags() *FlagSets {
2017-09-05 04:02:02 +00:00
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.BoolVar(&BoolVar{
Name: "status",
Target: &c.flagStatus,
Default: false,
Usage: "Print the current initialization status. An exit code of 0 means " +
"the Vault is already initialized. An exit code of 1 means an error " +
"occurred. An exit code of 2 means the mean is not initialized.",
})
f.IntVar(&IntVar{
Name: "key-shares",
Aliases: []string{"n"},
Target: &c.flagKeyShares,
Default: 5,
Completion: complete.PredictAnything,
Usage: "Number of key shares to split the generated master key into. " +
"This is the number of \"unseal keys\" to generate.",
})
f.IntVar(&IntVar{
Name: "key-threshold",
Aliases: []string{"t"},
Target: &c.flagKeyThreshold,
Default: 3,
Completion: complete.PredictAnything,
Usage: "Number of key shares required to reconstruct the master key. " +
"This must be less than or equal to -key-shares.",
})
f.VarFlag(&VarFlag{
Name: "pgp-keys",
Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagPGPKeys),
Completion: complete.PredictAnything,
Usage: "Comma-separated list of paths to files on disk containing " +
"public GPG keys OR a comma-separated list of Keybase usernames using " +
"the format \"keybase:<username>\". When supplied, the generated " +
"unseal keys will be encrypted and base64-encoded in the order " +
"specified in this list. The number of entires must match -key-shares, " +
"unless -store-shares are used.",
})
f.VarFlag(&VarFlag{
Name: "root-token-pgp-key",
Value: (*pgpkeys.PubKeyFileFlag)(&c.flagRootTokenPGPKey),
Completion: complete.PredictAnything,
Usage: "Path to a file on disk containing a binary or base64-encoded " +
"public GPG key. This can also be specified as a Keybase username " +
"using the format \"keybase:<username>\". When supplied, the generated " +
"root token will be encrypted and base64-encoded with the given public " +
"key.",
})
// Consul Options
f = set.NewFlagSet("Consul Options")
f.BoolVar(&BoolVar{
Name: "consul-auto",
Target: &c.flagConsulAuto,
Default: false,
Usage: "Perform automatic service discovery using Consul in HA mode. " +
"When all nodes in a Vault HA cluster are registered with Consul, " +
"enabling this option will trigger automatic service discovery based " +
"on the provided -consul-service value. When Consul is Vault's HA " +
"backend, this functionality is automatically enabled. Ensure the " +
"proper Consul environment variables are set (CONSUL_HTTP_ADDR, etc). " +
"When only one Vault server is discovered, it will be initialized " +
"automatically. When more than one Vault server is discovered, they " +
"will each be output for selection.",
})
f.StringVar(&StringVar{
Name: "consul-service",
Target: &c.flagConsulService,
Default: "vault",
Completion: complete.PredictAnything,
Usage: "Name of the service in Consul under which the Vault servers are " +
"registered.",
})
// HSM Options
f = set.NewFlagSet("HSM Options")
f.IntVar(&IntVar{
Name: "recovery-shares",
Target: &c.flagRecoveryShares,
Default: 5,
Completion: complete.PredictAnything,
Usage: "Number of key shares to split the recovery key into. " +
"This is only used in HSM mode.",
})
f.IntVar(&IntVar{
Name: "recovery-threshold",
Target: &c.flagRecoveryThreshold,
Default: 3,
Completion: complete.PredictAnything,
Usage: "Number of key shares required to reconstruct the recovery key. " +
"This is only used in HSM mode.",
})
f.VarFlag(&VarFlag{
Name: "recovery-pgp-keys",
Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagRecoveryPGPKeys),
Completion: complete.PredictAnything,
Usage: "Behaves like -pgp-keys, but for the recovery key shares. This " +
"is only used in HSM mode.",
})
f.IntVar(&IntVar{
Name: "stored-shares",
Target: &c.flagStoredShares,
Default: 0, // No default, because we need to check if was supplied
Completion: complete.PredictAnything,
Usage: "Number of unseal keys to store on an HSM. This must be equal to " +
"-key-shares. This is only used in HSM mode.",
})
// Deprecations
// TODO: remove in 0.9.0
f.BoolVar(&BoolVar{
Name: "check", // prefer -status
Target: &c.flagCheck,
Default: false,
Hidden: true,
Usage: "",
})
f.BoolVar(&BoolVar{
Name: "auto", // prefer -consul-auto
Target: &c.flagAuto,
Default: false,
Hidden: true,
Usage: "",
})
return set
}
2016-07-21 23:04:43 +00:00
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) AutocompleteArgs() complete.Predictor {
2017-09-05 04:02:02 +00:00
return nil
}
2016-07-20 19:38:53 +00:00
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) AutocompleteFlags() complete.Flags {
2017-09-05 04:02:02 +00:00
return c.Flags().Completions()
}
2016-07-20 19:38:53 +00:00
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) Run(args []string) int {
2017-09-05 04:02:02 +00:00
f := c.Flags()
2016-07-20 19:38:53 +00:00
2017-09-05 04:02:02 +00:00
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
2016-07-20 19:38:53 +00:00
2017-09-05 04:02:02 +00:00
// Deprecations
// TODO: remove in 0.9.0
if c.flagAuto {
c.UI.Warn(wrapAtLength("WARNING! -auto is deprecated. Please use " +
"-consul-auto instead. This will be removed in Vault 0.11 " +
"(or later)."))
2017-09-05 04:02:02 +00:00
c.flagConsulAuto = true
}
if c.flagCheck {
c.UI.Warn(wrapAtLength("WARNING! -check is deprecated. Please use " +
"-status instead. This will be removed in Vault 0.11 (or later)."))
2017-09-05 04:02:02 +00:00
c.flagStatus = true
}
2016-07-20 19:38:53 +00:00
2017-09-05 04:02:02 +00:00
// Build the initial init request
initReq := &api.InitRequest{
SecretShares: c.flagKeyShares,
SecretThreshold: c.flagKeyThreshold,
PGPKeys: c.flagPGPKeys,
RootTokenPGPKey: c.flagRootTokenPGPKey,
StoredShares: c.flagStoredShares,
RecoveryShares: c.flagRecoveryShares,
RecoveryThreshold: c.flagRecoveryThreshold,
RecoveryPGPKeys: c.flagRecoveryPGPKeys,
}
2016-07-20 19:38:53 +00:00
2017-09-05 04:02:02 +00:00
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
2016-07-20 19:38:53 +00:00
}
2017-09-05 04:02:02 +00:00
// Check auto mode
switch {
case c.flagStatus:
return c.status(client)
case c.flagConsulAuto:
return c.consulAuto(client, initReq)
default:
return c.init(client, initReq)
}
2016-07-20 19:38:53 +00:00
}
2017-09-05 04:02:02 +00:00
// consulAuto enables auto-joining via Consul.
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) consulAuto(client *api.Client, req *api.InitRequest) int {
2017-09-05 04:02:02 +00:00
// Capture the client original address and reset it
originalAddr := client.Address()
defer client.SetAddress(originalAddr)
// Create a client to communicate with Consul
consulClient, err := consulapi.NewClient(consulapi.DefaultConfig())
2015-03-13 17:32:39 +00:00
if err != nil {
2017-09-05 04:02:02 +00:00
c.UI.Error(fmt.Sprintf("Failed to create Consul client:%v", err))
2015-03-13 17:32:39 +00:00
return 1
}
2017-09-05 04:02:02 +00:00
// Pull the scheme from the Vault client to determine if the Consul agent
// should talk via HTTP or HTTPS.
addr := client.Address()
clientURL, err := url.Parse(addr)
if err != nil || clientURL == nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %s: %s", addr, err))
return 1
}
var uninitedVaults []string
var initedVault string
// Query the nodes belonging to the cluster
services, _, err := consulClient.Catalog().Service(c.flagConsulService, "", &consulapi.QueryOptions{
AllowStale: true,
})
if err == nil {
for _, service := range services {
// Set the address on the client temporarily
vaultAddr := (&url.URL{
Scheme: clientURL.Scheme,
Host: fmt.Sprintf("%s:%d", service.ServiceAddress, service.ServicePort),
}).String()
client.SetAddress(vaultAddr)
// Check the initialization status of the discovered node
inited, err := client.Sys().InitStatus()
if err != nil {
c.UI.Error(fmt.Sprintf("Error checking init status of %q: %s", vaultAddr, err))
}
if inited {
initedVault = vaultAddr
break
}
// If we got this far, we communicated successfully with Vault, but it
// was not initialized.
uninitedVaults = append(uninitedVaults, vaultAddr)
}
}
// Get the correct export keywords and quotes for *nix vs Windows
export := "export"
quote := "\""
if runtime.GOOS == "windows" {
export = "set"
quote = ""
}
if initedVault != "" {
vaultURL, err := url.Parse(initedVault)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
return 2
}
vaultAddr := vaultURL.String()
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Discovered an initialized Vault node at %q with Consul service name "+
"%q. Set the following environment variable to target the discovered "+
"Vault server:",
vaultURL.String(), c.flagConsulService)))
c.UI.Output("")
c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
c.UI.Output("")
return 0
}
switch len(uninitedVaults) {
case 0:
c.UI.Error(fmt.Sprintf("No Vault nodes registered as %q in Consul", c.flagConsulService))
return 2
case 1:
// There was only one node found in the Vault cluster and it was
// uninitialized.
vaultURL, err := url.Parse(uninitedVaults[0])
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
return 2
}
vaultAddr := vaultURL.String()
// Update the client to connect to this Vault server
client.SetAddress(vaultAddr)
// Let the client know that initialization is perfomed on the
// discovered node.
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Discovered an initialized Vault node at %q with Consul service name "+
"%q. Set the following environment variable to target the discovered "+
"Vault server:",
vaultURL.String(), c.flagConsulService)))
c.UI.Output("")
c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
c.UI.Output("")
c.UI.Output("Attempting to initialize it...")
c.UI.Output("")
// Attempt to initialize it
return c.init(client, req)
default:
// If more than one Vault node were discovered, print out all of them,
// requiring the client to update VAULT_ADDR and to run init again.
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Discovered %d uninitialized Vault servers with Consul service name "+
"%q. To initialize these Vaults, set any one of the following "+
2017-09-05 04:02:02 +00:00
"environment variables and run \"vault init\":",
len(uninitedVaults), c.flagConsulService)))
c.UI.Output("")
// Print valid commands to make setting the variables easier
for _, node := range uninitedVaults {
vaultURL, err := url.Parse(node)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
return 2
}
vaultAddr := vaultURL.String()
c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
}
c.UI.Output("")
return 0
2016-01-22 18:06:40 +00:00
}
2017-09-05 04:02:02 +00:00
}
2016-01-22 18:06:40 +00:00
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) init(client *api.Client, req *api.InitRequest) int {
2017-09-05 04:02:02 +00:00
resp, err := client.Sys().Init(req)
2015-03-13 17:32:39 +00:00
if err != nil {
2017-09-05 04:02:02 +00:00
c.UI.Error(fmt.Sprintf("Error initializing: %s", err))
return 2
}
CLI Enhancements (#3897) * Use Colored UI if stdout is a tty * Add format options to operator unseal * Add format test on operator unseal * Add -no-color output flag, and use BasicUi if no-color flag is provided * Move seal status formatting logic to OutputSealStatus * Apply no-color to warnings from DeprecatedCommands as well * Add OutputWithFormat to support arbitrary data, add format option to auth list * Add ability to output arbitrary list data on TableFormatter * Clear up switch logic on format * Add format option for list-related commands * Add format option to rest of commands that returns a client API response * Remove initOutputYAML and initOutputJSON, and use OutputWithFormat instead * Remove outputAsYAML and outputAsJSON, and use OutputWithFormat instead * Remove -no-color flag, use env var exclusively to toggle colored output * Fix compile * Remove -no-color flag in main.go * Add missing FlagSetOutputFormat * Fix generate-root/decode test * Migrate init functions to main.go * Add no-color flag back as hidden * Handle non-supported data types for TableFormatter.OutputList * Pull formatting much further up to remove the need to use c.flagFormat (#3950) * Pull formatting much further up to remove the need to use c.flagFormat Also remove OutputWithFormat as the logic can cause issues. * Use const for env var * Minor updates * Remove unnecessary check * Fix SSH output and some tests * Fix tests * Make race detector not run on generate root since it kills Travis these days * Update docs * Update docs * Address review feedback * Handle --format as well as -format
2018-02-12 23:12:16 +00:00
switch Format(c.UI) {
2017-09-05 04:02:02 +00:00
case "table":
default:
CLI Enhancements (#3897) * Use Colored UI if stdout is a tty * Add format options to operator unseal * Add format test on operator unseal * Add -no-color output flag, and use BasicUi if no-color flag is provided * Move seal status formatting logic to OutputSealStatus * Apply no-color to warnings from DeprecatedCommands as well * Add OutputWithFormat to support arbitrary data, add format option to auth list * Add ability to output arbitrary list data on TableFormatter * Clear up switch logic on format * Add format option for list-related commands * Add format option to rest of commands that returns a client API response * Remove initOutputYAML and initOutputJSON, and use OutputWithFormat instead * Remove outputAsYAML and outputAsJSON, and use OutputWithFormat instead * Remove -no-color flag, use env var exclusively to toggle colored output * Fix compile * Remove -no-color flag in main.go * Add missing FlagSetOutputFormat * Fix generate-root/decode test * Migrate init functions to main.go * Add no-color flag back as hidden * Handle non-supported data types for TableFormatter.OutputList * Pull formatting much further up to remove the need to use c.flagFormat (#3950) * Pull formatting much further up to remove the need to use c.flagFormat Also remove OutputWithFormat as the logic can cause issues. * Use const for env var * Minor updates * Remove unnecessary check * Fix SSH output and some tests * Fix tests * Make race detector not run on generate root since it kills Travis these days * Update docs * Update docs * Address review feedback * Handle --format as well as -format
2018-02-12 23:12:16 +00:00
return OutputData(c.UI, newMachineInit(req, resp))
2015-03-13 17:32:39 +00:00
}
for i, key := range resp.Keys {
if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) {
2017-09-05 04:02:02 +00:00
c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, resp.KeysB64[i]))
} else {
2017-09-05 04:02:02 +00:00
c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, key))
}
2016-04-04 14:44:22 +00:00
}
for i, key := range resp.RecoveryKeys {
if resp.RecoveryKeysB64 != nil && len(resp.RecoveryKeysB64) == len(resp.RecoveryKeys) {
2017-09-05 04:02:02 +00:00
c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, resp.RecoveryKeysB64[i]))
} else {
2017-09-05 04:02:02 +00:00
c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, key))
}
2015-03-13 17:32:39 +00:00
}
2017-09-05 04:02:02 +00:00
c.UI.Output("")
c.UI.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken))
if req.StoredShares < 1 {
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault initialized with %d key shares and a key threshold of %d. Please "+
"securely distribute the key shares printed above. When the Vault is "+
2017-09-05 04:02:02 +00:00
"re-sealed, restarted, or stopped, you must supply at least %d of "+
"these keys to unseal it before it can start servicing requests.",
req.SecretShares,
req.SecretThreshold,
req.SecretThreshold)))
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault does not store the generated master key. Without at least %d "+
"key to reconstruct the master key, Vault will remain permanently "+
"sealed!",
req.SecretThreshold)))
c.UI.Output("")
c.UI.Output(wrapAtLength(
"It is possible to generate new unseal keys, provided you have a quorum " +
"of existing unseal keys shares. See \"vault rekey\" for more " +
"information."))
2016-04-04 14:44:22 +00:00
} else {
2017-09-05 04:02:02 +00:00
c.UI.Output("")
c.UI.Output("Success! Vault is initialized")
2016-04-04 14:44:22 +00:00
}
2017-09-05 04:02:02 +00:00
2016-04-04 14:44:22 +00:00
if len(resp.RecoveryKeys) > 0 {
2017-09-05 04:02:02 +00:00
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Recovery key initialized with %d key shares and a key threshold of %d. "+
"Please securely distribute the key shares printed above.",
req.RecoveryShares,
req.RecoveryThreshold)))
2016-04-04 14:44:22 +00:00
}
2015-03-13 17:32:39 +00:00
return 0
}
2017-09-05 04:02:02 +00:00
// status inspects the init status of vault and returns an appropriate error
// code and message.
2017-09-08 02:03:12 +00:00
func (c *OperatorInitCommand) status(client *api.Client) int {
2017-09-05 04:02:02 +00:00
inited, err := client.Sys().InitStatus()
if err != nil {
c.UI.Error(fmt.Sprintf("Error checking init status: %s", err))
return 1 // Normally we'd return 2, but 2 means something special here
}
2015-03-13 17:32:39 +00:00
2017-09-05 04:02:02 +00:00
if inited {
c.UI.Output("Vault is initialized")
return 0
}
c.UI.Output("Vault is not initialized")
return 2
2015-03-13 17:32:39 +00:00
}
2017-09-05 04:02:02 +00:00
// machineInit is used to output information about the init command.
type machineInit struct {
UnsealKeysB64 []string `json:"unseal_keys_b64"`
UnsealKeysHex []string `json:"unseal_keys_hex"`
UnsealShares int `json:"unseal_shares"`
UnsealThreshold int `json:"unseal_threshold"`
RecoveryKeysB64 []string `json:"recovery_keys_b64"`
RecoveryKeysHex []string `json:"recovery_keys_hex"`
RecoveryShares int `json:"recovery_keys_shares"`
RecoveryThreshold int `json:"recovery_keys_threshold"`
RootToken string `json:"root_token"`
}
2017-09-05 04:02:02 +00:00
func newMachineInit(req *api.InitRequest, resp *api.InitResponse) *machineInit {
init := &machineInit{}
init.UnsealKeysHex = make([]string, len(resp.Keys))
for i, v := range resp.Keys {
init.UnsealKeysHex[i] = v
}
init.UnsealKeysB64 = make([]string, len(resp.KeysB64))
for i, v := range resp.KeysB64 {
init.UnsealKeysB64[i] = v
}
2017-09-05 04:02:02 +00:00
init.UnsealShares = req.SecretShares
init.UnsealThreshold = req.SecretThreshold
init.RecoveryKeysHex = make([]string, len(resp.RecoveryKeys))
for i, v := range resp.RecoveryKeys {
init.RecoveryKeysHex[i] = v
}
init.RecoveryKeysB64 = make([]string, len(resp.RecoveryKeysB64))
for i, v := range resp.RecoveryKeysB64 {
init.RecoveryKeysB64[i] = v
}
init.RecoveryShares = req.RecoveryShares
init.RecoveryThreshold = req.RecoveryThreshold
init.RootToken = resp.RootToken
return init
}