From 6773a1a8d17d5e3ca639b65c3582bca4ed66f508 Mon Sep 17 00:00:00 2001 From: Tero Saarni Date: Sun, 14 Jan 2024 21:58:39 +0200 Subject: [PATCH] added support for upstream verification for TCPProxy Signed-off-by: Tero Saarni --- changelogs/unreleased/6079-tsaarni-minor.md | 4 ++ internal/dag/httpproxy_processor.go | 52 +++++++++++++++---- .../v3/backendcavalidation_test.go | 50 ++++++++++++++++-- .../featuretests/v3/backendclientauth_test.go | 31 +++++++++++ 4 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 changelogs/unreleased/6079-tsaarni-minor.md diff --git a/changelogs/unreleased/6079-tsaarni-minor.md b/changelogs/unreleased/6079-tsaarni-minor.md new file mode 100644 index 00000000000..a3e372871f8 --- /dev/null +++ b/changelogs/unreleased/6079-tsaarni-minor.md @@ -0,0 +1,4 @@ +## Upstream TLS validation and client certificate for TCPProxy + +TCPProxy now supports validating server certificate and using client certificate for upstream TLS connections. +Set `httpproxy.spec.tcpproxy.services.validation.caSecret` and `subjectName` to enable optional validation and `tls.envoy-client-certificate` configuration file field or `ContourConfiguration.spec.envoy.clientCertificate` to set the optional client certificate. diff --git a/internal/dag/httpproxy_processor.go b/internal/dag/httpproxy_processor.go index f6a43c57a93..72590f74d21 100644 --- a/internal/dag/httpproxy_processor.go +++ b/internal/dag/httpproxy_processor.go @@ -951,17 +951,8 @@ func (p *HTTPProxyProcessor) computeRoutes( var uv *PeerValidationContext if (protocol == "tls" || protocol == "h2") && service.UpstreamValidation != nil { - caCertNamespacedName := k8s.NamespacedNameFrom(service.UpstreamValidation.CACertificate, k8s.DefaultNamespace(proxy.Namespace)) - // we can only validate TLS connections to services that talk TLS - uv, err = p.source.LookupUpstreamValidation(service.UpstreamValidation, caCertNamespacedName, proxy.Namespace) - if err != nil { - if _, ok := err.(DelegationNotPermittedError); ok { - validCond.AddErrorf(contour_api_v1.ConditionTypeTLSError, "CACertificateNotDelegated", - "service.UpstreamValidation.CACertificate Secret %q is not configured for certificate delegation", caCertNamespacedName) - } else { - validCond.AddErrorf(contour_api_v1.ConditionTypeServiceError, "TLSUpstreamValidation", - "Service [%s:%d] TLS upstream validation policy error: %s", service.Name, service.Port, err) - } + uv = p.peerValidationContext(validCond, proxy, service) + if uv == nil { return nil } } @@ -1212,6 +1203,25 @@ func (p *HTTPProxyProcessor) processHTTPProxyTCPProxy(validCond *contour_api_v1. return false } + var uv *PeerValidationContext + if (protocol == "tls" || protocol == "h2") && service.UpstreamValidation != nil { + uv = p.peerValidationContext(validCond, httpproxy, service) + if uv == nil { + return false + } + } + + var clientCertSecret *Secret + if p.ClientCertificate != nil { + // Since the client certificate is configured by admin, explicit delegation is not required. + clientCertSecret, err = p.source.LookupTLSSecretInsecure(*p.ClientCertificate) + if err != nil { + validCond.AddErrorf(contour_api_v1.ConditionTypeTLSError, "SecretNotValid", + "tls.envoy-client-certificate Secret %q is invalid: %s", p.ClientCertificate, err) + return false + } + } + proxy.Clusters = append(proxy.Clusters, &Cluster{ Upstream: s, Weight: uint32(service.Weight), @@ -1220,6 +1230,9 @@ func (p *HTTPProxyProcessor) processHTTPProxyTCPProxy(validCond *contour_api_v1. TCPHealthCheckPolicy: healthPolicy, SNI: s.ExternalName, TimeoutPolicy: ClusterTimeoutPolicy{ConnectTimeout: p.ConnectTimeout}, + UpstreamTLS: p.UpstreamTLS, + UpstreamValidation: uv, + ClientCertificate: clientCertSecret, }) } @@ -1482,6 +1495,23 @@ func (p *HTTPProxyProcessor) GlobalAuthorizationContext() map[string]string { return nil } +func (p *HTTPProxyProcessor) peerValidationContext(validCond *contour_api_v1.DetailedCondition, httpproxy *contour_api_v1.HTTPProxy, service contour_api_v1.Service) *PeerValidationContext { + caCertNamespacedName := k8s.NamespacedNameFrom(service.UpstreamValidation.CACertificate, k8s.DefaultNamespace(httpproxy.Namespace)) + // we can only validate TLS connections to services that talk TLS + uv, err := p.source.LookupUpstreamValidation(service.UpstreamValidation, caCertNamespacedName, httpproxy.Namespace) + if err != nil { + if _, ok := err.(DelegationNotPermittedError); ok { + validCond.AddErrorf(contour_api_v1.ConditionTypeTLSError, "CACertificateNotDelegated", + "service.UpstreamValidation.CACertificate Secret %q is not configured for certificate delegation", caCertNamespacedName) + } else { + validCond.AddErrorf(contour_api_v1.ConditionTypeServiceError, "TLSUpstreamValidation", + "Service [%s:%d] TLS upstream validation policy error: %s", service.Name, service.Port, err) + } + return nil + } + return uv +} + // expandPrefixMatches adds new Routes to account for the difference // between prefix replacement when matching on '/foo' and '/foo/'. // diff --git a/internal/featuretests/v3/backendcavalidation_test.go b/internal/featuretests/v3/backendcavalidation_test.go index ba2f6f5805c..897bb7dbf7b 100644 --- a/internal/featuretests/v3/backendcavalidation_test.go +++ b/internal/featuretests/v3/backendcavalidation_test.go @@ -21,6 +21,7 @@ import ( "github.com/projectcontour/contour/internal/dag" "github.com/projectcontour/contour/internal/featuretests" "github.com/projectcontour/contour/internal/fixture" + "github.com/projectcontour/contour/internal/ref" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -30,7 +31,7 @@ func TestClusterServiceTLSBackendCAValidation(t *testing.T) { rh, c, done := setup(t) defer done() - secret := &v1.Secret{ + caSecret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: "default", @@ -60,7 +61,7 @@ func TestClusterServiceTLSBackendCAValidation(t *testing.T) { }}, }, } - rh.OnAdd(secret) + rh.OnAdd(caSecret) rh.OnAdd(svc) rh.OnAdd(p1) @@ -93,7 +94,7 @@ func TestClusterServiceTLSBackendCAValidation(t *testing.T) { Name: svc.Name, Port: 443, UpstreamValidation: &contour_api_v1.UpstreamValidation{ - CACertificate: secret.Name, + CACertificate: caSecret.Name, SubjectName: "subjname", }, }}, @@ -140,7 +141,7 @@ func TestClusterServiceTLSBackendCAValidation(t *testing.T) { Name: svc.Name, Port: 443, UpstreamValidation: &contour_api_v1.UpstreamValidation{ - CACertificate: secret.Name, + CACertificate: caSecret.Name, SubjectName: "subjname", }, }}, @@ -174,4 +175,45 @@ func TestClusterServiceTLSBackendCAValidation(t *testing.T) { Resources: nil, TypeUrl: secretType, }) + + rh.OnDelete(hp1) + + tlsSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Type: v1.SecretTypeTLS, + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + } + rh.OnAdd(tlsSecret) + + tcpproxy := fixture.NewProxy("tcpproxy").WithSpec( + contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "www.example.com", + TLS: &contour_api_v1.TLS{ + SecretName: tlsSecret.Name, + }, + }, + TCPProxy: &contour_api_v1.TCPProxy{ + Services: []contour_api_v1.Service{{ + Name: svc.Name, + Port: 443, + Protocol: ref.To("tls"), + UpstreamValidation: &contour_api_v1.UpstreamValidation{ + CACertificate: caSecret.Name, + SubjectName: "subjname", + }, + }}, + }, + }) + rh.OnAdd(tcpproxy) + + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + tlsCluster(cluster("default/kuard/443/c6ccd34de5", "default/kuard/securebackend", "default_kuard_443"), []byte(featuretests.CERTIFICATE), "subjname", "", nil, nil), + ), + TypeUrl: clusterType, + }) } diff --git a/internal/featuretests/v3/backendclientauth_test.go b/internal/featuretests/v3/backendclientauth_test.go index 5b2f066b150..f1f52811853 100644 --- a/internal/featuretests/v3/backendclientauth_test.go +++ b/internal/featuretests/v3/backendclientauth_test.go @@ -126,6 +126,37 @@ func TestBackendClientAuthenticationWithHTTPProxy(t *testing.T) { TypeUrl: clusterType, }) + rh.OnDelete(proxy) + + tcpproxy := fixture.NewProxy("tcpproxy").WithSpec( + projcontour.HTTPProxySpec{ + VirtualHost: &projcontour.VirtualHost{ + Fqdn: "www.example.com", + TLS: &projcontour.TLS{ + SecretName: sec1.Name, + }, + }, + TCPProxy: &projcontour.TCPProxy{ + Services: []projcontour.Service{{ + Name: svc.Name, + Port: 443, + Protocol: ref.To("tls"), + UpstreamValidation: &projcontour.UpstreamValidation{ + CACertificate: sec2.Name, + SubjectName: "subjname", + }, + }}, + }, + }) + rh.OnAdd(tcpproxy) + + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + tlsCluster(cluster("default/backend/443/950c17581f", "default/backend/http", "default_backend_443"), []byte(featuretests.CERTIFICATE), "subjname", "", sec1, nil), + ), + TypeUrl: clusterType, + }) + // Test the error branch when Envoy client certificate secret does not exist. rh.OnDelete(sec1) c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{