Skip to content

Commit

Permalink
added support for upstream verification for TCPProxy
Browse files Browse the repository at this point in the history
Signed-off-by: Tero Saarni <tero.saarni@est.tech>
  • Loading branch information
tsaarni committed Jan 14, 2024
1 parent cc85f47 commit 1987eac
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 15 deletions.
4 changes: 4 additions & 0 deletions changelogs/unreleased/6079-tsaarni-minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Upstream TLS validation for TCPProxy

TCPProxy now supports validating server certificates of upstream TLS connections.
Set `httpproxy.spec.tcpproxy.services.validation.caSecret` and `subjectName` to enable optional validation.
52 changes: 41 additions & 11 deletions internal/dag/httpproxy_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -952,17 +952,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
}
}
Expand Down Expand Up @@ -1213,6 +1204,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
}

Check warning on line 1212 in internal/dag/httpproxy_processor.go

View check run for this annotation

Codecov / codecov/patch

internal/dag/httpproxy_processor.go#L1211-L1212

Added lines #L1211 - L1212 were not covered by tests
}

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
}

Check warning on line 1223 in internal/dag/httpproxy_processor.go

View check run for this annotation

Codecov / codecov/patch

internal/dag/httpproxy_processor.go#L1220-L1223

Added lines #L1220 - L1223 were not covered by tests
}

proxy.Clusters = append(proxy.Clusters, &Cluster{
Upstream: s,
Weight: uint32(service.Weight),
Expand All @@ -1221,6 +1231,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,
})
}

Expand Down Expand Up @@ -1483,6 +1496,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/'.
//
Expand Down
50 changes: 46 additions & 4 deletions internal/featuretests/v3/backendcavalidation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down Expand Up @@ -60,7 +61,7 @@ func TestClusterServiceTLSBackendCAValidation(t *testing.T) {
}},
},
}
rh.OnAdd(secret)
rh.OnAdd(caSecret)
rh.OnAdd(svc)
rh.OnAdd(p1)

Expand Down Expand Up @@ -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",
},
}},
Expand Down Expand Up @@ -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",
},
}},
Expand Down Expand Up @@ -175,4 +176,45 @@ func TestClusterServiceTLSBackendCAValidation(t *testing.T) {
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,
})

}
31 changes: 31 additions & 0 deletions internal/featuretests/v3/backendclientauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit 1987eac

Please sign in to comment.