mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-03 22:02:19 -04:00
Merge pull request #1642 from hashicorp/init-service-discovery
Add service discovery to init command
This commit is contained in:
commit
2511231c64
3 changed files with 202 additions and 47 deletions
|
|
@ -45,7 +45,7 @@ type InitStatusResponse struct {
|
|||
}
|
||||
|
||||
type InitResponse struct {
|
||||
Keys []string
|
||||
Keys []string `json:"keys"`
|
||||
RecoveryKeys []string `json:"recovery_keys"`
|
||||
RootToken string `json:"root_token"`
|
||||
}
|
||||
|
|
|
|||
241
command/init.go
241
command/init.go
|
|
@ -2,11 +2,16 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
consulapi "github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/pgpkeys"
|
||||
"github.com/hashicorp/vault/meta"
|
||||
"github.com/hashicorp/vault/physical"
|
||||
)
|
||||
|
||||
// InitCommand is a Command that initializes a new Vault server.
|
||||
|
|
@ -17,7 +22,8 @@ type InitCommand struct {
|
|||
func (c *InitCommand) Run(args []string) int {
|
||||
var threshold, shares, storedShares, recoveryThreshold, recoveryShares int
|
||||
var pgpKeys, recoveryPgpKeys pgpkeys.PubKeyFilesFlag
|
||||
var check bool
|
||||
var auto, check bool
|
||||
var consulServiceName string
|
||||
flags := c.Meta.FlagSet("init", meta.FlagSetDefault)
|
||||
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
flags.IntVar(&shares, "key-shares", 5, "")
|
||||
|
|
@ -28,10 +34,145 @@ func (c *InitCommand) Run(args []string) int {
|
|||
flags.IntVar(&recoveryThreshold, "recovery-threshold", 3, "")
|
||||
flags.Var(&recoveryPgpKeys, "recovery-pgp-keys", "")
|
||||
flags.BoolVar(&check, "check", false, "")
|
||||
flags.BoolVar(&auto, "auto", false, "")
|
||||
flags.StringVar(&consulServiceName, "consul-service", physical.DefaultServiceName, "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
initRequest := &api.InitRequest{
|
||||
SecretShares: shares,
|
||||
SecretThreshold: threshold,
|
||||
StoredShares: storedShares,
|
||||
PGPKeys: pgpKeys,
|
||||
RecoveryShares: recoveryShares,
|
||||
RecoveryThreshold: recoveryThreshold,
|
||||
RecoveryPGPKeys: recoveryPgpKeys,
|
||||
}
|
||||
|
||||
// If running in 'auto' mode, run service discovery based on environment
|
||||
// variables of Consul.
|
||||
if auto {
|
||||
|
||||
// Create configuration for Consul
|
||||
consulConfig := consulapi.DefaultConfig()
|
||||
|
||||
// Create a client to communicate with Consul
|
||||
consulClient, err := consulapi.NewClient(consulConfig)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("failed to create Consul client:%v", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var uninitializedVaults []string
|
||||
var initializedVault string
|
||||
|
||||
// Query the nodes belonging to the cluster
|
||||
if services, _, err := consulClient.Catalog().Service(consulServiceName, "", &consulapi.QueryOptions{AllowStale: true}); err == nil {
|
||||
Loop:
|
||||
for _, service := range services {
|
||||
vaultAddress := &url.URL{
|
||||
Scheme: consulConfig.Scheme,
|
||||
Host: fmt.Sprintf("%s:%d", service.ServiceAddress, service.ServicePort),
|
||||
}
|
||||
|
||||
// Set VAULT_ADDR to the discovered node
|
||||
os.Setenv(api.EnvVaultAddress, vaultAddress.String())
|
||||
|
||||
// Create a client to communicate with the discovered node
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %v", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check the initialization status of the discovered node
|
||||
inited, err := client.Sys().InitStatus()
|
||||
switch {
|
||||
case err != nil:
|
||||
c.Ui.Error(fmt.Sprintf("Error checking initialization status of discovered node: %+q. Err: %v", vaultAddress.String(), err))
|
||||
return 1
|
||||
case inited:
|
||||
// One of the nodes in the cluster is initialized. Break out.
|
||||
initializedVault = vaultAddress.String()
|
||||
break Loop
|
||||
default:
|
||||
// Vault is uninitialized.
|
||||
uninitializedVaults = append(uninitializedVaults, vaultAddress.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export := "export"
|
||||
quote := "'"
|
||||
if runtime.GOOS == "windows" {
|
||||
export = "set"
|
||||
quote = ""
|
||||
}
|
||||
|
||||
if initializedVault != "" {
|
||||
vaultURL, err := url.Parse(initializedVault)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", initializedVault, err))
|
||||
}
|
||||
c.Ui.Output(fmt.Sprintf("Discovered an initialized Vault node at %+q, using Consul service name %+q", vaultURL.String(), consulServiceName))
|
||||
c.Ui.Output("\nSet the following environment variable to operate on the discovered Vault:\n")
|
||||
c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote))
|
||||
return 0
|
||||
}
|
||||
|
||||
switch len(uninitializedVaults) {
|
||||
case 0:
|
||||
c.Ui.Error(fmt.Sprintf("Failed to discover Vault nodes using Consul service name %+q", consulServiceName))
|
||||
return 1
|
||||
case 1:
|
||||
// There was only one node found in the Vault cluster and it
|
||||
// was uninitialized.
|
||||
|
||||
vaultURL, err := url.Parse(uninitializedVaults[0])
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", uninitializedVaults[0], err))
|
||||
}
|
||||
|
||||
// Set the VAULT_ADDR to the discovered node. This will ensure
|
||||
// that the client created will operate on the discovered node.
|
||||
os.Setenv(api.EnvVaultAddress, vaultURL.String())
|
||||
|
||||
// Let the client know that initialization is perfomed on the
|
||||
// discovered node.
|
||||
c.Ui.Output(fmt.Sprintf("Discovered Vault at %+q using Consul service name %+q\n", vaultURL.String(), consulServiceName))
|
||||
|
||||
// Attempt initializing it
|
||||
ret := c.runInit(check, initRequest)
|
||||
|
||||
// Regardless of success or failure, instruct client to update VAULT_ADDR
|
||||
c.Ui.Output("\nSet the following environment variable to operate on the discovered Vault:\n")
|
||||
c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote))
|
||||
|
||||
return ret
|
||||
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(fmt.Sprintf("Discovered more than one uninitialized Vaults using Consul service name %+q\n", consulServiceName))
|
||||
c.Ui.Output("To initialize these Vaults, set any *one* of the following environment variables and run 'vault init':")
|
||||
|
||||
// Print valid commands to make setting the variables easier
|
||||
for _, vaultNode := range uninitializedVaults {
|
||||
vaultURL, err := url.Parse(vaultNode)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", vaultNode, err))
|
||||
}
|
||||
c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote))
|
||||
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return c.runInit(check, initRequest)
|
||||
}
|
||||
|
||||
func (c *InitCommand) runInit(check bool, initRequest *api.InitRequest) int {
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
|
|
@ -43,15 +184,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
return c.checkStatus(client)
|
||||
}
|
||||
|
||||
resp, err := client.Sys().Init(&api.InitRequest{
|
||||
SecretShares: shares,
|
||||
SecretThreshold: threshold,
|
||||
StoredShares: storedShares,
|
||||
PGPKeys: pgpKeys,
|
||||
RecoveryShares: recoveryShares,
|
||||
RecoveryThreshold: recoveryThreshold,
|
||||
RecoveryPGPKeys: recoveryPgpKeys,
|
||||
})
|
||||
resp, err := client.Sys().Init(initRequest)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error initializing Vault: %s", err))
|
||||
|
|
@ -67,7 +200,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||
|
||||
c.Ui.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken))
|
||||
|
||||
if storedShares < 1 {
|
||||
if initRequest.StoredShares < 1 {
|
||||
c.Ui.Output(fmt.Sprintf(
|
||||
"\n"+
|
||||
"Vault initialized with %d keys and a key threshold of %d. Please\n"+
|
||||
|
|
@ -76,10 +209,10 @@ func (c *InitCommand) Run(args []string) int {
|
|||
"to unseal it again.\n\n"+
|
||||
"Vault does not store the master key. Without at least %d keys,\n"+
|
||||
"your Vault will remain permanently sealed.",
|
||||
shares,
|
||||
threshold,
|
||||
threshold,
|
||||
threshold,
|
||||
initRequest.SecretShares,
|
||||
initRequest.SecretThreshold,
|
||||
initRequest.SecretThreshold,
|
||||
initRequest.SecretThreshold,
|
||||
))
|
||||
} else {
|
||||
c.Ui.Output(
|
||||
|
|
@ -92,8 +225,8 @@ func (c *InitCommand) Run(args []string) int {
|
|||
"\n"+
|
||||
"Recovery key initialized with %d keys and a key threshold of %d. Please\n"+
|
||||
"securely distribute the above keys.",
|
||||
recoveryShares,
|
||||
recoveryThreshold,
|
||||
initRequest.RecoveryShares,
|
||||
initRequest.RecoveryThreshold,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -136,39 +269,61 @@ General Options:
|
|||
` + meta.GeneralOptionsUsage() + `
|
||||
Init Options:
|
||||
|
||||
-check Don't actually initialize, just check if Vault is
|
||||
already initialized. A return code of 0 means Vault
|
||||
is initialized; a return code of 2 means Vault is not
|
||||
initialized; a return code of 1 means an error was
|
||||
encountered.
|
||||
-check Don't actually initialize, just check if Vault is
|
||||
already initialized. A return code of 0 means Vault
|
||||
is initialized; a return code of 2 means Vault is not
|
||||
initialized; a return code of 1 means an error was
|
||||
encountered.
|
||||
|
||||
-key-shares=5 The number of key shares to split the master key
|
||||
into.
|
||||
-key-shares=5 The number of key shares to split the master key
|
||||
into.
|
||||
|
||||
-key-threshold=3 The number of key shares required to reconstruct
|
||||
the master key.
|
||||
-key-threshold=3 The number of key shares required to reconstruct
|
||||
the master key.
|
||||
|
||||
-stored-shares=0 The number of unseal keys to store. This is not
|
||||
normally available.
|
||||
-stored-shares=0 The number of unseal keys to store. This is not
|
||||
normally available.
|
||||
|
||||
-pgp-keys If provided, must be a comma-separated list of
|
||||
files on disk containing binary- or base64-format
|
||||
public PGP keys, or Keybase usernames specified as
|
||||
"keybase:<username>". The number of given entries
|
||||
must match 'key-shares'. The output unseal keys will
|
||||
be encrypted and hex-encoded, in order, with the
|
||||
given public keys. If you want to use them with the
|
||||
'vault unseal' command, you will need to hex decode
|
||||
and decrypt; this will be the plaintext unseal key.
|
||||
-pgp-keys If provided, must be a comma-separated list of
|
||||
files on disk containing binary- or base64-format
|
||||
public PGP keys, or Keybase usernames specified as
|
||||
"keybase:<username>". The number of given entries
|
||||
must match 'key-shares'. The output unseal keys will
|
||||
be encrypted and hex-encoded, in order, with the
|
||||
given public keys. If you want to use them with the
|
||||
'vault unseal' command, you will need to hex decode
|
||||
and decrypt; this will be the plaintext unseal key.
|
||||
|
||||
-recovery-shares=5 The number of key shares to split the recovery key
|
||||
into. This is not normally available.
|
||||
-recovery-shares=5 The number of key shares to split the recovery key
|
||||
into. This is not normally available.
|
||||
|
||||
-recovery-threshold=3 The number of key shares required to reconstruct
|
||||
the recovery key. This is not normally available.
|
||||
-recovery-threshold=3 The number of key shares required to reconstruct
|
||||
the recovery key. This is not normally available.
|
||||
|
||||
-recovery-pgp-keys If provided, behaves like "pgp-keys" but for the
|
||||
recovery key shares. This is not normally available.
|
||||
-recovery-pgp-keys If provided, behaves like "pgp-keys" but for the
|
||||
recovery key shares. This is not normally available.
|
||||
|
||||
-auto If set, performs service discovery using the underlying
|
||||
Consul storage backend. When one or more Vault servers
|
||||
are using Consul for data storage, setting this flag
|
||||
will create a Consul client and discover nodes using
|
||||
the service name under which Vault nodes are registered
|
||||
with Consul. The service name can be changed using
|
||||
'consul-service' flag. This option works well when each
|
||||
Vault cluster is registered under a unique service name.
|
||||
Ensure that environment variables required to communicate
|
||||
with Consul, like (CONSUL_HTTP_ADDR, CONSUL_HTTP_TOKEN,
|
||||
CONSUL_HTTP_SSL, et al) are properly set. If only one
|
||||
Vault node is discovered, then an initialization attempt
|
||||
will be made. If more than one Vault node is discovered,
|
||||
they will be output.
|
||||
|
||||
-consul-service Service name under which all the nodes of a Vault cluster
|
||||
are registered with Consul. When Vault uses Consul as its
|
||||
storage backend, by default, it will register as a service
|
||||
with Consul by the name "vault". This name can be modified
|
||||
in Vault's configuration file, using the "service" option
|
||||
for the Consul backend.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ const (
|
|||
// defaultCheckTimeout changes the timeout of TTL checks
|
||||
defaultCheckTimeout = 5 * time.Second
|
||||
|
||||
// defaultServiceName is the default Consul service name used when
|
||||
// DefaultServiceName is the default Consul service name used when
|
||||
// advertising a Vault instance.
|
||||
defaultServiceName = "vault"
|
||||
DefaultServiceName = "vault"
|
||||
|
||||
// reconcileTimeout is how often Vault should query Consul to detect
|
||||
// and fix any state drift.
|
||||
|
|
@ -104,7 +104,7 @@ func newConsulBackend(conf map[string]string, logger *log.Logger) (Backend, erro
|
|||
// Get the service name to advertise in Consul
|
||||
service, ok := conf["service"]
|
||||
if !ok {
|
||||
service = defaultServiceName
|
||||
service = DefaultServiceName
|
||||
}
|
||||
logger.Printf("[DEBUG]: consul: config service set to %s", service)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue