diff --git a/agent/cache-types/connect_ca_leaf.go b/agent/cache-types/connect_ca_leaf.go index 137a3c4003..5e8c91f956 100644 --- a/agent/cache-types/connect_ca_leaf.go +++ b/agent/cache-types/connect_ca_leaf.go @@ -525,6 +525,17 @@ func (c *ConnectCALeaf) generateNewLeaf(req *ConnectCALeafRequest, } // Create a new private key + + // TODO: for now we always generate EC keys on clients regardless of the key + // type being used by the active CA. This is fine and allowed in TLS1.2 and + // signing EC CSRs with an RSA key is supported by all current CA providers so + // it's OK. IFF we ever need to support a CA provider that refuses to sign a + // CSR with a different signature algorithm, or if we have compatibility + // issues with external PKI systems that require EC certs be signed with ECDSA + // from the CA (this was required in TLS1.1 but not in 1.2) then we can + // instead intelligently pick the key type we generate here based on the key + // type of the active signing CA. We already have that loaded since we need + // the trust domain. pk, pkPEM, err := connect.GeneratePrivateKey() if err != nil { return result, err diff --git a/agent/connect/ca/provider_consul.go b/agent/connect/ca/provider_consul.go index 74b2c9f60e..1f5508a1f0 100644 --- a/agent/connect/ca/provider_consul.go +++ b/agent/connect/ca/provider_consul.go @@ -341,11 +341,14 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) { // cluster. A minute is more than enough for typical DC clock drift. effectiveNow := time.Now().Add(-1 * time.Minute) template := x509.Certificate{ - SerialNumber: sn, - Subject: pkix.Name{CommonName: subject}, - URIs: csr.URIs, - Signature: csr.Signature, - SignatureAlgorithm: csr.SignatureAlgorithm, + SerialNumber: sn, + Subject: pkix.Name{CommonName: subject}, + URIs: csr.URIs, + Signature: csr.Signature, + // We use the correct signature algorithm for the CA key we are signing with + // regardless of the algorithm used to sign the CSR signature above since + // the leaf might use a different key type. + SignatureAlgorithm: connect.SigAlgoForKey(signer), PublicKeyAlgorithm: csr.PublicKeyAlgorithm, PublicKey: csr.PublicKey, BasicConstraintsValid: true, @@ -413,7 +416,7 @@ func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string, if err != nil { return "", err } - subjectKeyId, err := connect.KeyId(csr.PublicKey) + subjectKeyID, err := connect.KeyId(csr.PublicKey) if err != nil { return "", err } @@ -436,7 +439,7 @@ func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string, Subject: csr.Subject, URIs: csr.URIs, Signature: csr.Signature, - SignatureAlgorithm: csr.SignatureAlgorithm, + SignatureAlgorithm: connect.SigAlgoForKey(signer), PublicKeyAlgorithm: csr.PublicKeyAlgorithm, PublicKey: csr.PublicKey, BasicConstraintsValid: true, @@ -447,7 +450,7 @@ func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string, MaxPathLenZero: true, NotAfter: effectiveNow.AddDate(1, 0, 0), NotBefore: effectiveNow, - SubjectKeyId: subjectKeyId, + SubjectKeyId: subjectKeyID, } // Create the certificate, PEM encode it and return that value. diff --git a/agent/connect/ca/provider_consul_test.go b/agent/connect/ca/provider_consul_test.go index b6f1d4871e..eac076c0dc 100644 --- a/agent/connect/ca/provider_consul_test.go +++ b/agent/connect/ca/provider_consul_test.go @@ -62,7 +62,7 @@ func newMockDelegate(t *testing.T, conf *structs.CAConfiguration) *consulCAMockD func testConsulCAConfig() *structs.CAConfiguration { return &structs.CAConfiguration{ - ClusterID: "asdf", + ClusterID: connect.TestClusterID, Provider: "consul", Config: map[string]interface{}{ // Tests duration parsing after msgpack type mangling during raft apply. @@ -128,120 +128,138 @@ func TestConsulCAProvider_Bootstrap_WithCert(t *testing.T) { func TestConsulCAProvider_SignLeaf(t *testing.T) { t.Parallel() - require := require.New(t) - conf := testConsulCAConfig() - conf.Config["LeafCertTTL"] = "1h" - delegate := newMockDelegate(t, conf) + for _, tc := range KeyTestCases { + tc := tc + t.Run(tc.Desc, func(t *testing.T) { + require := require.New(t) + conf := testConsulCAConfig() + conf.Config["LeafCertTTL"] = "1h" + conf.Config["PrivateKeyType"] = tc.KeyType + conf.Config["PrivateKeyBits"] = tc.KeyBits + delegate := newMockDelegate(t, conf) - provider := &ConsulProvider{Delegate: delegate} - require.NoError(provider.Configure(conf.ClusterID, true, conf.Config)) - require.NoError(provider.GenerateRoot()) + provider := &ConsulProvider{Delegate: delegate} + require.NoError(provider.Configure(conf.ClusterID, true, conf.Config)) + require.NoError(provider.GenerateRoot()) - spiffeService := &connect.SpiffeIDService{ - Host: "node1", - Namespace: "default", - Datacenter: "dc1", - Service: "foo", + spiffeService := &connect.SpiffeIDService{ + Host: "node1", + Namespace: "default", + Datacenter: "dc1", + Service: "foo", + } + + // Generate a leaf cert for the service. + { + raw, _ := connect.TestCSR(t, spiffeService) + + csr, err := connect.ParseCSR(raw) + require.NoError(err) + + cert, err := provider.Sign(csr) + require.NoError(err) + + parsed, err := connect.ParseCert(cert) + require.NoError(err) + require.Equal(parsed.URIs[0], spiffeService.URI()) + require.Equal(parsed.Subject.CommonName, "foo") + require.Equal(uint64(2), parsed.SerialNumber.Uint64()) + requireNotEncoded(t, parsed.SubjectKeyId) + requireNotEncoded(t, parsed.AuthorityKeyId) + + // Ensure the cert is valid now and expires within the correct limit. + now := time.Now() + require.True(parsed.NotAfter.Sub(now) < time.Hour) + require.True(parsed.NotBefore.Before(now)) + } + + // Generate a new cert for another service and make sure + // the serial number is incremented. + spiffeService.Service = "bar" + { + raw, _ := connect.TestCSR(t, spiffeService) + + csr, err := connect.ParseCSR(raw) + require.NoError(err) + + cert, err := provider.Sign(csr) + require.NoError(err) + + parsed, err := connect.ParseCert(cert) + require.NoError(err) + require.Equal(parsed.URIs[0], spiffeService.URI()) + require.Equal(parsed.Subject.CommonName, "bar") + require.Equal(parsed.SerialNumber.Uint64(), uint64(2)) + requireNotEncoded(t, parsed.SubjectKeyId) + requireNotEncoded(t, parsed.AuthorityKeyId) + + // Ensure the cert is valid now and expires within the correct limit. + require.True(time.Until(parsed.NotAfter) < 3*24*time.Hour) + require.True(parsed.NotBefore.Before(time.Now())) + } + + spiffeAgent := &connect.SpiffeIDAgent{ + Host: "node1", + Datacenter: "dc1", + Agent: "uuid", + } + // Generate a leaf cert for an agent. + { + raw, _ := connect.TestCSR(t, spiffeAgent) + + csr, err := connect.ParseCSR(raw) + require.NoError(err) + + cert, err := provider.Sign(csr) + require.NoError(err) + + parsed, err := connect.ParseCert(cert) + require.NoError(err) + require.Equal(spiffeAgent.URI(), parsed.URIs[0]) + require.Equal("uuid", parsed.Subject.CommonName) + require.Equal(uint64(2), parsed.SerialNumber.Uint64()) + requireNotEncoded(t, parsed.SubjectKeyId) + requireNotEncoded(t, parsed.AuthorityKeyId) + + // Ensure the cert is valid now and expires within the correct limit. + now := time.Now() + require.True(parsed.NotAfter.Sub(now) < time.Hour) + require.True(parsed.NotBefore.Before(now)) + } + }) } - - // Generate a leaf cert for the service. - { - raw, _ := connect.TestCSR(t, spiffeService) - - csr, err := connect.ParseCSR(raw) - require.NoError(err) - - cert, err := provider.Sign(csr) - require.NoError(err) - - parsed, err := connect.ParseCert(cert) - require.NoError(err) - require.Equal(parsed.URIs[0], spiffeService.URI()) - require.Equal(parsed.Subject.CommonName, "foo") - require.Equal(uint64(2), parsed.SerialNumber.Uint64()) - requireNotEncoded(t, parsed.SubjectKeyId) - requireNotEncoded(t, parsed.AuthorityKeyId) - - // Ensure the cert is valid now and expires within the correct limit. - now := time.Now() - require.True(parsed.NotAfter.Sub(now) < time.Hour) - require.True(parsed.NotBefore.Before(now)) - } - - // Generate a new cert for another service and make sure - // the serial number is incremented. - spiffeService.Service = "bar" - { - raw, _ := connect.TestCSR(t, spiffeService) - - csr, err := connect.ParseCSR(raw) - require.NoError(err) - - cert, err := provider.Sign(csr) - require.NoError(err) - - parsed, err := connect.ParseCert(cert) - require.NoError(err) - require.Equal(parsed.URIs[0], spiffeService.URI()) - require.Equal(parsed.Subject.CommonName, "bar") - require.Equal(parsed.SerialNumber.Uint64(), uint64(2)) - requireNotEncoded(t, parsed.SubjectKeyId) - requireNotEncoded(t, parsed.AuthorityKeyId) - - // Ensure the cert is valid now and expires within the correct limit. - require.True(time.Until(parsed.NotAfter) < 3*24*time.Hour) - require.True(parsed.NotBefore.Before(time.Now())) - } - - spiffeAgent := &connect.SpiffeIDAgent{ - Host: "node1", - Datacenter: "dc1", - Agent: "uuid", - } - // Generate a leaf cert for an agent. - { - raw, _ := connect.TestCSR(t, spiffeAgent) - - csr, err := connect.ParseCSR(raw) - require.NoError(err) - - cert, err := provider.Sign(csr) - require.NoError(err) - - parsed, err := connect.ParseCert(cert) - require.NoError(err) - require.Equal(spiffeAgent.URI(), parsed.URIs[0]) - require.Equal("uuid", parsed.Subject.CommonName) - require.Equal(uint64(2), parsed.SerialNumber.Uint64()) - requireNotEncoded(t, parsed.SubjectKeyId) - requireNotEncoded(t, parsed.AuthorityKeyId) - - // Ensure the cert is valid now and expires within the correct limit. - now := time.Now() - require.True(parsed.NotAfter.Sub(now) < time.Hour) - require.True(parsed.NotBefore.Before(now)) - } - } func TestConsulCAProvider_CrossSignCA(t *testing.T) { t.Parallel() - require := require.New(t) - conf1 := testConsulCAConfig() - delegate1 := newMockDelegate(t, conf1) - provider1 := &ConsulProvider{Delegate: delegate1} - require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config)) - require.NoError(provider1.GenerateRoot()) + tests := CASigningKeyTypeCases() - conf2 := testConsulCAConfig() - conf2.CreateIndex = 10 - delegate2 := newMockDelegate(t, conf2) - provider2 := &ConsulProvider{Delegate: delegate2} - require.NoError(provider2.Configure(conf2.ClusterID, true, conf2.Config)) - require.NoError(provider2.GenerateRoot()) + for _, tc := range tests { + tc := tc + t.Run(tc.Desc, func(t *testing.T) { + require := require.New(t) - testCrossSignProviders(t, provider1, provider2) + conf1 := testConsulCAConfig() + delegate1 := newMockDelegate(t, conf1) + provider1 := &ConsulProvider{Delegate: delegate1} + conf1.Config["PrivateKeyType"] = tc.SigningKeyType + conf1.Config["PrivateKeyBits"] = tc.SigningKeyBits + require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config)) + require.NoError(provider1.GenerateRoot()) + + conf2 := testConsulCAConfig() + conf2.CreateIndex = 10 + delegate2 := newMockDelegate(t, conf2) + provider2 := &ConsulProvider{Delegate: delegate2} + conf2.Config["PrivateKeyType"] = tc.CSRKeyType + conf2.Config["PrivateKeyBits"] = tc.CSRKeyBits + require.NoError(provider2.Configure(conf2.ClusterID, true, conf2.Config)) + require.NoError(provider2.GenerateRoot()) + + testCrossSignProviders(t, provider1, provider2) + }) + } } func testCrossSignProviders(t *testing.T, provider1, provider2 Provider) { @@ -328,21 +346,34 @@ func testCrossSignProviders(t *testing.T, provider1, provider2 Provider) { func TestConsulProvider_SignIntermediate(t *testing.T) { t.Parallel() - require := require.New(t) - conf1 := testConsulCAConfig() - delegate1 := newMockDelegate(t, conf1) - provider1 := &ConsulProvider{Delegate: delegate1} - require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config)) - require.NoError(provider1.GenerateRoot()) + tests := CASigningKeyTypeCases() - conf2 := testConsulCAConfig() - conf2.CreateIndex = 10 - delegate2 := newMockDelegate(t, conf2) - provider2 := &ConsulProvider{Delegate: delegate2} - require.NoError(provider2.Configure(conf2.ClusterID, false, conf2.Config)) + for _, tc := range tests { + tc := tc + t.Run(tc.Desc, func(t *testing.T) { + require := require.New(t) + + conf1 := testConsulCAConfig() + delegate1 := newMockDelegate(t, conf1) + provider1 := &ConsulProvider{Delegate: delegate1} + conf1.Config["PrivateKeyType"] = tc.SigningKeyType + conf1.Config["PrivateKeyBits"] = tc.SigningKeyBits + require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config)) + require.NoError(provider1.GenerateRoot()) + + conf2 := testConsulCAConfig() + conf2.CreateIndex = 10 + delegate2 := newMockDelegate(t, conf2) + provider2 := &ConsulProvider{Delegate: delegate2} + conf2.Config["PrivateKeyType"] = tc.CSRKeyType + conf2.Config["PrivateKeyBits"] = tc.CSRKeyBits + require.NoError(provider2.Configure(conf2.ClusterID, false, conf2.Config)) + + testSignIntermediateCrossDC(t, provider1, provider2) + }) + } - testSignIntermediateCrossDC(t, provider1, provider2) } func testSignIntermediateCrossDC(t *testing.T, provider1, provider2 Provider) { diff --git a/agent/connect/ca/provider_vault_test.go b/agent/connect/ca/provider_vault_test.go index f9be60fe07..fb64adf19c 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -1,6 +1,7 @@ package ca import ( + "crypto/x509" "fmt" "io/ioutil" "os" @@ -83,6 +84,22 @@ func TestVaultCAProvider_Bootstrap(t *testing.T) { } } +func assertCorrectKeyType(t *testing.T, want, certPEM string) { + t.Helper() + + cert, err := connect.ParseCert(certPEM) + require.NoError(t, err) + + switch want { + case "ec": + require.Equal(t, x509.ECDSA, cert.PublicKeyAlgorithm) + case "rsa": + require.Equal(t, x509.RSA, cert.PublicKeyAlgorithm) + default: + t.Fatal("test doesn't support key type") + } +} + func TestVaultCAProvider_SignLeaf(t *testing.T) { t.Parallel() @@ -90,61 +107,82 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) { return } - require := require.New(t) - provider, testVault := testVaultProviderWithConfig(t, true, map[string]interface{}{ - "LeafCertTTL": "1h", - }) - defer testVault.Stop() + for _, tc := range KeyTestCases { + tc := tc + t.Run(tc.Desc, func(t *testing.T) { + require := require.New(t) + provider, testVault := testVaultProviderWithConfig(t, true, map[string]interface{}{ + "LeafCertTTL": "1h", + "PrivateKeyType": tc.KeyType, + "PrivateKeyBits": tc.KeyBits, + }) + defer testVault.Stop() - spiffeService := &connect.SpiffeIDService{ - Host: "node1", - Namespace: "default", - Datacenter: "dc1", - Service: "foo", - } + spiffeService := &connect.SpiffeIDService{ + Host: "node1", + Namespace: "default", + Datacenter: "dc1", + Service: "foo", + } - // Generate a leaf cert for the service. - var firstSerial uint64 - { - raw, _ := connect.TestCSR(t, spiffeService) + rootPEM, err := provider.ActiveRoot() + require.NoError(err) + assertCorrectKeyType(t, tc.KeyType, rootPEM) - csr, err := connect.ParseCSR(raw) - require.NoError(err) + intPEM, err := provider.ActiveIntermediate() + require.NoError(err) + assertCorrectKeyType(t, tc.KeyType, intPEM) - cert, err := provider.Sign(csr) - require.NoError(err) + // Generate a leaf cert for the service. + var firstSerial uint64 + { + raw, _ := connect.TestCSR(t, spiffeService) - parsed, err := connect.ParseCert(cert) - require.NoError(err) - require.Equal(parsed.URIs[0], spiffeService.URI()) - firstSerial = parsed.SerialNumber.Uint64() + csr, err := connect.ParseCSR(raw) + require.NoError(err) - // Ensure the cert is valid now and expires within the correct limit. - now := time.Now() - require.True(parsed.NotAfter.Sub(now) < time.Hour) - require.True(parsed.NotBefore.Before(now)) - } + cert, err := provider.Sign(csr) + require.NoError(err) - // Generate a new cert for another service and make sure - // the serial number is unique. - spiffeService.Service = "bar" - { - raw, _ := connect.TestCSR(t, spiffeService) + parsed, err := connect.ParseCert(cert) + require.NoError(err) + require.Equal(parsed.URIs[0], spiffeService.URI()) + firstSerial = parsed.SerialNumber.Uint64() - csr, err := connect.ParseCSR(raw) - require.NoError(err) + // Ensure the cert is valid now and expires within the correct limit. + now := time.Now() + require.True(parsed.NotAfter.Sub(now) < time.Hour) + require.True(parsed.NotBefore.Before(now)) - cert, err := provider.Sign(csr) - require.NoError(err) + // Make sure we can validate the cert as expected. + require.NoError(connect.ValidateLeaf(rootPEM, cert, []string{intPEM})) + } - parsed, err := connect.ParseCert(cert) - require.NoError(err) - require.Equal(parsed.URIs[0], spiffeService.URI()) - require.NotEqual(firstSerial, parsed.SerialNumber.Uint64()) + // Generate a new cert for another service and make sure + // the serial number is unique. + spiffeService.Service = "bar" + { + raw, _ := connect.TestCSR(t, spiffeService) - // Ensure the cert is valid now and expires within the correct limit. - require.True(time.Until(parsed.NotAfter) < time.Hour) - require.True(parsed.NotBefore.Before(time.Now())) + csr, err := connect.ParseCSR(raw) + require.NoError(err) + + cert, err := provider.Sign(csr) + require.NoError(err) + + parsed, err := connect.ParseCert(cert) + require.NoError(err) + require.Equal(parsed.URIs[0], spiffeService.URI()) + require.NotEqual(firstSerial, parsed.SerialNumber.Uint64()) + + // Ensure the cert is valid now and expires within the correct limit. + require.True(time.Until(parsed.NotAfter) < time.Hour) + require.True(parsed.NotBefore.Before(time.Now())) + + // Make sure we can validate the cert as expected. + require.NoError(connect.ValidateLeaf(rootPEM, cert, []string{intPEM})) + } + }) } } @@ -155,13 +193,54 @@ func TestVaultCAProvider_CrossSignCA(t *testing.T) { return } - provider1, testVault1 := testVaultProvider(t) - defer testVault1.Stop() + tests := CASigningKeyTypeCases() - provider2, testVault2 := testVaultProvider(t) - defer testVault2.Stop() + for _, tc := range tests { + tc := tc + t.Run(tc.Desc, func(t *testing.T) { + require := require.New(t) - testCrossSignProviders(t, provider1, provider2) + if tc.SigningKeyType != tc.CSRKeyType { + // See https://github.com/hashicorp/vault/issues/7709 + t.Skip("Vault doesn't support cross-signing different key types yet.") + } + provider1, testVault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{ + "LeafCertTTL": "1h", + "PrivateKeyType": tc.SigningKeyType, + "PrivateKeyBits": tc.SigningKeyBits, + }) + defer testVault1.Stop() + + { + rootPEM, err := provider1.ActiveRoot() + require.NoError(err) + assertCorrectKeyType(t, tc.SigningKeyType, rootPEM) + + intPEM, err := provider1.ActiveIntermediate() + require.NoError(err) + assertCorrectKeyType(t, tc.SigningKeyType, intPEM) + } + + provider2, testVault2 := testVaultProviderWithConfig(t, true, map[string]interface{}{ + "LeafCertTTL": "1h", + "PrivateKeyType": tc.CSRKeyType, + "PrivateKeyBits": tc.CSRKeyBits, + }) + defer testVault2.Stop() + + { + rootPEM, err := provider2.ActiveRoot() + require.NoError(err) + assertCorrectKeyType(t, tc.CSRKeyType, rootPEM) + + intPEM, err := provider2.ActiveIntermediate() + require.NoError(err) + assertCorrectKeyType(t, tc.CSRKeyType, intPEM) + } + + testCrossSignProviders(t, provider1, provider2) + }) + } } func TestVaultProvider_SignIntermediate(t *testing.T) { @@ -171,13 +250,28 @@ func TestVaultProvider_SignIntermediate(t *testing.T) { return } - provider1, testVault1 := testVaultProvider(t) - defer testVault1.Stop() + tests := CASigningKeyTypeCases() - provider2, testVault2 := testVaultProviderWithConfig(t, false, nil) - defer testVault2.Stop() + for _, tc := range tests { + tc := tc + t.Run(tc.Desc, func(t *testing.T) { + provider1, testVault1 := testVaultProviderWithConfig(t, true, map[string]interface{}{ + "LeafCertTTL": "1h", + "PrivateKeyType": tc.SigningKeyType, + "PrivateKeyBits": tc.SigningKeyBits, + }) + defer testVault1.Stop() - testSignIntermediateCrossDC(t, provider1, provider2) + provider2, testVault2 := testVaultProviderWithConfig(t, false, map[string]interface{}{ + "LeafCertTTL": "1h", + "PrivateKeyType": tc.CSRKeyType, + "PrivateKeyBits": tc.CSRKeyBits, + }) + defer testVault2.Stop() + + testSignIntermediateCrossDC(t, provider1, provider2) + }) + } } func TestVaultProvider_SignIntermediateConsul(t *testing.T) { @@ -241,7 +335,7 @@ func testVaultProviderWithConfig(t *testing.T, isRoot bool, rawConf map[string]i provider := &VaultProvider{} - if err := provider.Configure("asdf", isRoot, conf); err != nil { + if err := provider.Configure(connect.TestClusterID, isRoot, conf); err != nil { testVault.Stop() t.Fatalf("err: %v", err) } diff --git a/agent/connect/ca/testing.go b/agent/connect/ca/testing.go new file mode 100644 index 0000000000..996bd4cf51 --- /dev/null +++ b/agent/connect/ca/testing.go @@ -0,0 +1,63 @@ +package ca + +import ( + "fmt" + + "github.com/hashicorp/consul/agent/connect" +) + +// KeyTestCases is a list of the important CA key types that we should test +// against when signing. For now leaf keys are always EC P256 but CA can be EC +// (any NIST curve) or RSA (2048, 4096). Providers must be able to complete all +// signing operations with both types that includes: +// - Sign must be able to sign EC P256 leaf with all these types of CA key +// - CrossSignCA must be able to sign all these types of new CA key with all +// these types of old CA key. +// - SignIntermediate muse bt able to sign all the types of secondary +// intermediate CA key with all these types of primary CA key +var KeyTestCases = []struct { + Desc string + KeyType string + KeyBits int +}{ + { + Desc: "Default Key Type (EC 256)", + KeyType: connect.DefaultPrivateKeyType, + KeyBits: connect.DefaultPrivateKeyBits, + }, + { + Desc: "RSA 2048", + KeyType: "rsa", + KeyBits: 2048, + }, +} + +// CASigningKeyTypes is a struct with params for tests that sign one CA CSR with +// another CA key. +type CASigningKeyTypes struct { + Desc string + SigningKeyType string + SigningKeyBits int + CSRKeyType string + CSRKeyBits int +} + +// CASigningKeyTypeCases returns the cross-product of the important supported CA +// key types for generating table tests for CA signing tests (CrossSignCA and +// SignIntermediate). +func CASigningKeyTypeCases() []CASigningKeyTypes { + cases := make([]CASigningKeyTypes, 0, len(KeyTestCases)*len(KeyTestCases)) + for _, outer := range KeyTestCases { + for _, inner := range KeyTestCases { + cases = append(cases, CASigningKeyTypes{ + Desc: fmt.Sprintf("%s-%d signing %s-%d", outer.KeyType, outer.KeyBits, + inner.KeyType, inner.KeyBits), + SigningKeyType: outer.KeyType, + SigningKeyBits: outer.KeyBits, + CSRKeyType: inner.KeyType, + CSRKeyBits: inner.KeyBits, + }) + } + } + return cases +} diff --git a/agent/connect/csr.go b/agent/connect/csr.go index 61a73ed33a..8fc57840e2 100644 --- a/agent/connect/csr.go +++ b/agent/connect/csr.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto" "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -11,12 +12,41 @@ import ( "net/url" ) +// SigAlgoForKey returns the preferred x509.SignatureAlgorithm for a given key +// based on it's type. If the key type is not supported we return +// ECDSAWithSHA256 on the basis that it will fail anyway and we've already type +// checked keys by the time we call this in general. +func SigAlgoForKey(key crypto.Signer) x509.SignatureAlgorithm { + if _, ok := key.(*rsa.PrivateKey); ok { + return x509.SHA256WithRSA + } + // We default to ECDSA but don't bother detecting invalid key types as we do + // that in lots of other places and it will fail anyway if we try to sign with + // an incompatible type. + return x509.ECDSAWithSHA256 +} + +// SigAlgoForKeyType returns the preferred x509.SignatureAlgorithm for a given +// key type string from configuration or an existing cert. If the key type is +// not supported we return ECDSAWithSHA256 on the basis that it will fail anyway +// and we've already type checked config by the time we call this in general. +func SigAlgoForKeyType(keyType string) x509.SignatureAlgorithm { + switch keyType { + case "rsa": + return x509.SHA256WithRSA + case "ec": + fallthrough + default: + return x509.ECDSAWithSHA256 + } +} + // CreateCSR returns a CSR to sign the given service along with the PEM-encoded // private key for this certificate. func CreateCSR(uri CertURI, privateKey crypto.Signer, extensions ...pkix.Extension) (string, error) { template := &x509.CertificateRequest{ URIs: []*url.URL{uri.URI()}, - SignatureAlgorithm: x509.ECDSAWithSHA256, + SignatureAlgorithm: SigAlgoForKey(privateKey), ExtraExtensions: extensions, } diff --git a/agent/connect/generate_test.go b/agent/connect/generate_test.go index 73d6b8d009..7aad8f8636 100644 --- a/agent/connect/generate_test.go +++ b/agent/connect/generate_test.go @@ -121,7 +121,8 @@ func TestValidateBadConfigs(t *testing.T) { } } -// Tests the ability of a CA to sign a CSR using a different key type. If the key types differ, the test should fail. +// Tests the ability of a CA to sign a CSR using a different key type. This is +// allowed by TLS 1.2 and should succeed in all combinations. func TestSignatureMismatches(t *testing.T) { t.Parallel() r := require.New(t) @@ -135,15 +136,11 @@ func TestSignatureMismatches(t *testing.T) { r.Equal(p1.keyType, ca.PrivateKeyType) r.Equal(p1.keyBits, ca.PrivateKeyBits) certPEM, keyPEM, err := testLeaf(t, "foobar.service.consul", ca, p2.keyType, p2.keyBits) - if p1.keyType == p2.keyType { - r.NoError(err) - _, err := ParseCert(certPEM) - r.NoError(err) - _, err = ParseSigner(keyPEM) - r.NoError(err) - } else { - r.Error(err) - } + r.NoError(err) + _, err = ParseCert(certPEM) + r.NoError(err) + _, err = ParseSigner(keyPEM) + r.NoError(err) }) } } diff --git a/agent/connect/parsing.go b/agent/connect/parsing.go index 26d9e2ea24..5ee6f805fa 100644 --- a/agent/connect/parsing.go +++ b/agent/connect/parsing.go @@ -206,3 +206,16 @@ func IsHexString(input []byte) bool { _, err := hex.DecodeString(s) return err == nil } + +// KeyInfoFromCert returns the key type and key bit length for the key used by +// the certificate. +func KeyInfoFromCert(cert *x509.Certificate) (keyType string, keyBits int, err error) { + switch k := cert.PublicKey.(type) { + case *ecdsa.PublicKey: + return "ec", k.Curve.Params().BitSize, nil + case *rsa.PublicKey: + return "rsa", k.N.BitLen(), nil + default: + return "", 0, fmt.Errorf("unsupported key type") + } +} diff --git a/agent/connect/testing_ca.go b/agent/connect/testing_ca.go index a2b447e0b0..1e2b563c66 100644 --- a/agent/connect/testing_ca.go +++ b/agent/connect/testing_ca.go @@ -27,6 +27,35 @@ const TestClusterID = "11111111-2222-3333-4444-555555555555" // unique names for the CA certs. var testCACounter uint64 +// ValidateLeaf is a convenience helper that returns an error if the certificate +// provided in leadPEM does not validate against the CAs provided. If there is +// an intermediate CA then it's cert must be in caPEMs as well as the root. +func ValidateLeaf(caPEM string, leafPEM string, intermediatePEMs []string) error { + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM([]byte(caPEM)) + if !ok { + return fmt.Errorf("Failed to add root CA") + } + + intermediates := x509.NewCertPool() + for idx, ca := range intermediatePEMs { + ok := intermediates.AppendCertsFromPEM([]byte(ca)) + if !ok { + return fmt.Errorf("Failed to add intermediate CA at index %d to pool", idx) + } + } + + leaf, err := ParseCert(leafPEM) + if err != nil { + return err + } + _, err = leaf.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + }) + return err +} + func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *structs.CARoot { var result structs.CARoot result.Active = true @@ -36,8 +65,6 @@ func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *struc signer, keyPEM := testPrivateKey(t, keyType, keyBits) result.SigningKey = keyPEM result.SigningKeyID = EncodeSigningKeyID(testKeyID(t, signer.Public())) - result.PrivateKeyType = keyType - result.PrivateKeyBits = keyBits // The serial number for the cert sn, err := testSerialNumber() @@ -83,6 +110,8 @@ func testCA(t testing.T, xc *structs.CARoot, keyType string, keyBits int) *struc result.SerialNumber = uint64(sn.Int64()) result.NotBefore = template.NotBefore.UTC() result.NotAfter = template.NotAfter.UTC() + result.PrivateKeyType = keyType + result.PrivateKeyBits = keyBits // If there is a prior CA to cross-sign with, then we need to create that // and set it as the signing cert. @@ -174,9 +203,9 @@ func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string, return "", "", fmt.Errorf("failed to generate private key: %s", err) } - sigAlgo := x509.ECDSAWithSHA256 - if keyType == "rsa" { - sigAlgo = x509.SHA256WithRSA + rootKeyType, _, err := KeyInfoFromCert(caCert) + if err != nil { + return "", "", fmt.Errorf("error getting CA key type: %s", err) } // Cert template for generation @@ -184,7 +213,7 @@ func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string, SerialNumber: sn, Subject: pkix.Name{CommonName: service}, URIs: []*url.URL{spiffeId.URI()}, - SignatureAlgorithm: sigAlgo, + SignatureAlgorithm: SigAlgoForKeyType(rootKeyType), BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement | @@ -218,7 +247,12 @@ func testLeaf(t testing.T, service string, root *structs.CARoot, keyType string, // TestLeaf returns a valid leaf certificate and it's private key for the named // service with the given CA Root. func TestLeaf(t testing.T, service string, root *structs.CARoot) (string, string) { - certPEM, keyPEM, err := testLeaf(t, service, root, root.PrivateKeyType, root.PrivateKeyBits) + // Currently we only support EC leaf keys and certs even if the CA is using + // RSA. We might allow Leafs to follow the signing CA key type later if we + // need to for compatibility sake but this is allowed by TLS 1.2 and works with + // both openssl verify (which we use as a sanity check in our tests of this + // package) and Go's TLS verification. + certPEM, keyPEM, err := testLeaf(t, service, root, DefaultPrivateKeyType, DefaultPrivateKeyBits) if err != nil { t.Fatalf(err.Error()) } @@ -325,8 +359,6 @@ func testCAConfigSet(t testing.T, a TestAgentRPC, "PrivateKey": ca.SigningKey, "RootCert": ca.RootCert, "RotationPeriod": 180 * 24 * time.Hour, - "PrivateKeyType": ca.PrivateKeyType, - "PrivateKeyBits": ca.PrivateKeyBits, }, } args := &structs.CARequest{ diff --git a/agent/connect/testing_ca_test.go b/agent/connect/testing_ca_test.go index e2414b1010..4fdf7ccc1a 100644 --- a/agent/connect/testing_ca_test.go +++ b/agent/connect/testing_ca_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // hasOpenSSL is used to determine if the openssl CLI exists for unit tests. @@ -35,7 +36,7 @@ func testCAAndLeaf(t *testing.T, keyType string, keyBits int) { return } - assert := assert.New(t) + require := require.New(t) // Create the certs ca := TestCAWithKeyType(t, nil, keyType, keyBits) @@ -43,12 +44,12 @@ func testCAAndLeaf(t *testing.T, keyType string, keyBits int) { // Create a temporary directory for storing the certs td, err := ioutil.TempDir("", "consul") - assert.Nil(err) + require.NoError(err) defer os.RemoveAll(td) // Write the cert - assert.Nil(ioutil.WriteFile(filepath.Join(td, "ca.pem"), []byte(ca.RootCert), 0644)) - assert.Nil(ioutil.WriteFile(filepath.Join(td, "leaf.pem"), []byte(leaf), 0644)) + require.NoError(ioutil.WriteFile(filepath.Join(td, "ca.pem"), []byte(ca.RootCert), 0644)) + require.NoError(ioutil.WriteFile(filepath.Join(td, "leaf.pem"), []byte(leaf[:]), 0644)) // Use OpenSSL to verify so we have an external, known-working process // that can verify this outside of our own implementations. @@ -56,8 +57,11 @@ func testCAAndLeaf(t *testing.T, keyType string, keyBits int) { "openssl", "verify", "-verbose", "-CAfile", "ca.pem", "leaf.pem") cmd.Dir = td output, err := cmd.Output() - t.Log(string(output)) - assert.Nil(err) + t.Log("STDOUT:", string(output)) + if ee, ok := err.(*exec.ExitError); ok { + t.Log("STDERR:", string(ee.Stderr)) + } + require.NoError(err) } // Test cross-signing. diff --git a/agent/consul/connect_ca_endpoint_test.go b/agent/consul/connect_ca_endpoint_test.go index d11de89b77..db48086def 100644 --- a/agent/consul/connect_ca_endpoint_test.go +++ b/agent/consul/connect_ca_endpoint_test.go @@ -297,56 +297,73 @@ func TestConnectCAConfig_TriggerRotation(t *testing.T) { func TestConnectCASign(t *testing.T) { t.Parallel() - assert := assert.New(t) - require := require.New(t) - dir1, s1 := testServer(t) - defer os.RemoveAll(dir1) - defer s1.Shutdown() - codec := rpcClient(t, s1) - defer codec.Close() - - testrpc.WaitForLeader(t, s1.RPC, "dc1") - - // Generate a CSR and request signing - spiffeId := connect.TestSpiffeIDService(t, "web") - csr, _ := connect.TestCSR(t, spiffeId) - args := &structs.CASignRequest{ - Datacenter: "dc1", - CSR: csr, - } - var reply structs.IssuedCert - require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply)) - - // Generate a second CSR and request signing - spiffeId2 := connect.TestSpiffeIDService(t, "web2") - csr, _ = connect.TestCSR(t, spiffeId2) - args = &structs.CASignRequest{ - Datacenter: "dc1", - CSR: csr, + tests := []struct { + caKeyType string + caKeyBits int + }{ + { + caKeyType: connect.DefaultPrivateKeyType, + caKeyBits: connect.DefaultPrivateKeyBits, + }, + { + // Ensure that an RSA Keyed CA can sign EC leaves and they validate. + caKeyType: "rsa", + caKeyBits: 2048, + }, } - var reply2 structs.IssuedCert - require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply2)) - require.True(reply2.ModifyIndex > reply.ModifyIndex) + for _, tt := range tests { + t.Run(fmt.Sprintf("%s-%d", tt.caKeyType, tt.caKeyBits), func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + dir1, s1 := testServerWithConfig(t, func(cfg *Config) { + cfg.CAConfig.Config["PrivateKeyType"] = tt.caKeyType + cfg.CAConfig.Config["PrivateKeyBits"] = tt.caKeyBits + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() - // Get the current CA - state := s1.fsm.State() - _, ca, err := state.CARootActive(nil) - require.NoError(err) + testrpc.WaitForLeader(t, s1.RPC, "dc1") - // Verify that the cert is signed by the CA - roots := x509.NewCertPool() - assert.True(roots.AppendCertsFromPEM([]byte(ca.RootCert))) - leaf, err := connect.ParseCert(reply.CertPEM) - require.NoError(err) - _, err = leaf.Verify(x509.VerifyOptions{ - Roots: roots, - }) - require.NoError(err) + // Generate a CSR and request signing + spiffeId := connect.TestSpiffeIDService(t, "web") - // Verify other fields - assert.Equal("web", reply.Service) - assert.Equal(spiffeId.URI().String(), reply.ServiceURI) + // TestCSR will always generate a CSR with an EC key currently. + csr, _ := connect.TestCSR(t, spiffeId) + args := &structs.CASignRequest{ + Datacenter: "dc1", + CSR: csr, + } + var reply structs.IssuedCert + require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply)) + + // Generate a second CSR and request signing + spiffeId2 := connect.TestSpiffeIDService(t, "web2") + csr, _ = connect.TestCSR(t, spiffeId2) + args = &structs.CASignRequest{ + Datacenter: "dc1", + CSR: csr, + } + + var reply2 structs.IssuedCert + require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.Sign", args, &reply2)) + require.True(reply2.ModifyIndex > reply.ModifyIndex) + + // Get the current CA + state := s1.fsm.State() + _, ca, err := state.CARootActive(nil) + require.NoError(err) + + // Verify that the cert is signed by the CA + require.NoError(connect.ValidateLeaf(ca.RootCert, reply.CertPEM, nil)) + + // Verify other fields + assert.Equal("web", reply.Service) + assert.Equal(spiffeId.URI().String(), reply.ServiceURI) + }) + } } // Bench how long Signing RPC takes. This was used to ballpark reasonable diff --git a/agent/consul/leader_connect.go b/agent/consul/leader_connect.go index a604c598d1..5085d70008 100644 --- a/agent/consul/leader_connect.go +++ b/agent/consul/leader_connect.go @@ -81,6 +81,10 @@ func parseCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error) if err != nil { return nil, fmt.Errorf("error parsing root cert: %v", err) } + keyType, keyBits, err := connect.KeyInfoFromCert(rootCert) + if err != nil { + return nil, fmt.Errorf("error extracting root key info: %v", err) + } return &structs.CARoot{ ID: id, Name: fmt.Sprintf("%s CA Root Cert", strings.Title(provider)), @@ -90,6 +94,8 @@ func parseCARoot(pemValue, provider, clusterID string) (*structs.CARoot, error) NotBefore: rootCert.NotBefore, NotAfter: rootCert.NotAfter, RootCert: pemValue, + PrivateKeyType: keyType, + PrivateKeyBits: keyBits, Active: true, }, nil } @@ -224,13 +230,6 @@ func (s *Server) initializeRootCA(provider ca.Provider, conf *structs.CAConfigur return fmt.Errorf("error getting intermediate cert: %v", err) } - commonConfig, err := conf.GetCommonConfig() - if err != nil { - return err - } - rootCA.PrivateKeyType = commonConfig.PrivateKeyType - rootCA.PrivateKeyBits = commonConfig.PrivateKeyBits - // Check if the CA root is already initialized and exit if it is, // adding on any existing intermediate certs since they aren't directly // tied to the provider. diff --git a/agent/consul/leader_connect_test.go b/agent/consul/leader_connect_test.go index a8cb283106..0b68e6f9b4 100644 --- a/agent/consul/leader_connect_test.go +++ b/agent/consul/leader_connect_test.go @@ -2,7 +2,10 @@ package consul import ( "crypto/x509" + "fmt" + "io/ioutil" "os" + "path/filepath" "reflect" "strings" "testing" @@ -24,109 +27,118 @@ import ( func TestLeader_SecondaryCA_Initialize(t *testing.T) { t.Parallel() - masterToken := "8a85f086-dd95-4178-b128-e10902767c5c" - - // Initialize primary as the primary DC - dir1, s1 := testServerWithConfig(t, func(c *Config) { - c.Datacenter = "primary" - c.ACLDatacenter = "primary" - c.Build = "1.6.0" - c.ACLsEnabled = true - c.ACLMasterToken = masterToken - c.ACLDefaultPolicy = "deny" - }) - defer os.RemoveAll(dir1) - defer s1.Shutdown() - - s1.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig) - - testrpc.WaitForLeader(t, s1.RPC, "primary") - - // secondary as a secondary DC - dir2, s2 := testServerWithConfig(t, func(c *Config) { - c.Datacenter = "secondary" - c.ACLDatacenter = "primary" - c.Build = "1.6.0" - c.ACLsEnabled = true - c.ACLDefaultPolicy = "deny" - c.ACLTokenReplication = true - }) - defer os.RemoveAll(dir2) - defer s2.Shutdown() - - s2.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig) - s2.tokens.UpdateReplicationToken(masterToken, token.TokenSourceConfig) - - testrpc.WaitForLeader(t, s2.RPC, "secondary") - - // Create the WAN link - joinWAN(t, s2, s1) - - waitForNewACLs(t, s1) - waitForNewACLs(t, s2) - - // Ensure s2 is authoritative. - waitForNewACLReplication(t, s2, structs.ACLReplicateTokens, 1, 1, 0) - - // Wait until the providers are fully bootstrapped. - var ( - caRoot *structs.CARoot - secondaryProvider ca.Provider - intermediatePEM string - err error - ) - retry.Run(t, func(r *retry.R) { - _, caRoot = s1.getCAProvider() - secondaryProvider, _ = s2.getCAProvider() - intermediatePEM, err = secondaryProvider.ActiveIntermediate() - require.NoError(r, err) - - // Verify the root lists are equal in each DC's state store. - state1 := s1.fsm.State() - _, roots1, err := state1.CARoots(nil) - require.NoError(r, err) - - state2 := s2.fsm.State() - _, roots2, err := state2.CARoots(nil) - require.NoError(r, err) - require.Len(r, roots1, 1) - require.Len(r, roots1, 1) - require.Equal(r, roots1[0].ID, roots2[0].ID) - require.Equal(r, roots1[0].RootCert, roots2[0].RootCert) - require.Empty(r, roots1[0].IntermediateCerts) - require.NotEmpty(r, roots2[0].IntermediateCerts) - }) - - // Have secondary sign a leaf cert and make sure the chain is correct. - spiffeService := &connect.SpiffeIDService{ - Host: "node1", - Namespace: "default", - Datacenter: "primary", - Service: "foo", + tests := []struct { + keyType string + keyBits int + }{ + {connect.DefaultPrivateKeyType, connect.DefaultPrivateKeyBits}, + {"rsa", 2048}, } - raw, _ := connect.TestCSR(t, spiffeService) - leafCsr, err := connect.ParseCSR(raw) - require.NoError(t, err) + for _, tc := range tests { + tc := tc + t.Run(fmt.Sprintf("%s-%d", tc.keyType, tc.keyBits), func(t *testing.T) { + masterToken := "8a85f086-dd95-4178-b128-e10902767c5c" - leafPEM, err := secondaryProvider.Sign(leafCsr) - require.NoError(t, err) + // Initialize primary as the primary DC + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "primary" + c.ACLDatacenter = "primary" + c.Build = "1.6.0" + c.ACLsEnabled = true + c.ACLMasterToken = masterToken + c.ACLDefaultPolicy = "deny" + c.CAConfig.Config["PrivateKeyType"] = tc.keyType + c.CAConfig.Config["PrivateKeyBits"] = tc.keyBits + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() - cert, err := connect.ParseCert(leafPEM) - require.NoError(t, err) + s1.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig) - // Check that the leaf signed by the new cert can be verified using the - // returned cert chain (signed intermediate + remote root). - intermediatePool := x509.NewCertPool() - intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM)) - rootPool := x509.NewCertPool() - rootPool.AppendCertsFromPEM([]byte(caRoot.RootCert)) + testrpc.WaitForLeader(t, s1.RPC, "primary") - _, err = cert.Verify(x509.VerifyOptions{ - Intermediates: intermediatePool, - Roots: rootPool, - }) - require.NoError(t, err) + // secondary as a secondary DC + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.Datacenter = "secondary" + c.ACLDatacenter = "primary" + c.Build = "1.6.0" + c.ACLsEnabled = true + c.ACLDefaultPolicy = "deny" + c.ACLTokenReplication = true + c.CAConfig.Config["PrivateKeyType"] = tc.keyType + c.CAConfig.Config["PrivateKeyBits"] = tc.keyBits + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + s2.tokens.UpdateAgentToken(masterToken, token.TokenSourceConfig) + s2.tokens.UpdateReplicationToken(masterToken, token.TokenSourceConfig) + + testrpc.WaitForLeader(t, s2.RPC, "secondary") + + // Create the WAN link + joinWAN(t, s2, s1) + + waitForNewACLs(t, s1) + waitForNewACLs(t, s2) + + // Ensure s2 is authoritative. + waitForNewACLReplication(t, s2, structs.ACLReplicateTokens, 1, 1, 0) + + // Wait until the providers are fully bootstrapped. + var ( + caRoot *structs.CARoot + secondaryProvider ca.Provider + intermediatePEM string + err error + ) + retry.Run(t, func(r *retry.R) { + _, caRoot = s1.getCAProvider() + secondaryProvider, _ = s2.getCAProvider() + intermediatePEM, err = secondaryProvider.ActiveIntermediate() + require.NoError(r, err) + + // Sanity check CA is using the correct key type + require.Equal(r, tc.keyType, caRoot.PrivateKeyType) + require.Equal(r, tc.keyBits, caRoot.PrivateKeyBits) + + // Verify the root lists are equal in each DC's state store. + state1 := s1.fsm.State() + _, roots1, err := state1.CARoots(nil) + require.NoError(r, err) + + state2 := s2.fsm.State() + _, roots2, err := state2.CARoots(nil) + require.NoError(r, err) + require.Len(r, roots1, 1) + require.Len(r, roots2, 1) + require.Equal(r, roots1[0].ID, roots2[0].ID) + require.Equal(r, roots1[0].RootCert, roots2[0].RootCert) + require.Empty(r, roots1[0].IntermediateCerts) + require.NotEmpty(r, roots2[0].IntermediateCerts) + }) + + // Have secondary sign a leaf cert and make sure the chain is correct. + spiffeService := &connect.SpiffeIDService{ + Host: "node1", + Namespace: "default", + Datacenter: "primary", + Service: "foo", + } + raw, _ := connect.TestCSR(t, spiffeService) + + leafCsr, err := connect.ParseCSR(raw) + require.NoError(t, err) + + leafPEM, err := secondaryProvider.Sign(leafCsr) + require.NoError(t, err) + + // Check that the leaf signed by the new cert can be verified using the + // returned cert chain (signed intermediate + remote root). + require.NoError(t, connect.ValidateLeaf(caRoot.RootCert, leafPEM, []string{intermediatePEM})) + }) + } } func TestLeader_SecondaryCA_IntermediateRefresh(t *testing.T) { @@ -1202,44 +1214,83 @@ func TestLeader_PersistIntermediateCAs(t *testing.T) { func TestLeader_ParseCARoot(t *testing.T) { type test struct { - pem string - expectedError bool + name string + pem string + wantSerial uint64 + wantSigningKeyID string + wantKeyType string + wantKeyBits int + wantErr bool } + // Test certs generated with + // go run connect/certgen/certgen.go -out-dir /tmp/connect-certs -key-type ec -key-bits 384 + // for various key types. This does limit the exposure to formats that might + // exist in external certificates which can be used as Connect CAs. + // Specifically many other certs will have serial numbers that don't fit into + // 64 bits but for reasons we truncate down to 64 bits which means our + // `SerialNumber` will not match the one reported by openssl. We should + // probably fix that at some point as it seems like a big footgun but it would + // be a breaking API change to change the type to not be a JSON number and + // JSON numbers don't even support the full range of a uint64... tests := []test{ - {"", true}, - {`-----BEGIN CERTIFICATE----- -MIIDHDCCAsKgAwIBAgIQS+meruRVzrmVwEhXNrtk9jAKBggqhkjOPQQDAjCBuTEL -MAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2Nv -MRowGAYDVQQJExExMDEgU2Vjb25kIFN0cmVldDEOMAwGA1UEERMFOTQxMDUxFzAV -BgNVBAoTDkhhc2hpQ29ycCBJbmMuMUAwPgYDVQQDEzdDb25zdWwgQWdlbnQgQ0Eg -MTkzNzYxNzQwMjcxNzUxOTkyMzAyMzE1NDkxNjUzODYyMzAwNzE3MB4XDTE5MDQx -MjA5MTg0NVoXDTIwMDQxMTA5MTg0NVowHDEaMBgGA1UEAxMRY2xpZW50LmRjMS5j -b25zdWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS2UroGUh5k7eR//iPsn9ne -CMCVsERnjqQnK6eDWnM5kTXgXcPPe5pcAS9xs0g8BZ+oVsJSc7sH6RYvX+gw6bCl -o4IBRjCCAUIwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggr -BgEFBQcDATAMBgNVHRMBAf8EAjAAMGgGA1UdDgRhBF84NDphNDplZjoxYTpjODo1 -MzoxMDo1YTpjNTplYTpjZTphYTowZDo2ZjpjOTozODozZDphZjo0NTphZTo5OTo4 -YzpiYjoyNzpiYzpiMzpmYTpmMDozMToxNDo4ZTozNDBqBgNVHSMEYzBhgF8yYTox -MjpjYTo0Mzo0NzowODpiZjoxYTo0Yjo4MTpkNDo2MzowNTo1ODowZToxYzo3Zjoy -NTo0ZjozNDpmNDozYjpmYzo5YTpkNzo4Mjo2YjpkYzpmODo3YjphMTo5ZDAtBgNV -HREEJjAkghFjbGllbnQuZGMxLmNvbnN1bIIJbG9jYWxob3N0hwR/AAABMAoGCCqG -SM49BAMCA0gAMEUCIHcLS74KSQ7RA+edwOprmkPTh1nolwXz9/y9CJ5nMVqEAiEA -h1IHCbxWsUT3AiARwj5/D/CUppy6BHIFkvcpOCQoVyo= ------END CERTIFICATE-----`, false}, + {"no cert", "", 0, "", "", 0, true}, + { + name: "default cert", + // Watchout for indentations they will break PEM format + pem: readTestData(t, "cert-with-ec-256-key.pem"), + // Based on `openssl x509 -noout -text` report from the cert + wantSerial: 8341954965092507701, + wantSigningKeyID: "97:4D:17:81:64:F8:B4:AF:05:E8:6C:79:C5:40:3B:0E:3E:8B:C0:AE:38:51:54:8A:2F:05:DB:E3:E8:E4:24:EC", + wantKeyType: "ec", + wantKeyBits: 256, + wantErr: false, + }, + { + name: "ec 384 cert", + // Watchout for indentations they will break PEM format + pem: readTestData(t, "cert-with-ec-384-key.pem"), + // Based on `openssl x509 -noout -text` report from the cert + wantSerial: 2935109425518279965, + wantSigningKeyID: "0B:A0:88:9B:DC:95:31:51:2E:3D:D4:F9:42:D0:6A:A0:62:46:82:D2:7C:22:E7:29:A9:AA:E8:A5:8C:CF:C7:42", + wantKeyType: "ec", + wantKeyBits: 384, + wantErr: false, + }, + { + name: "rsa 4096 cert", + // Watchout for indentations they will break PEM format + pem: readTestData(t, "cert-with-rsa-4096-key.pem"), + // Based on `openssl x509 -noout -text` report from the cert + wantSerial: 5186695743100577491, + wantSigningKeyID: "92:FA:CC:97:57:1E:31:84:A2:33:DD:9B:6A:A8:7C:FC:BE:E2:94:CA:AC:B3:33:17:39:3B:B8:67:9B:DC:C1:08", + wantKeyType: "rsa", + wantKeyBits: 4096, + wantErr: false, + }, } - for _, test := range tests { - root, err := parseCARoot(test.pem, "consul", "cluster") - if err == nil && test.expectedError { - require.Error(t, err) - } - if test.pem != "" { - rootCert, err := connect.ParseCert(test.pem) - require.NoError(t, err) - - // just to make sure these two are not the same - require.NotEqual(t, rootCert.AuthorityKeyId, rootCert.SubjectKeyId) - - require.Equal(t, connect.EncodeSigningKeyID(rootCert.SubjectKeyId), root.SigningKeyID) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + root, err := parseCARoot(tt.pem, "consul", "cluster") + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + require.Equal(tt.wantSerial, root.SerialNumber) + require.Equal(strings.ToLower(tt.wantSigningKeyID), root.SigningKeyID) + require.Equal(tt.wantKeyType, root.PrivateKeyType) + require.Equal(tt.wantKeyBits, root.PrivateKeyBits) + }) } } + +func readTestData(t *testing.T, name string) string { + t.Helper() + path := filepath.Join("testdata", name) + bs, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("failed reading fixture file %s: %s", name, err) + } + return string(bs) +} diff --git a/agent/consul/testdata/cert-with-ec-256-key.pem b/agent/consul/testdata/cert-with-ec-256-key.pem new file mode 100644 index 0000000000..e60234bb5f --- /dev/null +++ b/agent/consul/testdata/cert-with-ec-256-key.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB3DCCAYKgAwIBAgIIc8ST19qlIDUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ +VGVzdCBDQSAxMB4XDTE5MTAxNzExNDYyOVoXDTI5MTAxNzExNDYyOVowFDESMBAG +A1UEAxMJVGVzdCBDQSAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErA61DUlq +qDnXAcHIHVJKBUtyDYoQmZB1T1H7NHTn4XezkF23RjL9Ha8DghMR/bwz7YhZ1Tv6 +UnYSq5r28P6b06OBvTCBujAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zApBgNVHQ4EIgQgl00XgWT4tK8F6Gx5xUA7Dj6LwK44UVSKLwXb4+jkJOwwKwYD +VR0jBCQwIoAgl00XgWT4tK8F6Gx5xUA7Dj6LwK44UVSKLwXb4+jkJOwwPwYDVR0R +BDgwNoY0c3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1NTU1NTU1 +NTU1LmNvbnN1bDAKBggqhkjOPQQDAgNIADBFAiEA/x2MeYU5vCk2hwP7zlrv7bx3 +9zx5YSbn04sgP6sNK30CIEPfjxDGy6K2dPDckATboYkZVQ4CJpPd6WrgwQaHpWC9 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/agent/consul/testdata/cert-with-ec-384-key.pem b/agent/consul/testdata/cert-with-ec-384-key.pem new file mode 100644 index 0000000000..cd7aad5564 --- /dev/null +++ b/agent/consul/testdata/cert-with-ec-384-key.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIIKLuaeLzq/R0wCgYIKoZIzj0EAwMwFDESMBAGA1UEAxMJ +VGVzdCBDQSAxMB4XDTE5MTAxNzExNTUxOFoXDTI5MTAxNzExNTUxOFowFDESMBAG +A1UEAxMJVGVzdCBDQSAxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFWtdx2o3c9qv +ka8vxPO2Iv9NAbiwh3cl1a90miRzhQMP7s6wycXfl1xKE02PRxiLQtuukKwE6ohv +Ha5h4kkqGB+YdOT+18JS+ixJwmmSZL5pkAh6SMEGby3wf5sap5F/o4G9MIG6MA4G +A1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MCkGA1UdDgQiBCALoIib3JUx +US491PlC0GqgYkaC0nwi5ympquiljM/HQjArBgNVHSMEJDAigCALoIib3JUxUS49 +1PlC0GqgYkaC0nwi5ympquiljM/HQjA/BgNVHREEODA2hjRzcGlmZmU6Ly8xMTEx +MTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqGSM49 +BAMDA2gAMGUCMBT0orKHSATvulb6nRxVHq3OWOfmVgHu8VUCq9yuyAu1AAy/przY +/U0ury3g8T4jhwIxAIoCqYwWSJMFb13DZAR3XY+aFssVP5+vzhlaulqtg+YqjpKP +KzuCBpS3yUyAwWDphg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/agent/consul/testdata/cert-with-rsa-4096-key.pem b/agent/consul/testdata/cert-with-rsa-4096-key.pem new file mode 100644 index 0000000000..c5f41ed260 --- /dev/null +++ b/agent/consul/testdata/cert-with-rsa-4096-key.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIIR/rYTE2KZtMwDQYJKoZIhvcNAQELBQAwFDESMBAGA1UE +AxMJVGVzdCBDQSAxMB4XDTE5MTAxNzExNTMxNVoXDTI5MTAxNzExNTMxNVowFDES +MBAGA1UEAxMJVGVzdCBDQSAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAlIsTpCearMRx0fW2BKmGXHUYCf1ATALcRn31VQCuzszyt77RRbQn+v/XB+Q5 +Td+/o3ZkpKvCTAF31oZI0ihXEEq7o13pERGPZdD//jNvgePIoauzxjkWfEX5bQGx +PxY3mGWakmiunQHm3ls8bm+ZYBfDM8rJTaKFBUCaNe6xi5znQ953+YELV0YHpSXm +H0lP93/RVeua9094+YNadUfB1nOWvlayn/YX3oXPAwQsoT1DlqRlGFDAp3MPFNkC +RvymdigxAf1HrsyZ+MlD99Htgx2YkmHRXidRW9OzMmVSTe5gT6AOCvC4Zd5udH2H +bH+DUNuyJr2YC0VB7RBWbqCvpmxItLaDoSHLndRjP0wY9chMDqrKKRIzupeaYfVK +isFdSwj39EnNQEfm7ZJT/8aRNtF8pVFYHO0fZ+2eObpteWyQj0iOJjHuRpUWI2LT +ZMEPkj0N2I1tAvg1g/RYasXKwme+L7ejxzyYnC8kqDf4/C4tNaDURXP5fWn8aVsI +Sp8gMZHxXD8e8FAMPuDQi4y0FZu9bY9mJ71OSqcR5u0DHKzaCfAwr+qheFxCCly0 +UnP7FAOSg96mRYAzgYGTFM5/HSmj0vvnsfAAebuiloE4IKfeBzp9kPuauEbcGbTa +4bBbaoamqdEUT3bFqRXWCXCQ6kT1xos+YcfjBkpfmdOrrdUCAwEAAaOBvTCBujAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zApBgNVHQ4EIgQgkvrMl1ce +MYSiM92baqh8/L7ilMqsszMXOTu4Z5vcwQgwKwYDVR0jBCQwIoAgkvrMl1ceMYSi +M92baqh8/L7ilMqsszMXOTu4Z5vcwQgwPwYDVR0RBDgwNoY0c3BpZmZlOi8vMTEx +MTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1NTU1NTU1NTU1LmNvbnN1bDANBgkqhkiG +9w0BAQsFAAOCAgEAaGorq69i6S6RIsOXx6zou2gr2ez6siVFkxoJEXx8lZKS8UXs +mmAxJXXKePv921zFeUB5jcdxIaBlxyEHiJGr9KUDYWnkAY0g8343JueY8epPOGpb +u1QoTpCRSJBdOyakK3ZhYqRa28G0fP0eQQ+mF54X2YA4jtg7pb30wahQd9U0M2ey +A6VLw4xdmqC7KOfJSvQcCZJPi5Wkqv/pf55WmS28zhxrwMC9U4vaKVRsGKJi/p/X +WUmyPpOUM41nEylcpFvs/Xx8eKUSlWRaMTWYHUKTdaXKYL/1op2PhuyoYkH384aj +P7RzLkC1fRC8MPXo9L1YSoK7vxL5K/zLkJFb3KopDzujGx1cZbgO8QREe16bVN4b +gFLwIzW+VzywmtoSur5V9ythVfvRT1XesmsK/G2ySxWvipYIDYYJrwgKR5b0ikfz +RPw2YW6oqaMmZ9Uehxym8RDWqyyFPg9S0C73MTK7FitIROLW88hWKSpDDhFck/32 +FVaRL8cC0KVlMCFByL/o6u0AsRNCOux1q3BJEdmAh7VI84+SPgztHFkptR4VnlHZ +kKTj2Mj/OylHHwhe6AU9pbtAGM6DtcqSjmd4wrkRX8WJDd/F3RlYZ8WhOToOj9gP +ra4mUhGz/OlDg6vN9TSeVlb5Ap7c38KoCmmt2n+F/KUpe6V4L1QA5yfz0S8= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/agent/structs/connect_ca.go b/agent/structs/connect_ca.go index 47e4947e10..f458ef310e 100644 --- a/agent/structs/connect_ca.go +++ b/agent/structs/connect_ca.go @@ -102,8 +102,14 @@ type CARoot struct { // active root. RotatedOutAt time.Time `json:"-"` - // Type of private key used to create the CA cert. + // PrivateKeyType is the type of the private key used to sign certificates. It + // may be "rsa" or "ec". This is provided as a convenience to avoid parsing + // the public key to from the certificate to infer the type. PrivateKeyType string + + // PrivateKeyBits is the length of the private key used to sign certificates. + // This is provided as a convenience to avoid parsing the public key from the + // certificate to infer the type. PrivateKeyBits int RaftIndex @@ -282,7 +288,18 @@ type CommonCAProviderConfig struct { // is used. This is ignored if CSRMaxPerSecond is non-zero. CSRMaxConcurrent int + // PrivateKeyType specifies which type of key the CA should generate. It only + // applies when the provider is generating its own key and is ignored if the + // provider already has a key or an external key is provided. Supported values + // are "ec" or "rsa". "ec" is the default and will generate a NIST P-256 + // Elliptic key. PrivateKeyType string + + // PrivateKeyBits specifies the number of bits the CA's private key should + // use. For RSA, supported values are 2048 and 4096. For EC, supported values + // are 224, 256, 384 and 521 and correspond to the NIST P-* curve of the same + // name. As with PrivateKeyType this is only relevant whan the provier is + // generating new CA keys (root or intermediate). PrivateKeyBits int } @@ -302,14 +319,14 @@ func (c CommonCAProviderConfig) Validate() error { switch c.PrivateKeyType { case "ec": if c.PrivateKeyBits != 224 && c.PrivateKeyBits != 256 && c.PrivateKeyBits != 384 && c.PrivateKeyBits != 521 { - return fmt.Errorf("ECDSA key length must be one of (224, 256, 384, 521) bits") + return fmt.Errorf("EC key length must be one of (224, 256, 384, 521) bits") } case "rsa": if c.PrivateKeyBits != 2048 && c.PrivateKeyBits != 4096 { return fmt.Errorf("RSA key length must be 2048 or 4096 bits") } default: - return fmt.Errorf("private key type must be either 'ecdsa' or 'rsa'") + return fmt.Errorf("private key type must be either 'ec' or 'rsa'") } return nil diff --git a/connect/certgen/certgen.go b/connect/certgen/certgen.go index 3f5cae0e7f..6767fd9753 100644 --- a/connect/certgen/certgen.go +++ b/connect/certgen/certgen.go @@ -7,7 +7,7 @@ // // You can verify a given leaf with a given root using: // -// $ openssl verify -verbose -CAfile ca2-ca.cert.pem ca1-svc-db.cert.pem +// $ openssl verify -verbose -CAfile ca1-ca.cert.pem ca1-svc-db.cert.pem // // Note that to verify via the cross-signed intermediate, openssl requires it to // be bundled with the _root_ CA bundle and will ignore the cert if it's passed diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/bind.hcl b/test/integration/connect/envoy/case-multidc-rsa-ca/bind.hcl new file mode 100644 index 0000000000..f54393f03e --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/bind.hcl @@ -0,0 +1,2 @@ +bind_addr = "0.0.0.0" +advertise_addr = "{{ GetInterfaceIP \"eth0\" }}" \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/ca_config.hcl b/test/integration/connect/envoy/case-multidc-rsa-ca/ca_config.hcl new file mode 100644 index 0000000000..a9e59cca48 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/ca_config.hcl @@ -0,0 +1,7 @@ +connect { + enabled = true + ca_config { + private_key_type = "rsa" + private_key_bits = 2048 + } +} \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/capture.sh b/test/integration/connect/envoy/case-multidc-rsa-ca/capture.sh new file mode 100644 index 0000000000..7c9106ed79 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/capture.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:19000 s1 primary || true +snapshot_envoy_admin localhost:19001 s2 secondary || true \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/primary/s1.hcl b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/s1.hcl new file mode 100644 index 0000000000..96682de1a5 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/s1.hcl @@ -0,0 +1,17 @@ +services { + name = "s1" + port = 8080 + connect { + sidecar_service { + proxy { + upstreams = [ + { + destination_name = "s2" + datacenter = "secondary" + local_bind_port = 5000 + } + ] + } + } + } +} \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/primary/s2.hcl b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/s2.hcl new file mode 100644 index 0000000000..77164e722b --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/s2.hcl @@ -0,0 +1 @@ +# We don't want an s2 service in the primary dc \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/primary/setup.sh b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/setup.sh new file mode 100644 index 0000000000..ac7c2720f0 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/setup.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -eEuo pipefail + +gen_envoy_bootstrap s1 19000 primary +retry_default docker_consul primary curl -s "http://localhost:8500/v1/catalog/service/consul?dc=secondary" >/dev/null diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/primary/verify.bats b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/verify.bats new file mode 100644 index 0000000000..57f3f4a640 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/primary/verify.bats @@ -0,0 +1,43 @@ +#!/usr/bin/env bats + +load helpers + +@test "s1 proxy is running correct version" { + assert_envoy_version 19000 +} + +@test "s1 proxy admin is up on :19000" { + retry_default curl -f -s localhost:19000/stats -o /dev/null +} + +@test "s1 proxy listener should be up and have right cert" { + assert_proxy_presents_cert_uri localhost:21000 s1 +} + +@test "s1 upstream should have healthy endpoints for s2" { + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.secondary HEALTHY 1 +} + +@test "s1 upstream should be able to connect to s2" { + run retry_default curl -s -f -d hello localhost:5000 + [ "$status" -eq 0 ] + [ "$output" = "hello" ] +} + +@test "s1 upstream made 1 connection" { + assert_envoy_metric_at_least 127.0.0.1:19000 "cluster.s2.default.secondary.*cx_total" 1 +} + +@test "ca key should be RSA" { + run retry_default curl -f -s 127.0.0.1:8500/v1/connect/ca/roots + + echo "$status" + echo "OUTPUT: $output" + + [ "$status" -eq 0 ] + + KEY_TYPE=$(echo "$output" | jq -r '.Roots[0].PrivateKeyType') + echo "KEY_TYPE: $KEY_TYPE" + + [ "$KEY_TYPE" == "rsa" ] +} \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/join.hcl b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/join.hcl new file mode 100644 index 0000000000..fb1307d62a --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/join.hcl @@ -0,0 +1 @@ +retry_join_wan = ["consul-primary"] \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/s1.hcl b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/s1.hcl new file mode 100644 index 0000000000..eff93eb6b5 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/s1.hcl @@ -0,0 +1 @@ +# we don't want an s1 service in the secondary dc \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/setup.sh b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/setup.sh new file mode 100644 index 0000000000..6ea888c41c --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/setup.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -eEuo pipefail + +gen_envoy_bootstrap s2 19001 secondary +retry_default docker_consul secondary curl -s "http://localhost:8500/v1/catalog/service/consul?dc=primary" >/dev/null \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/verify.bats b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/verify.bats new file mode 100644 index 0000000000..cf31a6d344 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/secondary/verify.bats @@ -0,0 +1,19 @@ +#!/usr/bin/env bats + +load helpers + +@test "s2 proxy is running correct version" { + assert_envoy_version 19001 +} + +@test "s2 proxy admin is up on :19001" { + retry_default curl -f -s localhost:19001/stats -o /dev/null +} + +@test "s2 proxy listener should be up and have right cert" { + assert_proxy_presents_cert_uri localhost:21000 s2 secondary +} + +@test "s2 proxy should be healthy" { + assert_service_has_healthy_instances s2 1 secondary +} \ No newline at end of file diff --git a/test/integration/connect/envoy/case-multidc-rsa-ca/vars.sh b/test/integration/connect/envoy/case-multidc-rsa-ca/vars.sh new file mode 100644 index 0000000000..742813a1a9 --- /dev/null +++ b/test/integration/connect/envoy/case-multidc-rsa-ca/vars.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2-secondary s2-sidecar-proxy-secondary" +export REQUIRE_SECONDARY=1 \ No newline at end of file diff --git a/test/integration/connect/envoy/run-tests.sh b/test/integration/connect/envoy/run-tests.sh index 85084f085e..c966e01ec1 100755 --- a/test/integration/connect/envoy/run-tests.sh +++ b/test/integration/connect/envoy/run-tests.sh @@ -17,7 +17,7 @@ FILTER_TESTS=${FILTER_TESTS:-} STOP_ON_FAIL=${STOP_ON_FAIL:-} # ENVOY_VERSIONS is the list of envoy versions to run each test against -ENVOY_VERSIONS=${ENVOY_VERSIONS:-"1.10.0 1.9.1 1.8.0 1.11.1"} +ENVOY_VERSIONS=${ENVOY_VERSIONS:-"1.10.0 1.9.1 1.8.0 1.11.2"} if [ ! -z "$DEBUG" ] ; then set -x diff --git a/website/source/api/agent/connect.html.md b/website/source/api/agent/connect.html.md index 53ef3e0c99..4c0fe9a5b5 100644 --- a/website/source/api/agent/connect.html.md +++ b/website/source/api/agent/connect.html.md @@ -135,8 +135,8 @@ $ curl \ "RootCert": "-----BEGIN CERTIFICATE-----\nMIICmDCCAj6gAwIBAgIBBzAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtDb25zdWwg\nQ0EgNzAeFw0xODA1MjExNjMzMjhaFw0yODA1MTgxNjMzMjhaMBYxFDASBgNVBAMT\nC0NvbnN1bCBDQSA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER0qlxjnRcMEr\niSGlH7G7dYU7lzBEmLUSMZkyBbClmyV8+e8WANemjn+PLnCr40If9cmpr7RnC9Qk\nGTaLnLiF16OCAXswggF3MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/\nMGgGA1UdDgRhBF8xZjo5MTpjYTo0MTo4ZjphYzo2NzpiZjo1OTpjMjpmYTo0ZTo3\nNTo1YzpkODpmMDo1NTpkZTpiZTo3NTpiODozMzozMTpkNToyNDpiMDowNDpiMzpl\nODo5Nzo1Yjo3ZTBqBgNVHSMEYzBhgF8xZjo5MTpjYTo0MTo4ZjphYzo2NzpiZjo1\nOTpjMjpmYTo0ZTo3NTo1YzpkODpmMDo1NTpkZTpiZTo3NTpiODozMzozMTpkNToy\nNDpiMDowNDpiMzplODo5Nzo1Yjo3ZTA/BgNVHREEODA2hjRzcGlmZmU6Ly8xMjRk\nZjVhMC05ODIwLTc2YzMtOWFhOS02ZjYyMTY0YmExYzIuY29uc3VsMD0GA1UdHgEB\n/wQzMDGgLzAtgisxMjRkZjVhMC05ODIwLTc2YzMtOWFhOS02ZjYyMTY0YmExYzIu\nY29uc3VsMAoGCCqGSM49BAMCA0gAMEUCIQDzkkI7R+0U12a+zq2EQhP/n2mHmta+\nfs2hBxWIELGwTAIgLdO7RRw+z9nnxCIA6kNl//mIQb+PGItespiHZKAz74Q=\n-----END CERTIFICATE-----\n", "IntermediateCerts": null, "Active": true, - "PrivateKeyType": "", - "PrivateKeyBits": 0, + "PrivateKeyType": "ec", + "PrivateKeyBits": 256, "CreateIndex": 8, "ModifyIndex": 8 } diff --git a/website/source/api/connect/ca.html.md b/website/source/api/connect/ca.html.md index 54b152e679..43d9dd8485 100644 --- a/website/source/api/connect/ca.html.md +++ b/website/source/api/connect/ca.html.md @@ -56,8 +56,8 @@ $ curl \ "RootCert": "-----BEGIN CERTIFICATE-----\nMIICmDCCAj6gAwIBAgIBBzAKBggqhkjOPQQDAjAWMRQwEgYDVQQDEwtDb25zdWwg\nQ0EgNzAeFw0xODA1MjUyMTM5MjNaFw0yODA1MjIyMTM5MjNaMBYxFDASBgNVBAMT\nC0NvbnN1bCBDQSA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq4S32Pu0/VL4\nG75gvdyQuAhqMZFsfBRwD3pgvblgZMeJc9KDosxnPR+W34NXtMD/860NNVJIILln\n9lLhIjWPQqOCAXswggF3MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/\nMGgGA1UdDgRhBF8yZDowOTo1ZDo4NDpiOTo4OTo0YjpkZDplMzo4ODpiYjo5Yzpl\nMjpiMjo2OTo4MToxZjo0YjphNjpmZDo0ZDpkZjplZTo3NDo2MzpmMzo3NDo1NTpj\nYTpiMDpiNTo2NTBqBgNVHSMEYzBhgF8yZDowOTo1ZDo4NDpiOTo4OTo0YjpkZDpl\nMzo4ODpiYjo5YzplMjpiMjo2OTo4MToxZjo0YjphNjpmZDo0ZDpkZjplZTo3NDo2\nMzpmMzo3NDo1NTpjYTpiMDpiNTo2NTA/BgNVHREEODA2hjRzcGlmZmU6Ly83ZjQy\nZjQ5Ni1mYmM3LTg2OTItMDVlZC0zMzRhYTUzNDBjMWUuY29uc3VsMD0GA1UdHgEB\n/wQzMDGgLzAtgis3ZjQyZjQ5Ni1mYmM3LTg2OTItMDVlZC0zMzRhYTUzNDBjMWUu\nY29uc3VsMAoGCCqGSM49BAMCA0gAMEUCIBBBDOWXWApx4S6bHJ49AW87Nw8uQ/gJ\nJ6lvm3HzEQw2AiEA4PVqWt+z8fsQht0cACM42kghL97SgDSf8rgCqfLYMng=\n-----END CERTIFICATE-----\n", "IntermediateCerts": null, "Active": true, - "PrivateKeyType": "", - "PrivateKeyBits": 0, + "PrivateKeyType": "ec", + "PrivateKeyBits": 256, "CreateIndex": 8, "ModifyIndex": 8 } diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index 4d616176fe..9f94a44fba 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -914,19 +914,18 @@ default will automatically work with some tooling.

