diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index 3a1d5128e7..c7b99a13f3 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -16,10 +16,9 @@ import ( vaultapi "github.com/hashicorp/vault/api" "github.com/mitchellh/mapstructure" - "github.com/hashicorp/consul/lib/decode" - "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/lib/decode" ) const ( @@ -400,17 +399,14 @@ func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM string) error { return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter") } - err := validateSetIntermediate( - intermediatePEM, rootPEM, - "", // we don't have access to the private key directly - v.spiffeID, - ) + // the private key is in vault, so we can't use it in this validation + err := validateSetIntermediate(intermediatePEM, rootPEM, "", v.spiffeID) if err != nil { return err } _, err = v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{ - "certificate": fmt.Sprintf("%s\n%s", intermediatePEM, rootPEM), + "certificate": intermediatePEM, }) if err != nil { return err diff --git a/agent/consul/leader_connect_ca.go b/agent/consul/leader_connect_ca.go index 203d3f2a40..5b795727fe 100644 --- a/agent/consul/leader_connect_ca.go +++ b/agent/consul/leader_connect_ca.go @@ -654,7 +654,7 @@ func (c *CAManager) secondaryInitializeIntermediateCA(provider ca.Provider, conf } if needsNewIntermediate { - if err := c.secondaryRenewIntermediate(provider, newActiveRoot); err != nil { + if err := c.secondaryRequestNewSigningCert(provider, newActiveRoot); err != nil { return err } } else { @@ -1028,9 +1028,11 @@ func (c *CAManager) primaryRenewIntermediate(provider ca.Provider, newActiveRoot return nil } -// secondaryRenewIntermediate should only be called while the state lock is held by -// setting the state to non-ready. -func (c *CAManager) secondaryRenewIntermediate(provider ca.Provider, newActiveRoot *structs.CARoot) error { +// secondaryRequestNewSigningCert creates a Certificate Signing Request, sends +// the request to the primary, and stores the received certificate in the +// provider. +// Should only be called while the state lock is held by setting the state to non-ready. +func (c *CAManager) secondaryRequestNewSigningCert(provider ca.Provider, newActiveRoot *structs.CARoot) error { csr, err := provider.GenerateIntermediateCSR() if err != nil { return err @@ -1145,7 +1147,7 @@ func (c *CAManager) RenewIntermediate(ctx context.Context, isPrimary bool) error // Enough time has passed, go ahead with getting a new intermediate. renewalFunc := c.primaryRenewIntermediate if !isPrimary { - renewalFunc = c.secondaryRenewIntermediate + renewalFunc = c.secondaryRequestNewSigningCert } errCh := make(chan error, 1) go func() { diff --git a/agent/consul/leader_connect_ca_test.go b/agent/consul/leader_connect_ca_test.go index 17b310cb6a..c3115668e7 100644 --- a/agent/consul/leader_connect_ca_test.go +++ b/agent/consul/leader_connect_ca_test.go @@ -17,6 +17,7 @@ import ( "time" msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc" + vaultapi "github.com/hashicorp/vault/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -606,6 +607,88 @@ func TestCAManager_UpdateConfiguration_Vault_Primary(t *testing.T) { require.Equal(t, connect.HexString(cert.SubjectKeyId), newRoot.SigningKeyID) } +func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + ca.SkipIfVaultNotPresent(t) + + vault := ca.NewTestVaultServer(t) + vclient := vault.Client() + generateExternalRootCA(t, vclient) + + meshRootPath := "pki-root" + primaryCert := setupPrimaryCA(t, vclient, meshRootPath) + + _, s1 := testServerWithConfig(t, func(c *Config) { + c.CAConfig = &structs.CAConfiguration{ + Provider: "vault", + Config: map[string]interface{}{ + "Address": vault.Addr, + "Token": vault.RootToken, + "RootPKIPath": meshRootPath, + "IntermediatePKIPath": "pki-intermediate/", + // TODO: there are failures to init the CA system if these are not set + // to the values of the already initialized CA. + "PrivateKeyType": "ec", + "PrivateKeyBits": 256, + }, + } + }) + defer s1.Shutdown() + + runStep(t, "check primary DC", func(t *testing.T) { + testrpc.WaitForTestAgent(t, s1.RPC, "dc1") + + codec := rpcClient(t, s1) + roots := structs.IndexedCARoots{} + err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) + require.NoError(t, err) + require.Len(t, roots.Roots, 1) + require.Equal(t, primaryCert, roots.Roots[0].RootCert) + + leafCertPEM := getLeafCert(t, codec, roots.TrustDomain, "dc1") + verifyLeafCert(t, roots.Roots[0], leafCertPEM) + }) + + // TODO: renew primary leaf signing cert + // TODO: rotate root + + runStep(t, "run secondary DC", func(t *testing.T) { + _, sDC2 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "dc2" + c.PrimaryDatacenter = "dc1" + c.CAConfig = &structs.CAConfiguration{ + Provider: "vault", + Config: map[string]interface{}{ + "Address": vault.Addr, + "Token": vault.RootToken, + "RootPKIPath": meshRootPath, + "IntermediatePKIPath": "pki-secondary/", + // TODO: there are failures to init the CA system if these are not set + // to the values of the already initialized CA. + "PrivateKeyType": "ec", + "PrivateKeyBits": 256, + }, + } + }) + defer sDC2.Shutdown() + joinWAN(t, sDC2, s1) + testrpc.WaitForActiveCARoot(t, sDC2.RPC, "dc2", nil) + + codec := rpcClient(t, sDC2) + roots := structs.IndexedCARoots{} + err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) + require.NoError(t, err) + require.Len(t, roots.Roots, 1) + + leafCertPEM := getLeafCert(t, codec, roots.TrustDomain, "dc2") + verifyLeafCert(t, roots.Roots[0], leafCertPEM) + + // TODO: renew secondary leaf signing cert + }) +} + func getLeafCert(t *testing.T, codec rpc.ClientCodec, trustDomain string, dc string) string { pk, _, err := connect.GeneratePrivateKey() require.NoError(t, err) @@ -624,3 +707,58 @@ func getLeafCert(t *testing.T, codec rpc.ClientCodec, trustDomain string, dc str return cert.CertPEM } + +func generateExternalRootCA(t *testing.T, client *vaultapi.Client) string { + t.Helper() + err := client.Sys().Mount("corp", &vaultapi.MountInput{ + Type: "pki", + Description: "External root, probably corporate CA", + Config: vaultapi.MountConfigInput{ + MaxLeaseTTL: "2400h", + DefaultLeaseTTL: "1h", + }, + }) + require.NoError(t, err, "failed to mount") + + resp, err := client.Logical().Write("corp/root/generate/internal", map[string]interface{}{ + "common_name": "corporate CA", + "ttl": "2400h", + }) + require.NoError(t, err, "failed to generate root") + return resp.Data["certificate"].(string) +} + +func setupPrimaryCA(t *testing.T, client *vaultapi.Client, path string) string { + t.Helper() + err := client.Sys().Mount(path, &vaultapi.MountInput{ + Type: "pki", + Description: "primary CA for Consul CA", + Config: vaultapi.MountConfigInput{ + MaxLeaseTTL: "2200h", + DefaultLeaseTTL: "1h", + }, + }) + require.NoError(t, err, "failed to mount") + + out, err := client.Logical().Write(path+"/intermediate/generate/internal", map[string]interface{}{ + "common_name": "primary CA", + "ttl": "2200h", + "key_type": "ec", + "key_bits": 256, + }) + require.NoError(t, err, "failed to generate root") + + intermediate, err := client.Logical().Write("corp/root/sign-intermediate", map[string]interface{}{ + "csr": out.Data["csr"], + "use_csr_values": true, + "format": "pem_bundle", + "ttl": "2200h", + }) + require.NoError(t, err, "failed to sign intermediate") + + _, err = client.Logical().Write(path+"/intermediate/set-signed", map[string]interface{}{ + "certificate": intermediate.Data["certificate"], + }) + require.NoError(t, err, "failed to set signed intermediate") + return ca.EnsureTrailingNewline(intermediate.Data["certificate"].(string)) +}