diff --git a/api/sys_init.go b/api/sys_init.go index d307f732b679..f824ab7ddbed 100644 --- a/api/sys_init.go +++ b/api/sys_init.go @@ -38,6 +38,7 @@ type InitRequest struct { RecoveryShares int `json:"recovery_shares"` RecoveryThreshold int `json:"recovery_threshold"` RecoveryPGPKeys []string `json:"recovery_pgp_keys"` + RootTokenPGPKey string `json:"root_token_pgp_key"` } type InitStatusResponse struct { diff --git a/command/init.go b/command/init.go index 91b8d6b75410..5f6418df6b6c 100644 --- a/command/init.go +++ b/command/init.go @@ -21,7 +21,7 @@ type InitCommand struct { func (c *InitCommand) Run(args []string) int { var threshold, shares, storedShares, recoveryThreshold, recoveryShares int - var pgpKeys, recoveryPgpKeys pgpkeys.PubKeyFilesFlag + var pgpKeys, recoveryPgpKeys, rootTokenPgpKey pgpkeys.PubKeyFilesFlag var auto, check bool var consulServiceName string flags := c.Meta.FlagSet("init", meta.FlagSetDefault) @@ -30,6 +30,7 @@ func (c *InitCommand) Run(args []string) int { flags.IntVar(&threshold, "key-threshold", 3, "") flags.IntVar(&storedShares, "stored-shares", 0, "") flags.Var(&pgpKeys, "pgp-keys", "") + flags.Var(&rootTokenPgpKey, "root-token-pgp-key", "") flags.IntVar(&recoveryShares, "recovery-shares", 5, "") flags.IntVar(&recoveryThreshold, "recovery-threshold", 3, "") flags.Var(&recoveryPgpKeys, "recovery-pgp-keys", "") @@ -50,6 +51,15 @@ func (c *InitCommand) Run(args []string) int { RecoveryPGPKeys: recoveryPgpKeys, } + switch len(rootTokenPgpKey) { + case 0: + case 1: + initRequest.RootTokenPGPKey = rootTokenPgpKey[0] + default: + c.Ui.Error("Only one PGP key can be specified for encrypting the root token") + return 1 + } + // If running in 'auto' mode, run service discovery based on environment // variables of Consul. if auto { @@ -60,7 +70,7 @@ func (c *InitCommand) Run(args []string) int { // 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)) + c.Ui.Error(fmt.Sprintf("Failed to create Consul client:%v", err)) return 1 } @@ -289,8 +299,9 @@ Init Options: -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. Only used with + Vault HSM. Must currently be equivalent to the + number of shares. -pgp-keys If provided, must be a comma-separated list of files on disk containing binary- or base64-format @@ -298,11 +309,18 @@ Init Options: "keybase:". The number of given entries must match 'key-shares'. The output unseal keys will be encrypted and base64-encoded, in order, with the - given public keys. If you want to use them with the + given public keys. If you want to use them with the 'vault unseal' command, you will need to base64- decode and decrypt; this will be the plaintext unseal key. + -root-token-pgp-key If provided, a file on disk with a binary- or + base64-format public PGP key, or a Keybase username + specified as "keybase:". The output root + token will be encrypted and base64-encoded, in + order, with the given public key. You will need + to base64-decode and decrypt the result. + -recovery-shares=5 The number of key shares to split the recovery key into. Only used with Vault HSM. diff --git a/command/init_test.go b/command/init_test.go index 7c70a3902a76..e09ba80dc05a 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -1,15 +1,20 @@ package command import ( + "bytes" + "encoding/base64" "os" "reflect" "regexp" + "strings" "testing" "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/meta" "github.com/hashicorp/vault/vault" + "github.com/keybase/go-crypto/openpgp" + "github.com/keybase/go-crypto/openpgp/packet" "github.com/mitchellh/cli" ) @@ -148,6 +153,57 @@ func TestInit_custom(t *testing.T) { if !reflect.DeepEqual(expected, sealConf) { t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf) } + + re, err := regexp.Compile("\\s+Initial Root Token:\\s+(.*)") + if err != nil { + t.Fatalf("Error compiling regex: %s", err) + } + matches := re.FindAllStringSubmatch(ui.OutputWriter.String(), -1) + if len(matches) != 1 { + t.Fatalf("Unexpected number of tokens found, got %d", len(matches)) + } + + rootToken := matches[0][1] + + client, err := c.Client() + if err != nil { + t.Fatalf("Error fetching client: %v", err) + } + + client.SetToken(rootToken) + + re, err = regexp.Compile("\\s*Unseal Key \\d+: (.*)") + if err != nil { + t.Fatalf("Error compiling regex: %s", err) + } + matches = re.FindAllStringSubmatch(ui.OutputWriter.String(), -1) + if len(matches) != 7 { + t.Fatalf("Unexpected number of keys returned, got %d, matches was \n\n%#v\n\n, input was \n\n%s\n\n", len(matches), matches, ui.OutputWriter.String()) + } + + var unsealed bool + for i := 0; i < 3; i++ { + decodedKey, err := base64.StdEncoding.DecodeString(strings.TrimSpace(matches[i][1])) + if err != nil { + t.Fatalf("err decoding key %v: %v", matches[i][1], err) + } + unsealed, err = core.Unseal(decodedKey) + if err != nil { + t.Fatalf("err during unseal: %v; key was %v", err, matches[i][1]) + } + } + if !unsealed { + t.Fatal("expected to be unsealed") + } + + tokenInfo, err := client.Auth().Token().LookupSelf() + if err != nil { + t.Fatalf("Error looking up root token info: %v", err) + } + + if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" { + t.Fatalf("expected root policy") + } } func TestInit_PGP(t *testing.T) { @@ -181,6 +237,7 @@ func TestInit_PGP(t *testing.T) { "-key-shares", "2", "-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2], "-key-threshold", "2", + "-root-token-pgp-key", pubFiles[0], } // This should fail, as key-shares does not match pgp-keys size @@ -193,6 +250,7 @@ func TestInit_PGP(t *testing.T) { "-key-shares", "4", "-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2] + "," + pubFiles[3], "-key-threshold", "2", + "-root-token-pgp-key", pubFiles[0], } ui.OutputWriter.Reset() @@ -242,7 +300,44 @@ func TestInit_PGP(t *testing.T) { t.Fatalf("Unexpected number of tokens found, got %d", len(matches)) } - rootToken := matches[0][1] + encRootToken := matches[0][1] + privKeyBytes, err := base64.StdEncoding.DecodeString(pgpkeys.TestPrivKey1) + if err != nil { + t.Fatalf("error decoding private key: %v", err) + } + ptBuf := bytes.NewBuffer(nil) + entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes))) + if err != nil { + t.Fatalf("Error parsing private key: %s", err) + } + var rootBytes []byte + rootBytes, err = base64.StdEncoding.DecodeString(encRootToken) + if err != nil { + t.Fatalf("Error decoding root token: %s", err) + } + entityList := &openpgp.EntityList{entity} + md, err := openpgp.ReadMessage(bytes.NewBuffer(rootBytes), entityList, nil, nil) + if err != nil { + t.Fatalf("Error decrypting root token: %s", err) + } + ptBuf.ReadFrom(md.UnverifiedBody) + rootToken := ptBuf.String() parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, nil, core) + + client, err := c.Client() + if err != nil { + t.Fatalf("Error fetching client: %v", err) + } + + client.SetToken(rootToken) + + tokenInfo, err := client.Auth().Token().LookupSelf() + if err != nil { + t.Fatalf("Error looking up root token info: %v", err) + } + + if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" { + t.Fatalf("expected root policy") + } } diff --git a/command/server.go b/command/server.go index 8af0733510d0..f63fbd418ba6 100644 --- a/command/server.go +++ b/command/server.go @@ -574,10 +574,13 @@ func (c *ServerCommand) Run(args []string) int { func (c *ServerCommand) enableDev(core *vault.Core, rootTokenID string) (*vault.InitResult, error) { // Initialize it with a basic single key - init, err := core.Initialize(&vault.SealConfig{ - SecretShares: 1, - SecretThreshold: 1, - }, nil) + init, err := core.Initialize(&vault.InitParams{ + BarrierConfig: &vault.SealConfig{ + SecretShares: 1, + SecretThreshold: 1, + }, + RecoveryConfig: nil, + }) if err != nil { return nil, err } diff --git a/http/sys_init.go b/http/sys_init.go index 78faabfaeaaa..e9fdcadfc008 100644 --- a/http/sys_init.go +++ b/http/sys_init.go @@ -81,7 +81,13 @@ func handleSysInitPut(core *vault.Core, w http.ResponseWriter, r *http.Request) } } - result, initErr := core.Initialize(barrierConfig, recoveryConfig) + initParams := &vault.InitParams{ + BarrierConfig: barrierConfig, + RecoveryConfig: recoveryConfig, + RootTokenPGPKey: req.RootTokenPGPKey, + } + + result, initErr := core.Initialize(initParams) if initErr != nil { if !errwrap.ContainsType(initErr, new(vault.NonFatalError)) { respondError(w, http.StatusBadRequest, initErr) @@ -128,6 +134,7 @@ type InitRequest struct { RecoveryShares int `json:"recovery_shares"` RecoveryThreshold int `json:"recovery_threshold"` RecoveryPGPKeys []string `json:"recovery_pgp_keys"` + RootTokenPGPKey string `json:"root_token_pgp_key"` } type InitResponse struct { diff --git a/logical/testing/testing.go b/logical/testing/testing.go index eba1f3a6e86b..b2072ea06c92 100644 --- a/logical/testing/testing.go +++ b/logical/testing/testing.go @@ -154,10 +154,13 @@ func Test(tt TestT, c TestCase) { } // Initialize the core - init, err := core.Initialize(&vault.SealConfig{ - SecretShares: 1, - SecretThreshold: 1, - }, nil) + init, err := core.Initialize(&vault.InitParams{ + BarrierConfig: &vault.SealConfig{ + SecretShares: 1, + SecretThreshold: 1, + }, + RecoveryConfig: nil, + }) if err != nil { tt.Fatal("error initializing core: ", err) return diff --git a/vault/core_test.go b/vault/core_test.go index 1e0f00bfe6aa..5e0648953d23 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -56,7 +56,10 @@ func TestCore_Unseal_MultiShare(t *testing.T) { SecretShares: 5, SecretThreshold: 3, } - res, err := c.Initialize(sealConf, nil) + res, err := c.Initialize(&InitParams{ + BarrierConfig: sealConf, + RecoveryConfig: nil, + }) if err != nil { t.Fatalf("err: %v", err) } @@ -141,7 +144,10 @@ func TestCore_Unseal_Single(t *testing.T) { SecretShares: 1, SecretThreshold: 1, } - res, err := c.Initialize(sealConf, nil) + res, err := c.Initialize(&InitParams{ + BarrierConfig: sealConf, + RecoveryConfig: nil, + }) if err != nil { t.Fatalf("err: %v", err) } @@ -196,7 +202,10 @@ func TestCore_Route_Sealed(t *testing.T) { t.Fatalf("err: %v", err) } - res, err := c.Initialize(sealConf, nil) + res, err := c.Initialize(&InitParams{ + BarrierConfig: sealConf, + RecoveryConfig: nil, + }) if err != nil { t.Fatalf("err: %v", err) } diff --git a/vault/init.go b/vault/init.go index 226e156e9f2d..e2c9f805b3f6 100644 --- a/vault/init.go +++ b/vault/init.go @@ -1,6 +1,7 @@ package vault import ( + "encoding/base64" "encoding/hex" "fmt" @@ -8,6 +9,14 @@ import ( "github.com/hashicorp/vault/shamir" ) +// InitParams keeps the init function from being littered with too many +// params, that's it! +type InitParams struct { + BarrierConfig *SealConfig + RecoveryConfig *SealConfig + RootTokenPGPKey string +} + // InitResult is used to provide the key parts back after // they are generated as part of the initialization. type InitResult struct { @@ -79,7 +88,10 @@ func (c *Core) generateShares(sc *SealConfig) ([]byte, [][]byte, error) { // Initialize is used to initialize the Vault with the given // configurations. -func (c *Core) Initialize(barrierConfig, recoveryConfig *SealConfig) (*InitResult, error) { +func (c *Core) Initialize(initParams *InitParams) (*InitResult, error) { + barrierConfig := initParams.BarrierConfig + recoveryConfig := initParams.RecoveryConfig + if c.seal.RecoveryKeySupported() { if recoveryConfig == nil { return nil, fmt.Errorf("recovery configuration must be supplied") @@ -219,6 +231,15 @@ func (c *Core) Initialize(barrierConfig, recoveryConfig *SealConfig) (*InitResul results.RootToken = rootToken.ID c.logger.Info("core: root token generated") + if initParams.RootTokenPGPKey != "" { + _, encryptedVals, err := pgpkeys.EncryptShares([][]byte{[]byte(results.RootToken)}, []string{initParams.RootTokenPGPKey}) + if err != nil { + c.logger.Error("core: root token encryption failed", "error", err) + return nil, err + } + results.RootToken = base64.StdEncoding.EncodeToString(encryptedVals[0]) + } + // Prepare to re-seal if err := c.preSeal(); err != nil { c.logger.Error("core: pre-seal teardown failed", "error", err) diff --git a/vault/init_test.go b/vault/init_test.go index 1e9dd69dc028..d957a7ee5a0e 100644 --- a/vault/init_test.go +++ b/vault/init_test.go @@ -69,7 +69,10 @@ func testCore_Init_Common(t *testing.T, c *Core, conf *CoreConfig, barrierConf, } } - res, err := c.Initialize(barrierConf, recoveryConf) + res, err := c.Initialize(&InitParams{ + BarrierConfig: barrierConf, + RecoveryConfig: recoveryConf, + }) if err != nil { t.Fatalf("err: %v", err) } @@ -87,7 +90,10 @@ func testCore_Init_Common(t *testing.T, c *Core, conf *CoreConfig, barrierConf, t.Fatalf("Bad: %#v", res) } - _, err = c.Initialize(barrierConf, recoveryConf) + _, err = c.Initialize(&InitParams{ + BarrierConfig: barrierConf, + RecoveryConfig: recoveryConf, + }) if err != ErrAlreadyInit { t.Fatalf("err: %v", err) } @@ -125,7 +131,10 @@ func testCore_Init_Common(t *testing.T, c *Core, conf *CoreConfig, barrierConf, t.Fatalf("err: %v", err) } - _, err = c2.Initialize(barrierConf, recoveryConf) + _, err = c2.Initialize(&InitParams{ + BarrierConfig: barrierConf, + RecoveryConfig: recoveryConf, + }) if err != ErrAlreadyInit { t.Fatalf("err: %v", err) } diff --git a/vault/seal_testing.go b/vault/seal_testing.go index e3e4ed4397ef..f29223071a8d 100644 --- a/vault/seal_testing.go +++ b/vault/seal_testing.go @@ -95,7 +95,10 @@ func (d *TestSeal) SetRecoveryKey(key []byte) error { func TestCoreUnsealedWithConfigs(t *testing.T, barrierConf, recoveryConf *SealConfig) (*Core, [][]byte, [][]byte, string) { seal := &TestSeal{} core := TestCoreWithSeal(t, seal) - result, err := core.Initialize(barrierConf, recoveryConf) + result, err := core.Initialize(&InitParams{ + BarrierConfig: barrierConf, + RecoveryConfig: recoveryConf, + }) if err != nil { t.Fatalf("err: %s", err) } diff --git a/vault/testing.go b/vault/testing.go index e9eabaec6d55..ed810cb0beb5 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -143,10 +143,13 @@ func TestCoreInit(t *testing.T, core *Core) ([]byte, string) { func TestCoreInitClusterWrapperSetup(t *testing.T, core *Core, clusterAddrs []*net.TCPAddr, handlerSetupFunc func() (http.Handler, http.Handler)) ([]byte, string) { core.SetClusterListenerAddrs(clusterAddrs) core.SetClusterSetupFuncs(handlerSetupFunc) - result, err := core.Initialize(&SealConfig{ - SecretShares: 1, - SecretThreshold: 1, - }, nil) + result, err := core.Initialize(&InitParams{ + BarrierConfig: &SealConfig{ + SecretShares: 1, + SecretThreshold: 1, + }, + RecoveryConfig: nil, + }) if err != nil { t.Fatalf("err: %s", err) } diff --git a/website/source/docs/http/sys-init.html.md b/website/source/docs/http/sys-init.html.md index 2ed1fa4aeee2..f403e02ad919 100644 --- a/website/source/docs/http/sys-init.html.md +++ b/website/source/docs/http/sys-init.html.md @@ -39,8 +39,9 @@ description: |-
Description
- Initializes a new Vault. The Vault must've not been previously - initialized. + Initializes a new Vault. The Vault must not have been previously + initialized. The recovery options, as well as the stored shares option, are + only available when using Vault HSM.
Method
@@ -49,6 +50,12 @@ description: |-
Parameters
    +
  • + root_token_pgp_key + optional + A PGP public key used to encrypt the initial root token. The key + must be base64-encoded from its original binary representation. +
  • secret_shares required @@ -57,8 +64,10 @@ description: |-
  • secret_threshold required - The number of shares required to reconstruct the master key. - This must be less than or equal to secret_shares. + The number of shares required to reconstruct the master key. This must + be less than or equal to secret_shares. If using Vault HSM + with auto-unsealing, this value must be the same as + secret_shares.
  • pgp_keys @@ -68,6 +77,33 @@ description: |- original binary representation. The size of this array must be the same as secret_shares.
  • +
  • + stored_shares + required + The number of shares that should be encrypted by the HSM and stored for + auto-unsealing (Vault HSM only). Currently must be the same as + secret_shares. +
  • +
  • + recovery_shares + required + The number of shares to split the recovery key into (Vault HSM only). +
  • +
  • + recovery_threshold + required + The number of shares required to reconstruct the recovery key (Vault + HSM only). This must be less than or equal to + recovery_shares. +
  • +
  • + recovery_pgp_keys + optional + An array of PGP public keys used to encrypt the output recovery keys + (Vault HSM only). Ordering is preserved. The keys must be + base64-encoded from their original binary representation. The size of + this array must be the same as recovery_shares. +