Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ca: fix storing the leaf signing cert with Vault provider #11671

Merged
merged 3 commits into from Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/11671.txt
@@ -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.
```
62 changes: 49 additions & 13 deletions agent/consul/leader_connect_ca.go
Expand Up @@ -520,12 +520,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
// rootCA's subjectKeyID here instead of the intermediate. For
// provider=consul this didn't matter since there are no intermediates in
// the primaryDC, but for vault it does matter.
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,
// adding on any existing intermediate certs since they aren't directly
Expand All @@ -537,26 +551,21 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf
if err != nil {
return err
}
if activeRoot != nil && needsSigningKeyUpdate {
c.logger.Info("Correcting stored SigningKeyID value", "previous", rootCA.SigningKeyID, "updated", expectedSigningKeyID)

} else if activeRoot != nil && !needsSigningKeyUpdate {
if activeRoot != nil && !rootUpdateRequired {
// This state shouldn't be possible to get into because we update the root and
// CA config in the same FSM operation.
if activeRoot.ID != rootCA.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) ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

rootCA.IntermediateCerts = activeRoot.IntermediateCerts
c.setCAProvider(provider, rootCA)

c.logger.Info("initialized primary datacenter CA from existing CARoot with provider", "provider", conf.Provider)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unrelated, but something I noticed while working on this.

I'm pretty sure that previously we would not log anything when a server initializes itself using an existing stored CARoot. I guess this would happen if the raft leader election on any cluster that already had a root setup.

I made the log message slightly different, so that it's clear this initialization was from an existing CARoot.

return nil
}

if needsSigningKeyUpdate {
rootCA.SigningKeyID = expectedSigningKeyID
}
Comment on lines -556 to -558
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line was moved up to the place where we set needsSigningKeyUpdate, to keep the logic together.


// Get the highest index
idx, _, err := state.CARoots(nil)
if err != nil {
Expand Down Expand Up @@ -584,6 +593,22 @@ func (c *CAManager) primaryInitialize(provider ca.Provider, conf *structs.CAConf
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
// 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
Expand Down Expand Up @@ -1149,10 +1174,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
// it isn't, we don't need to check for a renewal.
if isPrimary {
if _, ok := provider.(ca.PrimaryUsesIntermediate); !ok {
return nil
}
if isPrimary && !primaryUsesIntermediate(provider) {
return nil
}

activeIntermediate, err := provider.ActiveIntermediate()
Expand Down Expand Up @@ -1536,3 +1559,16 @@ func (c *CAManager) checkExpired(pem string) error {
}
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)
}
42 changes: 34 additions & 8 deletions agent/consul/leader_connect_test.go
Expand Up @@ -396,23 +396,34 @@ func TestLeader_Vault_PrimaryCA_IntermediateRenew(t *testing.T) {
provider, _ := getCAProviderWithLock(s1)
intermediatePEM, err := provider.ActiveIntermediate()
require.NoError(err)
_, err = connect.ParseCert(intermediatePEM)
intermediateCert, err := connect.ParseCert(intermediatePEM)
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.
// It is possible that test fails when the blocking query doesn't return.
retry.Run(t, func(r *retry.R) {
provider, _ = getCAProviderWithLock(s1)
newIntermediatePEM, err := provider.ActiveIntermediate()
r.Check(err)
_, err = connect.ParseCert(intermediatePEM)
r.Check(err)
if newIntermediatePEM == intermediatePEM {
r.Fatal("not a renewed intermediate")
}
intermediateCert, err = connect.ParseCert(newIntermediatePEM)
r.Check(err)
acpana marked this conversation as resolved.
Show resolved Hide resolved
intermediatePEM = newIntermediatePEM
})

_, activeRoot, err = store.CARootActive(nil)
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:
// dc1 leaf -> dc1 intermediate -> dc1 root
Expand All @@ -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
// returned cert chain (signed intermediate + remote root).
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))
rootPool := x509.NewCertPool()
rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert))
Expand Down Expand Up @@ -515,10 +528,10 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
secondaryProvider, _ := getCAProviderWithLock(s2)
intermediatePEM, err := secondaryProvider.ActiveIntermediate()
require.NoError(err)
cert, err := connect.ParseCert(intermediatePEM)
intermediateCert, err := connect.ParseCert(intermediatePEM)
require.NoError(err)
currentCertSerialNumber := cert.SerialNumber
currentCertAuthorityKeyId := cert.AuthorityKeyId
currentCertSerialNumber := intermediateCert.SerialNumber
currentCertAuthorityKeyId := intermediateCert.AuthorityKeyId

// Capture the current root
var originalRoot *structs.CARoot
Expand All @@ -532,6 +545,12 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
waitForActiveCARoot(t, s1, 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.
// It is possible that test fails when the blocking query doesn't return.
// When https://github.com/hashicorp/consul/pull/3777 is merged
Expand All @@ -548,8 +567,13 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
currentCertAuthorityKeyId = cert.AuthorityKeyId
r.Fatal("not a renewed intermediate")
}
intermediateCert = cert
})

_, activeRoot, err = store.CARootActive(nil)
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:
// dc2 leaf -> dc2 intermediate -> dc1 root
Expand All @@ -570,17 +594,19 @@ func TestLeader_SecondaryCA_IntermediateRenew(t *testing.T) {
leafPEM, err := secondaryProvider.Sign(leafCsr)
require.NoError(err)

cert, err = connect.ParseCert(leafPEM)
intermediateCert, err = connect.ParseCert(leafPEM)
require.NoError(err)

// Check that the leaf signed by the new intermediate can be verified using the
// returned cert chain (signed intermediate + remote root).
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))
rootPool := x509.NewCertPool()
rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert))

_, err = cert.Verify(x509.VerifyOptions{
_, err = intermediateCert.Verify(x509.VerifyOptions{
Intermediates: intermediatePool,
Roots: rootPool,
})
Expand Down
13 changes: 11 additions & 2 deletions agent/structs/connect_ca.go
Expand Up @@ -86,11 +86,20 @@ type CARoot struct {
NotBefore 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

// 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

// SigningCert is the PEM-encoded signing certificate and SigningKey
Expand Down