Merge pull request #11671 from hashicorp/dnephin/ca-fix-storing-vault-intermediate

ca: fix storing the leaf signing cert with Vault provider
This commit is contained in:
Daniel Nephin 2021-12-02 16:02:24 -05:00 committed by GitHub
commit ff4581092e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 23 deletions

3
.changelog/11671.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ca: fixes a bug that caused the intermediate cert used to sign leaf certs to be missing from the /connect/ca/roots API response when the Vault provider was used.
```

View File

@ -517,12 +517,26 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf
} }
} }
var rootUpdateRequired bool
// Versions prior to 1.9.3, 1.8.8, and 1.7.12 incorrectly used the primary // Versions prior to 1.9.3, 1.8.8, and 1.7.12 incorrectly used the primary
// rootCA's subjectKeyID here instead of the intermediate. For // rootCA's subjectKeyID here instead of the intermediate. For
// provider=consul this didn't matter since there are no intermediates in // provider=consul this didn't matter since there are no intermediates in
// the primaryDC, but for vault it does matter. // the primaryDC, but for vault it does matter.
expectedSigningKeyID := connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId) expectedSigningKeyID := connect.EncodeSigningKeyID(intermediateCert.SubjectKeyId)
needsSigningKeyUpdate := (rootCA.SigningKeyID != expectedSigningKeyID) if rootCA.SigningKeyID != expectedSigningKeyID {
c.logger.Info("Correcting stored CARoot values",
"previous-signing-key", rootCA.SigningKeyID, "updated-signing-key", expectedSigningKeyID)
rootCA.SigningKeyID = expectedSigningKeyID
rootUpdateRequired = true
}
// Add the local leaf signing cert to the rootCA struct. This handles both
// upgrades of existing state, and new rootCA.
if c.getLeafSigningCertFromRoot(rootCA) != interPEM {
rootCA.IntermediateCerts = append(rootCA.IntermediateCerts, interPEM)
rootUpdateRequired = true
}
// Check if the CA root is already initialized and exit if it is, // Check if the CA root is already initialized and exit if it is,
// adding on any existing intermediate certs since they aren't directly // adding on any existing intermediate certs since they aren't directly
@ -534,26 +548,21 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf
if err != nil { if err != nil {
return err return err
} }
if activeRoot != nil && needsSigningKeyUpdate { if activeRoot != nil && !rootUpdateRequired {
c.logger.Info("Correcting stored SigningKeyID value", "previous", rootCA.SigningKeyID, "updated", expectedSigningKeyID)
} else if activeRoot != nil && !needsSigningKeyUpdate {
// This state shouldn't be possible to get into because we update the root and // This state shouldn't be possible to get into because we update the root and
// CA config in the same FSM operation. // CA config in the same FSM operation.
if activeRoot.ID != rootCA.ID { if activeRoot.ID != rootCA.ID {
return fmt.Errorf("stored CA root %q is not the active root (%s)", rootCA.ID, activeRoot.ID) return fmt.Errorf("stored CA root %q is not the active root (%s)", rootCA.ID, activeRoot.ID)
} }
// TODO: why doesn't this c.setCAProvider(provider, activeRoot) ?
rootCA.IntermediateCerts = activeRoot.IntermediateCerts rootCA.IntermediateCerts = activeRoot.IntermediateCerts
c.setCAProvider(provider, rootCA) c.setCAProvider(provider, rootCA)
c.logger.Info("initialized primary datacenter CA from existing CARoot with provider", "provider", conf.Provider)
return nil return nil
} }
if needsSigningKeyUpdate {
rootCA.SigningKeyID = expectedSigningKeyID
}
// Get the highest index // Get the highest index
idx, _, err := state.CARoots(nil) idx, _, err := state.CARoots(nil)
if err != nil { if err != nil {
@ -577,6 +586,22 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf
return nil return nil
} }
// getLeafSigningCertFromRoot returns the PEM encoded certificate that should be used to
// sign leaf certificates in the local datacenter. The SubjectKeyId of the
// returned cert should always match the SigningKeyID of the CARoot.
//
// TODO: fix the data model so that we don't need this complicated lookup to
// find the leaf signing cert. See github.com/hashicorp/consul/issues/11347.
func (c *CAManager) getLeafSigningCertFromRoot(root *structs.CARoot) string {
if !c.isIntermediateUsedToSignLeaf() {
return root.RootCert
}
if len(root.IntermediateCerts) == 0 {
return ""
}
return root.IntermediateCerts[len(root.IntermediateCerts)-1]
}
// secondaryInitializeIntermediateCA runs the routine for generating an intermediate CA CSR and getting // secondaryInitializeIntermediateCA runs the routine for generating an intermediate CA CSR and getting
// it signed by the primary DC if the root CA of the primary DC has changed since the last // it signed by the primary DC if the root CA of the primary DC has changed since the last
// intermediate. It should only be called while the state lock is held by setting the state // intermediate. It should only be called while the state lock is held by setting the state
@ -1133,10 +1158,8 @@ func (c *CAManager) RenewIntermediate(ctx context.Context, isPrimary bool) error
// If this is the primary, check if this is a provider that uses an intermediate cert. If // If this is the primary, check if this is a provider that uses an intermediate cert. If
// it isn't, we don't need to check for a renewal. // it isn't, we don't need to check for a renewal.
if isPrimary { if isPrimary && !primaryUsesIntermediate(provider) {
if _, ok := provider.(ca.PrimaryUsesIntermediate); !ok { return nil
return nil
}
} }
activeIntermediate, err := provider.ActiveIntermediate() activeIntermediate, err := provider.ActiveIntermediate()
@ -1520,3 +1543,16 @@ func (c *CAManager) checkExpired(pem string) error {
} }
return nil return nil
} }
func primaryUsesIntermediate(provider ca.Provider) bool {
_, ok := provider.(ca.PrimaryUsesIntermediate)
return ok
}
func (c *CAManager) isIntermediateUsedToSignLeaf() bool {
if c.serverConf.Datacenter != c.serverConf.PrimaryDatacenter {
return true
}
provider, _ := c.getCAProvider()
return primaryUsesIntermediate(provider)
}

View File

@ -396,23 +396,34 @@ func TestLeader_Vault_PrimaryCA_IntermediateRenew(t *testing.T) {
provider, _ := getCAProviderWithLock(s1) provider, _ := getCAProviderWithLock(s1)
intermediatePEM, err := provider.ActiveIntermediate() intermediatePEM, err := provider.ActiveIntermediate()
require.NoError(err) require.NoError(err)
_, err = connect.ParseCert(intermediatePEM) intermediateCert, err := connect.ParseCert(intermediatePEM)
require.NoError(err) require.NoError(err)
// Check that the state store has the correct intermediate
store := s1.caManager.delegate.State()
_, activeRoot, err := store.CARootActive(nil)
require.NoError(err)
require.Equal(intermediatePEM, s1.caManager.getLeafSigningCertFromRoot(activeRoot))
require.Equal(connect.HexString(intermediateCert.SubjectKeyId), activeRoot.SigningKeyID)
// Wait for dc1's intermediate to be refreshed. // Wait for dc1's intermediate to be refreshed.
// It is possible that test fails when the blocking query doesn't return. // It is possible that test fails when the blocking query doesn't return.
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
provider, _ = getCAProviderWithLock(s1) provider, _ = getCAProviderWithLock(s1)
newIntermediatePEM, err := provider.ActiveIntermediate() newIntermediatePEM, err := provider.ActiveIntermediate()
r.Check(err) r.Check(err)
_, err = connect.ParseCert(intermediatePEM)
r.Check(err)
if newIntermediatePEM == intermediatePEM { if newIntermediatePEM == intermediatePEM {
r.Fatal("not a renewed intermediate") r.Fatal("not a renewed intermediate")
} }
intermediateCert, err = connect.ParseCert(newIntermediatePEM)
r.Check(err)
intermediatePEM = newIntermediatePEM intermediatePEM = newIntermediatePEM
}) })
_, activeRoot, err = store.CARootActive(nil)
require.NoError(err) require.NoError(err)
require.Equal(intermediatePEM, s1.caManager.getLeafSigningCertFromRoot(activeRoot))
require.Equal(connect.HexString(intermediateCert.SubjectKeyId), activeRoot.SigningKeyID)
// Get the root from dc1 and validate a chain of: // Get the root from dc1 and validate a chain of:
// dc1 leaf -> dc1 intermediate -> dc1 root // dc1 leaf -> dc1 intermediate -> dc1 root
@ -439,6 +450,8 @@ func TestLeader_Vault_PrimaryCA_IntermediateRenew(t *testing.T) {
// Check that the leaf signed by the new intermediate can be verified using the // Check that the leaf signed by the new intermediate can be verified using the
// returned cert chain (signed intermediate + remote root). // returned cert chain (signed intermediate + remote root).
intermediatePool := x509.NewCertPool() intermediatePool := x509.NewCertPool()
// TODO: do not explicitly add the intermediatePEM, we should have it available
// from leafPEM. Use connect.ParseLeafCerts to do the right thing.
intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM)) intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM))
rootPool := x509.NewCertPool() rootPool := x509.NewCertPool()
rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert)) rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert))
@ -515,10 +528,10 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
secondaryProvider, _ := getCAProviderWithLock(s2) secondaryProvider, _ := getCAProviderWithLock(s2)
intermediatePEM, err := secondaryProvider.ActiveIntermediate() intermediatePEM, err := secondaryProvider.ActiveIntermediate()
require.NoError(err) require.NoError(err)
cert, err := connect.ParseCert(intermediatePEM) intermediateCert, err := connect.ParseCert(intermediatePEM)
require.NoError(err) require.NoError(err)
currentCertSerialNumber := cert.SerialNumber currentCertSerialNumber := intermediateCert.SerialNumber
currentCertAuthorityKeyId := cert.AuthorityKeyId currentCertAuthorityKeyId := intermediateCert.AuthorityKeyId
// Capture the current root // Capture the current root
var originalRoot *structs.CARoot var originalRoot *structs.CARoot
@ -532,6 +545,12 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
waitForActiveCARoot(t, s1, originalRoot) waitForActiveCARoot(t, s1, originalRoot)
waitForActiveCARoot(t, s2, originalRoot) waitForActiveCARoot(t, s2, originalRoot)
store := s2.fsm.State()
_, activeRoot, err := store.CARootActive(nil)
require.NoError(err)
require.Equal(intermediatePEM, s2.caManager.getLeafSigningCertFromRoot(activeRoot))
require.Equal(connect.HexString(intermediateCert.SubjectKeyId), activeRoot.SigningKeyID)
// Wait for dc2's intermediate to be refreshed. // Wait for dc2's intermediate to be refreshed.
// It is possible that test fails when the blocking query doesn't return. // It is possible that test fails when the blocking query doesn't return.
// When https://github.com/hashicorp/consul/pull/3777 is merged // When https://github.com/hashicorp/consul/pull/3777 is merged
@ -548,8 +567,13 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
currentCertAuthorityKeyId = cert.AuthorityKeyId currentCertAuthorityKeyId = cert.AuthorityKeyId
r.Fatal("not a renewed intermediate") r.Fatal("not a renewed intermediate")
} }
intermediateCert = cert
}) })
_, activeRoot, err = store.CARootActive(nil)
require.NoError(err) require.NoError(err)
require.Equal(intermediatePEM, s2.caManager.getLeafSigningCertFromRoot(activeRoot))
require.Equal(connect.HexString(intermediateCert.SubjectKeyId), activeRoot.SigningKeyID)
// Get the root from dc1 and validate a chain of: // Get the root from dc1 and validate a chain of:
// dc2 leaf -> dc2 intermediate -> dc1 root // dc2 leaf -> dc2 intermediate -> dc1 root
@ -570,17 +594,19 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
leafPEM, err := secondaryProvider.Sign(leafCsr) leafPEM, err := secondaryProvider.Sign(leafCsr)
require.NoError(err) require.NoError(err)
cert, err = connect.ParseCert(leafPEM) intermediateCert, err = connect.ParseCert(leafPEM)
require.NoError(err) require.NoError(err)
// Check that the leaf signed by the new intermediate can be verified using the // Check that the leaf signed by the new intermediate can be verified using the
// returned cert chain (signed intermediate + remote root). // returned cert chain (signed intermediate + remote root).
intermediatePool := x509.NewCertPool() intermediatePool := x509.NewCertPool()
// TODO: do not explicitly add the intermediatePEM, we should have it available
// from leafPEM. Use connect.ParseLeafCerts to do the right thing.
intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM)) intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM))
rootPool := x509.NewCertPool() rootPool := x509.NewCertPool()
rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert)) rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert))
_, err = cert.Verify(x509.VerifyOptions{ _, err = intermediateCert.Verify(x509.VerifyOptions{
Intermediates: intermediatePool, Intermediates: intermediatePool,
Roots: rootPool, Roots: rootPool,
}) })

View File

@ -86,11 +86,20 @@ type CARoot struct {
NotBefore time.Time NotBefore time.Time
NotAfter time.Time NotAfter time.Time
// RootCert is the PEM-encoded public certificate. // RootCert is the PEM-encoded public certificate for the root CA. The
// certificate is the same for all federated clusters.
RootCert string RootCert string
// IntermediateCerts is a list of PEM-encoded intermediate certs to // IntermediateCerts is a list of PEM-encoded intermediate certs to
// attach to any leaf certs signed by this CA. // attach to any leaf certs signed by this CA. The list may include a
// certificate cross-signed by an old root CA, any subordinate CAs below the
// root CA, and the intermediate CA used to sign leaf certificates in the
// local Datacenter.
//
// If the provider which created this root uses an intermediate to sign
// leaf certificates (Vault provider), or this is a secondary Datacenter then
// the intermediate used to sign leaf certificates will be the last in the
// list.
IntermediateCerts []string IntermediateCerts []string
// SigningCert is the PEM-encoded signing certificate and SigningKey // SigningCert is the PEM-encoded signing certificate and SigningKey