There are also a number of common configuration options supported by all providers:

- * `leaf_cert_ttl` The upper bound on the - lease duration of a leaf certificate issued for a service. In most - cases a new leaf certificate will be requested by a proxy before this - limit is reached. This is also the effective limit on how long a - server outage can last (with no leader) before network connections - will start being rejected, and as a result the defaults is `72h` to - last through a weekend without intervention. This value cannot be - lower than 1 hour or higher than 1 year. - - This value is also used when rotating out old root certificates from - the cluster. When a root certificate has been inactive (rotated out) - for more than twice the *current* `leaf_cert_ttl`, it will be removed - from the trusted list. + * `csr_max_concurrent` Sets a limit + on how many Certificate Signing Requests will be processed + concurrently. Defaults to 0 (disabled). This is useful when you have + more than one or two cores available to the server. For example on an + 8 core server, setting this to 1 will ensure that even during a CA + rotation no more than one server core on the leader will be consumed + at a time with generating new certificates. Setting this is + recommended _instead_ of `csr_max_per_second` where you know there are + multiple cores available since it is simpler to reason about limiting + CSR resources this way without artificially slowing down rotations. + Added in 1.4.1. * `csr_max_per_second` Sets a rate @@ -941,18 +940,58 @@ default will automatically work with some tooling. `csr_max_concurrent` instead if servers have more than one core. Setting this to zero disables rate limiting. Added in 1.4.1. - * `csr_max_concurrent` Sets a limit - on how many Certificate Signing Requests will be processed - concurrently. Defaults to 0 (disabled). This is useful when you have - more than one or two cores available to the server. For example on an - 8 core server, setting this to 1 will ensure that even during a CA - rotation no more than one server core on the leader will be consumed - at a time with generating new certificates. Setting this is - recommended _instead_ of `csr_max_per_second` where you know there are - multiple cores available since it is simpler to reason about limiting - CSR resources this way without artificially slowing down rotations. - Added in 1.4.1. + * `leaf_cert_ttl` The upper bound on the + lease duration of a leaf certificate issued for a service. In most + cases a new leaf certificate will be requested by a proxy before this + limit is reached. This is also the effective limit on how long a + server outage can last (with no leader) before network connections + will start being rejected, and as a result the defaults is `72h` to + last through a weekend without intervention. This value cannot be + lower than 1 hour or higher than 1 year. + + This value is also used when rotating out old root certificates from + the cluster. When a root certificate has been inactive (rotated out) + for more than twice the *current* `leaf_cert_ttl`, it will be removed + from the trusted list. + + * `private_key_type` The type of key to + generate for this CA. This is only used when the provider is + generating a new key. If `private_key` is set for the Consul provider, + or existing root or intermediate PKI paths given for Vault then this + will be ignored. Currently supported options are `ec` or `rsa`. + Default is `ec`. + + It is required that all servers in a Datacenter have + the same config for the CA. It is recommended that servers in + different Datacenters have the same CA config for key type and size + although the built-in CA and Vault provider will both allow mixed CA + key types. + + Some CA providers (currently Vault) will not allow cross-signing a + new CA certificate with a different key type. This means that if you + migrate from an RSA-keyed Vault CA to an EC-keyed CA from any + provider, you may have to proceed without cross-signing which risks + temporary connection issues for workloads during the new certificate + rollout. We highly recommend testing this outside of production to + understand the impact and suggest sticking to same key type where + possible. + + Note that this only affects _CA_ keys generated by the provider. + Leaf certificate keys are always EC 256 regardless of the CA + configuration. + + * `private_key_bits` The length of key + to generate for this CA. This is only used when the provider is + generating a new key. If `private_key` is set for the Consul provider, + or existing root or intermediate PKI paths given for Vault then this + will be ignored. + + Currently supported values are: + - `private_key_type = ec` (default): `224, 256, 384, 521` + corresponding to the NIST P-* curves of the same name. + - `private_key_type = rsa`: `2048, 4096` * `datacenter` Equivalent to the [`-datacenter` command-line flag](#_datacenter).