diff --git a/.changelog/11910.txt b/.changelog/11910.txt new file mode 100644 index 000000000..8bae2b3f8 --- /dev/null +++ b/.changelog/11910.txt @@ -0,0 +1,3 @@ +```release-note:feature +ca: support using an external root CA with the vault CA provider +``` diff --git a/agent/consul/leader_connect_ca_test.go b/agent/consul/leader_connect_ca_test.go index f0aabb823..9bdfc0740 100644 --- a/agent/consul/leader_connect_ca_test.go +++ b/agent/consul/leader_connect_ca_test.go @@ -98,14 +98,25 @@ func TestCAManager_Initialize_Vault_Secondary_SharedVault(t *testing.T) { } func verifyLeafCert(t *testing.T, root *structs.CARoot, leafCertPEM string) { + t.Helper() + roots := structs.IndexedCARoots{ + ActiveRootID: root.ID, + Roots: []*structs.CARoot{root}, + } + verifyLeafCertWithRoots(t, roots, leafCertPEM) +} + +func verifyLeafCertWithRoots(t *testing.T, roots structs.IndexedCARoots, leafCertPEM string) { t.Helper() leaf, intermediates, err := connect.ParseLeafCerts(leafCertPEM) require.NoError(t, err) pool := x509.NewCertPool() - ok := pool.AppendCertsFromPEM([]byte(root.RootCert)) - if !ok { - t.Fatalf("Failed to add root CA PEM to cert pool") + for _, r := range roots.Roots { + ok := pool.AppendCertsFromPEM([]byte(r.RootCert)) + if !ok { + t.Fatalf("Failed to add root CA PEM to cert pool") + } } // verify with intermediates from leaf CertPEM @@ -118,10 +129,12 @@ func verifyLeafCert(t *testing.T, root *structs.CARoot, leafCertPEM string) { // verify with intermediates from the CARoot intermediates = x509.NewCertPool() - for _, intermediate := range root.IntermediateCerts { - c, err := connect.ParseCert(intermediate) - require.NoError(t, err) - intermediates.AddCert(c) + for _, r := range roots.Roots { + for _, intermediate := range r.IntermediateCerts { + c, err := connect.ParseCert(intermediate) + require.NoError(t, err) + intermediates.AddCert(c) + } } _, err = leaf.Verify(x509.VerifyOptions{ @@ -628,10 +641,6 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) { "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, }, } }) @@ -664,10 +673,6 @@ func TestCAManager_Initialize_Vault_WithIntermediateAsPrimaryCA(t *testing.T) { "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, }, } }) @@ -727,10 +732,6 @@ func TestCAManager_Initialize_Vault_WithExternalTrustedCA(t *testing.T) { "Token": vault.RootToken, "RootPKIPath": primaryCAPath, "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, }, } }) @@ -765,15 +766,12 @@ func TestCAManager_Initialize_Vault_WithExternalTrustedCA(t *testing.T) { } }) - // TODO: renew primary leaf signing cert - // TODO: rotate root - runStep(t, "start secondary DC", func(t *testing.T) { joinWAN(t, serverDC2, serverDC1) testrpc.WaitForActiveCARoot(t, serverDC2.RPC, "dc2", nil) codec := rpcClient(t, serverDC2) - roots := structs.IndexedCARoots{} + roots = structs.IndexedCARoots{} err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) require.NoError(t, err) require.Len(t, roots.Roots, 1) @@ -788,6 +786,7 @@ func TestCAManager_Initialize_Vault_WithExternalTrustedCA(t *testing.T) { renewLeafSigningCert(t, serverDC1.caManager, serverDC1.caManager.primaryRenewIntermediate) codec := rpcClient(t, serverDC1) + roots = structs.IndexedCARoots{} err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) require.NoError(t, err) require.Len(t, roots.Roots, 1) @@ -809,6 +808,7 @@ func TestCAManager_Initialize_Vault_WithExternalTrustedCA(t *testing.T) { renewLeafSigningCert(t, serverDC2.caManager, serverDC2.caManager.secondaryRequestNewSigningCert) codec := rpcClient(t, serverDC2) + roots = structs.IndexedCARoots{} err := msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) require.NoError(t, err) require.Len(t, roots.Roots, 1) @@ -824,6 +824,68 @@ func TestCAManager_Initialize_Vault_WithExternalTrustedCA(t *testing.T) { // original certs from old signing cert should still verify verifyLeafCert(t, roots.Roots[0], origLeaf) }) + + runStep(t, "rotate root by changing the provider", func(t *testing.T) { + codec := rpcClient(t, serverDC1) + req := &structs.CARequest{ + Op: structs.CAOpSetConfig, + Config: &structs.CAConfiguration{ + Provider: "consul", + }, + } + var resp error + err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", req, &resp) + require.NoError(t, err) + require.Nil(t, resp) + + roots = structs.IndexedCARoots{} + err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) + require.NoError(t, err) + require.Len(t, roots.Roots, 2) + active := roots.Active() + require.Len(t, active.IntermediateCerts, 1) + + leafPEM := getLeafCert(t, codec, roots.TrustDomain, "dc1") + verifyLeafCert(t, roots.Active(), leafPEM) + + // original certs from old root cert should still verify + verifyLeafCertWithRoots(t, roots, origLeaf) + }) + + runStep(t, "rotate to a different external root", func(t *testing.T) { + setupPrimaryCA(t, vclient, "pki-primary-2/", rootPEM) + + codec := rpcClient(t, serverDC1) + req := &structs.CARequest{ + Op: structs.CAOpSetConfig, + Config: &structs.CAConfiguration{ + Provider: "vault", + Config: map[string]interface{}{ + "Address": vault.Addr, + "Token": vault.RootToken, + "RootPKIPath": "pki-primary-2/", + "IntermediatePKIPath": "pki-intermediate-2/", + }, + }, + } + var resp error + err := msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", req, &resp) + require.NoError(t, err) + require.Nil(t, resp) + + roots = structs.IndexedCARoots{} + err = msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", &structs.DCSpecificRequest{}, &roots) + require.NoError(t, err) + require.Len(t, roots.Roots, 3) + active := roots.Active() + require.Len(t, active.IntermediateCerts, 2) + + leafPEM := getLeafCert(t, codec, roots.TrustDomain, "dc1") + verifyLeafCert(t, roots.Active(), leafPEM) + + // original certs from old root cert should still verify + verifyLeafCertWithRoots(t, roots, origLeaf) + }) } // renewLeafSigningCert mimics RenewIntermediate. This is unfortunate, but