Skip to content

Commit 2b268c2

Browse files
nfudensoloio-bulldozer[bot]bewebi
authored
projects/utils/ssl: Add a check for ssl secrets that will be rejected by envoy but not go (#10047)
Co-authored-by: soloio-bulldozer[bot] <48420018+soloio-bulldozer[bot]@users.noreply.github.com> Co-authored-by: Bernie Birnbaum <[email protected]>
1 parent b977adc commit 2b268c2

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
changelog:
2+
- type: FIX
3+
issueLink: https://github.com/solo-io/solo-projects/issues/6772
4+
resolvesIssue: false
5+
description: >-
6+
Plugs a gap where go would check a secret for validity per spec but Envoy is more aggressive.
7+
For example a TLS secret with a certChain that contains an invalid PEM block will be rejected by Envoy but not Go.
8+
Prior to this PR these types of secrets would be accepted by Gloo and nacked by Envoy.

projects/gateway2/translator/sslutils/ssl_utils.go

+29-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package sslutils
33
import (
44
"crypto/tls"
55
"fmt"
6+
"strings"
67

78
"github.com/rotisserie/eris"
89
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/client-go/util/cert"
911
)
1012

1113
var (
@@ -30,6 +32,10 @@ func validatedCertData(sslSecret *corev1.Secret) error {
3032
privateKey := sslSecret.Data[corev1.TLSPrivateKeyKey]
3133
rootCa := sslSecret.Data[corev1.ServiceAccountRootCAKey]
3234

35+
// we always return an error when the certChain and/or privateKey are invalid
36+
// in theory we could propagate only the valid blocks of the certChain (ie the output of cert.ParseCertsPEM(certChain))º
37+
// and this would be accepted by Envoy, however we choose to maintain consistency between the secret at rest and in
38+
// Envoy, which also maintains consistency with existing UX
3339
err := isValidSslKeyPair(certChain, privateKey, rootCa)
3440
if err != nil {
3541
return InvalidTlsSecretError(sslSecret, err)
@@ -38,12 +44,34 @@ func validatedCertData(sslSecret *corev1.Secret) error {
3844
return nil
3945
}
4046

47+
// isValidSslKeyPair validates that the cert and key are a valid pair
48+
// It previously only checked in go but now also checks that nothing is lost in cert encoding
4149
func isValidSslKeyPair(certChain, privateKey, rootCa []byte) error {
4250

4351
if len(certChain) == 0 || len(privateKey) == 0 {
4452
return NoCertificateFoundError
4553
}
4654

47-
_, err := tls.X509KeyPair([]byte(certChain), []byte(privateKey))
55+
_, err := tls.X509KeyPair(certChain, privateKey)
56+
if err != nil {
57+
return err
58+
}
59+
// validate that the parsed piece is valid
60+
// this is still faster than a call out to openssl despite this second parsing pass of the cert
61+
// pem parsing in go is permissive while envoy is not
62+
// this might not be needed once we have larger envoy validation
63+
candidateCert, err := cert.ParseCertsPEM(certChain)
64+
if err != nil {
65+
return err
66+
}
67+
reencoded, err := cert.EncodeCertificates(candidateCert...)
68+
if err != nil {
69+
return err
70+
}
71+
trimmedEncoded := strings.TrimSpace(string(reencoded))
72+
if trimmedEncoded != strings.TrimSpace(string(certChain)) {
73+
return fmt.Errorf("certificate chain does not match parsed certificate")
74+
}
75+
4876
return err
4977
}

projects/gloo/pkg/utils/ssl.go

+30
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package utils
33
import (
44
"crypto/tls"
55
"fmt"
6+
"strings"
67

78
envoycore "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
89
envoygrpccredential "github.com/envoyproxy/go-control-plane/envoy/config/grpc_credential/v3"
@@ -14,6 +15,7 @@ import (
1415
v1 "github.com/solo-io/gloo/projects/gloo/pkg/api/v1"
1516
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/ssl"
1617
"github.com/solo-io/solo-kit/pkg/api/v1/resources/core"
18+
"k8s.io/client-go/util/cert"
1719
)
1820

1921
//go:generate mockgen -destination mocks/mock_ssl.go github.com/solo-io/gloo/projects/gloo/pkg/utils SslConfigTranslator
@@ -429,6 +431,10 @@ func getSslSecrets(ref core.ResourceRef, secrets v1.SecretList) (string, string,
429431
rootCa := sslSecret.Tls.GetRootCa()
430432
ocspStaple := sslSecret.Tls.GetOcspStaple()
431433

434+
// we always return an error when the certChain and/or privateKey are invalid
435+
// in theory we could propagate only the valid blocks of the certChain (ie the output of cert.ParseCertsPEM(certChain))º
436+
// and this would be accepted by Envoy, however we choose to maintain consistency between the secret at rest and in
437+
// Envoy, which also maintains consistency with existing UX
432438
err = isValidSslKeyPair(certChain, privateKey, rootCa)
433439
if err != nil {
434440
return "", "", "", nil, InvalidTlsSecretError(secret.GetMetadata().Ref(), err)
@@ -437,13 +443,37 @@ func getSslSecrets(ref core.ResourceRef, secrets v1.SecretList) (string, string,
437443
return certChain, privateKey, rootCa, ocspStaple, nil
438444
}
439445

446+
// isValidSslKeyPair validates that the cert and key are a valid pair
447+
// It previously only checked in go but now also checks that nothing is lost in cert encoding
440448
func isValidSslKeyPair(certChain, privateKey, rootCa string) error {
441449
// in the case where we _only_ provide a rootCa, we do not want to validate tls.key+tls.cert
442450
if (certChain == "") && (privateKey == "") && (rootCa != "") {
443451
return nil
444452
}
445453

454+
// validate that the cert and key are a valid pair
446455
_, err := tls.X509KeyPair([]byte(certChain), []byte(privateKey))
456+
if err != nil {
457+
return err
458+
}
459+
460+
// validate that the parsed piece is valid
461+
// this is still faster than a call out to openssl despite this second parsing pass of the cert
462+
// pem parsing in go is permissive while envoy is not
463+
// this might not be needed once we have larger envoy validation
464+
candidateCert, err := cert.ParseCertsPEM([]byte(certChain))
465+
if err != nil {
466+
return err
467+
}
468+
reencoded, err := cert.EncodeCertificates(candidateCert...)
469+
if err != nil {
470+
return err
471+
}
472+
trimmedEncoded := strings.TrimSpace(string(reencoded))
473+
if trimmedEncoded != strings.TrimSpace(certChain) {
474+
return fmt.Errorf("certificate chain does not match parsed certificate")
475+
}
476+
447477
return err
448478
}
449479

projects/gloo/pkg/utils/ssl_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ var _ = Describe("Ssl", func() {
177177
Entry("upstreamCfg", func() utils.CertSource { return upstreamCfg }),
178178
Entry("downstreamCfg", func() utils.CertSource { return downstreamCfg }),
179179
)
180+
DescribeTable("should fail if invalid for envoy cert is provided",
181+
func(c func() utils.CertSource) {
182+
// unterminated cert in chain is valid for go b ut not envoy
183+
tlsSecret.CertChain += `-----BEGIN CERTIFICATE-----
184+
MIID6TCCA1ICAQEwDQYJKoZIhvcNAQEFBQAwgYsxCzAJBgNVBAYTAlVTMRMwEQYD`
185+
_, err := resolveCommonSslConfig(c(), secrets)
186+
Expect(err).To(HaveOccurred())
187+
188+
},
189+
Entry("upstreamCfg", func() utils.CertSource { return upstreamCfg }),
190+
Entry("downstreamCfg", func() utils.CertSource { return downstreamCfg }),
191+
)
180192
DescribeTable("should not have validation context if no rootca",
181193
func(c func() utils.CertSource) {
182194
tlsSecret.RootCa = ""

0 commit comments

Comments
 (0)