diff --git a/apis/contour/v1beta1/ingressroute.go b/apis/contour/v1beta1/ingressroute.go index 77699fb3b71..ca9dc3e6a24 100644 --- a/apis/contour/v1beta1/ingressroute.go +++ b/apis/contour/v1beta1/ingressroute.go @@ -120,9 +120,9 @@ type HealthCheck struct { type TLSVerification struct { // Required, the CA to use for TLS verification CA CA `json:"ca"` - // If specified, at least one of the hostnames must be included in the - // certificate's Subject Alternative Names field - Hostnames []string `json:"hostnames"` + // If specified, the hostname must be included in the certificate's Subject + // Alternative Names field + Hostname string `json:"hostname"` } // TLS verification for the upstream services diff --git a/deployment/common/common.yaml b/deployment/common/common.yaml index 3813de690e8..b886f985b1e 100644 --- a/deployment/common/common.yaml +++ b/deployment/common/common.yaml @@ -161,7 +161,5 @@ spec: type: string pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ # DNS-1123 subdomain hostnames: - type: array - items: - type: string + type: string --- diff --git a/deployment/render/daemonset-rbac.yaml b/deployment/render/daemonset-rbac.yaml index aac935762f3..e8af337f860 100644 --- a/deployment/render/daemonset-rbac.yaml +++ b/deployment/render/daemonset-rbac.yaml @@ -164,9 +164,7 @@ spec: type: string pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ # DNS-1123 subdomain hostnames: - type: array - items: - type: string + type: string --- apiVersion: extensions/v1beta1 kind: DaemonSet diff --git a/deployment/render/deployment-rbac.yaml b/deployment/render/deployment-rbac.yaml index 20952f43a5d..d4c4af31846 100644 --- a/deployment/render/deployment-rbac.yaml +++ b/deployment/render/deployment-rbac.yaml @@ -164,9 +164,7 @@ spec: type: string pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ # DNS-1123 subdomain hostnames: - type: array - items: - type: string + type: string --- apiVersion: extensions/v1beta1 kind: Deployment diff --git a/internal/contour/cluster.go b/internal/contour/cluster.go index c0eca1e2cb9..9a8bdfa16a9 100644 --- a/internal/contour/cluster.go +++ b/internal/contour/cluster.go @@ -97,13 +97,7 @@ func visitClusters(root dag.Vertex) map[string]*v2.Cluster { func (v *clusterVisitor) visit(vertex dag.Vertex) { switch service := vertex.(type) { - case *dag.HTTPService: - name := envoy.Clustername(&service.TCPService) - if _, ok := v.clusters[name]; !ok { - c := envoy.Cluster(service) - v.clusters[c.Name] = c - } - case *dag.TCPService: + case dag.Service: name := envoy.Clustername(service) if _, ok := v.clusters[name]; !ok { c := envoy.Cluster(service) diff --git a/internal/contour/cluster_test.go b/internal/contour/cluster_test.go index 849b5fc29bc..5b91195415a 100644 --- a/internal/contour/cluster_test.go +++ b/internal/contour/cluster_test.go @@ -789,6 +789,352 @@ func TestClusterVisit(t *testing.T) { CommonLbConfig: envoy.ClusterCommonLBConfig(), }), }, + "TLS verification of CA only": { + objs: []interface{}{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + "ca.crt": "CA certificate data", + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example.com", + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{{ + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca"}, + }, + }}, + }}, + }, + }, + serviceWithAnnotations( + "default", + "backend", + map[string]string{ + "contour.heptio.com/upstream-protocol.h2": "443,https", + }, + v1.ServicePort{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }, + ), + }, + want: clustermap( + &v2.Cluster{ + Name: "default/backend/443/f717bfc27a", + AltStatName: "default_backend_443", + Type: v2.Cluster_EDS, + EdsClusterConfig: &v2.Cluster_EdsClusterConfig{ + EdsConfig: envoy.ConfigSource("contour"), + ServiceName: "default/backend/https", + }, + ConnectTimeout: 250 * time.Millisecond, + LbPolicy: v2.Cluster_ROUND_ROBIN, + CommonLbConfig: envoy.ClusterCommonLBConfig(), + TlsContext: envoy.UpstreamTLSContextWithVerification([]byte("CA certificate data"), ""), + Http2ProtocolOptions: &core.Http2ProtocolOptions{}, + }, + ), + }, + "TLS verification of CA and hostname": { + objs: []interface{}{ + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: secretdata("certificate", "key"), + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + "ca.crt": "CA certificate data", + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example.com", + TLS: &ingressroutev1.TLS{ + SecretName: "secret", + }, + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{{ + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca"}, + Hostname: "backend.default.svc", + }, + }}, + }}, + }, + }, + serviceWithAnnotations( + "default", + "backend", + map[string]string{ + "contour.heptio.com/upstream-protocol.h2": "443,https", + }, + v1.ServicePort{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }, + ), + }, + want: clustermap( + &v2.Cluster{ + Name: "default/backend/443/45c43e101f", + AltStatName: "default_backend_443", + Type: v2.Cluster_EDS, + EdsClusterConfig: &v2.Cluster_EdsClusterConfig{ + EdsConfig: envoy.ConfigSource("contour"), + ServiceName: "default/backend/https", + }, + ConnectTimeout: 250 * time.Millisecond, + LbPolicy: v2.Cluster_ROUND_ROBIN, + CommonLbConfig: envoy.ClusterCommonLBConfig(), + TlsContext: envoy.UpstreamTLSContextWithVerification([]byte("CA certificate data"), "backend.default.svc"), + Http2ProtocolOptions: &core.Http2ProtocolOptions{}, + }, + ), + }, + "one service verified by two different CA certificates": { + objs: []interface{}{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca1", + Namespace: "default", + }, + Data: map[string]string{ + "ca.crt": "CA certificate data 1", + }, + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca2", + Namespace: "default", + }, + Data: map[string]string{ + "ca.crt": "CA certificate data 2", + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple1", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example1.com", + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{{ + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca1"}, + Hostname: "backend.default.svc", + }, + }}, + }}, + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple2", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example2.com", + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{{ + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca2"}, + Hostname: "backend.default.svc", + }, + }}, + }}, + }, + }, + serviceWithAnnotations( + "default", + "backend", + map[string]string{ + "contour.heptio.com/upstream-protocol.h2": "443,https", + }, + v1.ServicePort{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }, + ), + }, + want: clustermap( + &v2.Cluster{ + Name: "default/backend/443/72f5a65e99", + AltStatName: "default_backend_443", + Type: v2.Cluster_EDS, + EdsClusterConfig: &v2.Cluster_EdsClusterConfig{ + EdsConfig: envoy.ConfigSource("contour"), + ServiceName: "default/backend/https", + }, + ConnectTimeout: 250 * time.Millisecond, + LbPolicy: v2.Cluster_ROUND_ROBIN, + CommonLbConfig: envoy.ClusterCommonLBConfig(), + TlsContext: envoy.UpstreamTLSContextWithVerification([]byte("CA certificate data 1"), "backend.default.svc"), + Http2ProtocolOptions: &core.Http2ProtocolOptions{}, + }, + &v2.Cluster{ + Name: "default/backend/443/2004857f79", + AltStatName: "default_backend_443", + Type: v2.Cluster_EDS, + EdsClusterConfig: &v2.Cluster_EdsClusterConfig{ + EdsConfig: envoy.ConfigSource("contour"), + ServiceName: "default/backend/https", + }, + ConnectTimeout: 250 * time.Millisecond, + LbPolicy: v2.Cluster_ROUND_ROBIN, + CommonLbConfig: envoy.ClusterCommonLBConfig(), + TlsContext: envoy.UpstreamTLSContextWithVerification([]byte("CA certificate data 2"), "backend.default.svc"), + Http2ProtocolOptions: &core.Http2ProtocolOptions{}, + }, + ), + }, + "one service verified by two different hostnames": { + objs: []interface{}{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + "ca.crt": "CA certificate data", + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple1", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example1.com", + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{{ + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca"}, + Hostname: "backend1.default.svc", + }, + }}, + }}, + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple2", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example2.com", + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{{ + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca"}, + Hostname: "backend2.default.svc", + }, + }}, + }}, + }, + }, + serviceWithAnnotations( + "default", + "backend", + map[string]string{ + "contour.heptio.com/upstream-protocol.h2": "443,https", + }, + v1.ServicePort{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }, + ), + }, + want: clustermap( + &v2.Cluster{ + Name: "default/backend/443/d32859742c", + AltStatName: "default_backend_443", + Type: v2.Cluster_EDS, + EdsClusterConfig: &v2.Cluster_EdsClusterConfig{ + EdsConfig: envoy.ConfigSource("contour"), + ServiceName: "default/backend/https", + }, + ConnectTimeout: 250 * time.Millisecond, + LbPolicy: v2.Cluster_ROUND_ROBIN, + CommonLbConfig: envoy.ClusterCommonLBConfig(), + TlsContext: envoy.UpstreamTLSContextWithVerification([]byte("CA certificate data"), "backend1.default.svc"), + Http2ProtocolOptions: &core.Http2ProtocolOptions{}, + }, + &v2.Cluster{ + Name: "default/backend/443/1bf1802852", + AltStatName: "default_backend_443", + Type: v2.Cluster_EDS, + EdsClusterConfig: &v2.Cluster_EdsClusterConfig{ + EdsConfig: envoy.ConfigSource("contour"), + ServiceName: "default/backend/https", + }, + ConnectTimeout: 250 * time.Millisecond, + LbPolicy: v2.Cluster_ROUND_ROBIN, + CommonLbConfig: envoy.ClusterCommonLBConfig(), + TlsContext: envoy.UpstreamTLSContextWithVerification([]byte("CA certificate data"), "backend2.default.svc"), + Http2ProtocolOptions: &core.Http2ProtocolOptions{}, + }, + ), + }, } for name, tc := range tests { diff --git a/internal/contour/route_test.go b/internal/contour/route_test.go index 788d23089da..f5c03897dc4 100644 --- a/internal/contour/route_test.go +++ b/internal/contour/route_test.go @@ -1689,6 +1689,233 @@ func TestRouteVisit(t *testing.T) { }, }, }, + "ingressroute with TLS verification": { + objs: []interface{}{ + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: secretdata("certificate", "key"), + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + "ca.crt": "CA certificate data", + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example.com", + TLS: &ingressroutev1.TLS{ + SecretName: "secret", + }, + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{ + { + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca"}, + Hostname: "backend.default.svc", + }, + }, + }, + }}, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + Annotations: map[string]string{ + "contour.heptio.com/upstream-protocol.h2": "443,https", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }}, + }, + }, + }, + want: map[string]*v2.RouteConfiguration{ + "ingress_http": { + Name: "ingress_http", + VirtualHosts: []route.VirtualHost{{ + Name: "www.example.com", + Domains: []string{"www.example.com", "www.example.com:80"}, + Routes: []route.Route{{ + Match: envoy.PrefixMatch("/"), + Action: &route.Route_Redirect{ + Redirect: &route.RedirectAction{ + HttpsRedirect: true, + }, + }, + }}, + }}, + }, + "ingress_https": { + Name: "ingress_https", + VirtualHosts: []route.VirtualHost{{ + Name: "www.example.com", + Domains: []string{"www.example.com", "www.example.com:443"}, + Routes: []route.Route{{ + Match: envoy.PrefixMatch("/"), + Action: routecluster("default/backend/443/45c43e101f"), + }}, + }}, + }, + }, + }, + "ingressroute where TLS verification has non-existant configmap": { + objs: []interface{}{ + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: secretdata("certificate", "key"), + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example.com", + TLS: &ingressroutev1.TLS{ + SecretName: "secret", + }, + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{ + { + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca"}, + Hostname: "backend.default.svc", + }, + }, + }, + }}, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + Annotations: map[string]string{ + "contour.heptio.com/upstream-protocol.h2": "443,https", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }}, + }, + }, + }, + want: map[string]*v2.RouteConfiguration{ + "ingress_http": { + Name: "ingress_http", // expected to be empty, configmap was not found + }, + "ingress_https": { + Name: "ingress_https", // expected to be empty, configmap was not found + }, + }, + }, + "ingressroute where TLS verification has configmap with missing ca.crt key": { + objs: []interface{}{ + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "default", + }, + Data: secretdata("certificate", "key"), + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + Data: map[string]string{ + "wrong-key": "CA certificate data", + }, + }, + &ingressroutev1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "default", + }, + Spec: ingressroutev1.IngressRouteSpec{ + VirtualHost: &ingressroutev1.VirtualHost{ + Fqdn: "www.example.com", + TLS: &ingressroutev1.TLS{ + SecretName: "secret", + }, + }, + Routes: []ingressroutev1.Route{{ + Match: "/", + Services: []ingressroutev1.Service{ + { + Name: "backend", + Port: 443, + TLSVerification: &ingressroutev1.TLSVerification{ + CA: ingressroutev1.CA{ConfigMapName: "ca"}, + Hostname: "backend.default.svc", + }, + }, + }, + }}, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: "default", + Annotations: map[string]string{ + "contour.heptio.com/upstream-protocol.h2": "443,https", + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }}, + }, + }, + }, + want: map[string]*v2.RouteConfiguration{ + "ingress_http": { + Name: "ingress_http", // expected to be empty, configmap is missing the "ca.crt" key + }, + "ingress_https": { + Name: "ingress_https", // expected to be empty, configmap is missing the "ca.crt" key + }, + }, + }, } for name, tc := range tests { diff --git a/internal/dag/builder.go b/internal/dag/builder.go index 86c8eb42af0..5580628abde 100644 --- a/internal/dag/builder.go +++ b/internal/dag/builder.go @@ -164,8 +164,8 @@ type builder struct { } // lookupHTTPService returns a HTTPService that matches the meta and port supplied. -func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, weight int, strategy string, hc *ingressroutev1.HealthCheck) *HTTPService { - s := b.lookupService(m, port, weight, strategy, hc) +func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, weight int, strategy string, hc *ingressroutev1.HealthCheck, ca *ConfigMap, hostname string) *HTTPService { + s := b.lookupService(m, port, weight, strategy, hc, ca, hostname) switch s := s.(type) { case *HTTPService: return s @@ -177,10 +177,10 @@ func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, weight int, for i := range svc.Spec.Ports { p := &svc.Spec.Ports[i] if int(p.Port) == port.IntValue() { - return b.addHTTPService(svc, p, weight, strategy, hc) + return b.addHTTPService(svc, p, weight, strategy, hc, ca, hostname) } if port.String() == p.Name { - return b.addHTTPService(svc, p, weight, strategy, hc) + return b.addHTTPService(svc, p, weight, strategy, hc, ca, hostname) } } return nil @@ -192,7 +192,7 @@ func (b *builder) lookupHTTPService(m meta, port intstr.IntOrString, weight int, // lookupTCPService returns a TCPService that matches the meta and port supplied. func (b *builder) lookupTCPService(m meta, port intstr.IntOrString, weight int, strategy string, hc *ingressroutev1.HealthCheck) *TCPService { - s := b.lookupService(m, port, weight, strategy, hc) + s := b.lookupService(m, port, weight, strategy, hc, nil, "") switch s := s.(type) { case *TCPService: return s @@ -216,18 +216,24 @@ func (b *builder) lookupTCPService(m meta, port intstr.IntOrString, weight int, return nil } } -func (b *builder) lookupService(m meta, port intstr.IntOrString, weight int, strategy string, hc *ingressroutev1.HealthCheck) Service { +func (b *builder) lookupService(m meta, port intstr.IntOrString, weight int, strategy string, hc *ingressroutev1.HealthCheck, ca *ConfigMap, hostname string) Service { if port.Type != intstr.Int { // can't handle, give up return nil } + var caMeta meta + if ca != nil { + caMeta = ca.toMeta() + } sm := servicemeta{ - name: m.name, - namespace: m.namespace, - port: int32(port.IntValue()), - weight: weight, - strategy: strategy, - healthcheck: healthcheckToString(hc), + Name: m.name, + Namespace: m.namespace, + Port: int32(port.IntValue()), + Weight: weight, + Strategy: strategy, + Healthcheck: healthcheckToString(hc), + CA: caMeta, + Hostname: hostname, } s, ok := b.services[sm] if !ok { @@ -240,7 +246,7 @@ func healthcheckToString(hc *ingressroutev1.HealthCheck) string { return fmt.Sprintf("%#v", hc) } -func (b *builder) addHTTPService(svc *v1.Service, port *v1.ServicePort, weight int, strategy string, hc *ingressroutev1.HealthCheck) *HTTPService { +func (b *builder) addHTTPService(svc *v1.Service, port *v1.ServicePort, weight int, strategy string, hc *ingressroutev1.HealthCheck, ca *ConfigMap, hostname string) *HTTPService { if b.services == nil { b.services = make(map[servicemeta]Service) } @@ -264,7 +270,9 @@ func (b *builder) addHTTPService(svc *v1.Service, port *v1.ServicePort, weight i MaxRetries: parseAnnotation(svc.Annotations, annotationMaxRetries), HealthCheck: hc, }, - Protocol: protocol, + Protocol: protocol, + CACertificate: ca, + Hostname: hostname, } b.services[s.toMeta()] = s return s @@ -419,7 +427,7 @@ func (b *builder) compute() *DAG { r := prefixRoute(ing, prefix) m := meta{name: httppath.Backend.ServiceName, namespace: ing.Namespace} - if s := b.lookupHTTPService(m, httppath.Backend.ServicePort, 0, "", nil); s != nil { + if s := b.lookupHTTPService(m, httppath.Backend.ServicePort, 0, "", nil, nil, ""); s != nil { r.addHTTPService(s) } @@ -661,23 +669,42 @@ func (b *builder) processIngressRoute(ir *ingressroutev1.IngressRoute, prefixMat b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: fmt.Sprintf("route %q: service %q: weight must be greater than or equal to zero", route.Match, service.Name), Vhost: host}) return } + m := meta{name: service.Name, namespace: ir.Namespace} - if s := b.lookupHTTPService(m, intstr.FromInt(service.Port), service.Weight, service.Strategy, service.HealthCheck); s != nil { - if service.TLSVerification != nil { - m := meta{name: service.TLSVerification.CA.ConfigMapName, namespace: ir.Namespace} - cm := b.lookupConfigMap(m) + var s *HTTPService + if service.TLSVerification == nil { + s = b.lookupHTTPService(m, intstr.FromInt(service.Port), service.Weight, service.Strategy, service.HealthCheck, nil, "") + } else { + cmMeta := meta{name: service.TLSVerification.CA.ConfigMapName, namespace: ir.Namespace} + cm := b.lookupConfigMap(cmMeta) + s = b.lookupHTTPService(m, intstr.FromInt(service.Port), service.Weight, service.Strategy, service.HealthCheck, cm, service.TLSVerification.Hostname) + if s != nil { if cm == nil || cm.Data() == nil { - b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: fmt.Sprintf("route %q: service %q: failed to read configmap %s/%s", route.Match, service.Name, ir.Namespace, service.TLSVerification.CA.ConfigMapName), Vhost: host}) + status := Status{ + Object: ir, + Status: StatusInvalid, + Description: fmt.Sprintf("route %q: service %q: failed to read configmap %s/%s", route.Match, service.Name, ir.Namespace, service.TLSVerification.CA.ConfigMapName), + Vhost: host, + } + b.setStatus(status) return - } else if cm.Data()["ca.crt"] == "" { - b.setStatus(Status{Object: ir, Status: StatusInvalid, Description: fmt.Sprintf("route %q: service %q: configmap %s/%s is missing key \"ca.crt\"", route.Match, service.Name, ir.Namespace, service.TLSVerification.CA.ConfigMapName), Vhost: host}) + } else if cm.Data()[CACertKey] == "" { + status := Status{ + Object: ir, + Status: StatusInvalid, + Description: fmt.Sprintf("route %q: service %q: configmap %s/%s is missing key \"%s\"", route.Match, service.Name, ir.Namespace, service.TLSVerification.CA.ConfigMapName, CACertKey), + Vhost: host, + } + b.setStatus(status) return } s.Protocol = "h2" s.CACertificate = cm - s.Hostnames = service.TLSVerification.Hostnames + s.Hostname = service.TLSVerification.Hostname } + } + if s != nil { r.addHTTPService(s) } } diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index 69ac4cf4a5f..9889737d678 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -2446,7 +2446,7 @@ func TestBuilderLookupHTTPService(t *testing.T) { }, }, } - got := b.lookupHTTPService(tc.meta, tc.port, tc.weight, tc.strategy, tc.healthcheck) + got := b.lookupHTTPService(tc.meta, tc.port, tc.weight, tc.strategy, tc.healthcheck, nil, "") if diff := cmp.Diff(tc.want, got); diff != "" { t.Fatal(diff) } diff --git a/internal/dag/dag.go b/internal/dag/dag.go index cc2eb6ec646..b990224e18c 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -154,6 +154,7 @@ type Vertex interface { type Service interface { Vertex toMeta() servicemeta + GetTCPService() *TCPService } // TCPProxy represents a cluster of TCP endpoints. @@ -202,25 +203,52 @@ type TCPService struct { } type servicemeta struct { - name string - namespace string - port int32 - weight int - strategy string - healthcheck string // %#v of *ingressroutev1.HealthCheck + Name string + Namespace string + Port int32 + Weight int + Strategy string + Healthcheck string // %#v of *ingressroutev1.HealthCheck + CA meta + Hostname string +} + +func (s *HTTPService) toMeta() servicemeta { + var caMeta meta + if s.CACertificate != nil { + caMeta = s.CACertificate.toMeta() + } + return servicemeta{ + Name: s.Name, + Namespace: s.Namespace, + Port: s.Port, + Weight: s.Weight, + Strategy: s.LoadBalancerStrategy, + Healthcheck: healthcheckToString(s.HealthCheck), + CA: caMeta, + Hostname: s.Hostname, + } } func (s *TCPService) toMeta() servicemeta { return servicemeta{ - name: s.Name, - namespace: s.Namespace, - port: s.Port, - weight: s.Weight, - strategy: s.LoadBalancerStrategy, - healthcheck: healthcheckToString(s.HealthCheck), + Name: s.Name, + Namespace: s.Namespace, + Port: s.Port, + Weight: s.Weight, + Strategy: s.LoadBalancerStrategy, + Healthcheck: healthcheckToString(s.HealthCheck), } } +func (s *HTTPService) GetTCPService() *TCPService { + return &s.TCPService +} + +func (s *TCPService) GetTCPService() *TCPService { + return s +} + func (s *TCPService) Visit(func(Vertex)) { // TCPServices are leaves in the DAG. } @@ -237,11 +265,14 @@ type HTTPService struct { // ConfigMap that holds the CA certificate used for TLS verification CACertificate *ConfigMap - // If specified, at least one of the hostnames must be included in the - // certificate's Subject Alternative Names field - Hostnames []string + // If specified, the hostname must included in the certificate's Subject + // Alternative Names field + Hostname string } +// The key used in the ConfigMap to hold the CA certificate +var CACertKey = "ca.crt" + // ConfigMap represents a K8s ConfigMap as a DAG Vertex. A ConfigMap is // a leaf in the DAG. type ConfigMap struct { diff --git a/internal/envoy/auth.go b/internal/envoy/auth.go index d667381e413..9c9e96cf4fb 100644 --- a/internal/envoy/auth.go +++ b/internal/envoy/auth.go @@ -28,7 +28,14 @@ func UpstreamTLSContext() *auth.UpstreamTlsContext { } // UpstreamTLSContext creates an ALPN h2 enabled TLS Context with TLS verification enabled. -func UpstreamTLSContextWithVerification(cert []byte, hostnames []string) *auth.UpstreamTlsContext { +func UpstreamTLSContextWithVerification(cert []byte, hostname string) *auth.UpstreamTlsContext { + var hostnames []string + if hostname == "" { + hostnames = []string{} + } else { + hostnames = []string{hostname} + } + return &auth.UpstreamTlsContext{ CommonTlsContext: &auth.CommonTlsContext{ ValidationContextType: &auth.CommonTlsContext_ValidationContext{ diff --git a/internal/envoy/cluster.go b/internal/envoy/cluster.go index 6a26ef0fd24..053459aad59 100644 --- a/internal/envoy/cluster.go +++ b/internal/envoy/cluster.go @@ -42,7 +42,7 @@ func Cluster(s dag.Service) *v2.Cluster { } func httpCluster(service *dag.HTTPService) *v2.Cluster { - c := cluster(&service.TCPService) + c := cluster(service) switch service.Protocol { case "h2": if service.CACertificate == nil { @@ -52,7 +52,7 @@ func httpCluster(service *dag.HTTPService) *v2.Cluster { if cert := cm["ca.crt"]; cert == "" { c.TlsContext = UpstreamTLSContext() } else { - c.TlsContext = UpstreamTLSContextWithVerification([]byte(cert), service.Hostnames) + c.TlsContext = UpstreamTLSContextWithVerification([]byte(cert), service.Hostname) } } fallthrough @@ -62,24 +62,26 @@ func httpCluster(service *dag.HTTPService) *v2.Cluster { return c } -func cluster(service *dag.TCPService) *v2.Cluster { +func cluster(service dag.Service) *v2.Cluster { + tcpService := service.GetTCPService() + c := &v2.Cluster{ Name: Clustername(service), - AltStatName: altStatName(service), + AltStatName: altStatName(tcpService), Type: v2.Cluster_EDS, - EdsClusterConfig: edsconfig("contour", service), + EdsClusterConfig: edsconfig("contour", tcpService), ConnectTimeout: 250 * time.Millisecond, - LbPolicy: lbPolicy(service.LoadBalancerStrategy), + LbPolicy: lbPolicy(tcpService.LoadBalancerStrategy), CommonLbConfig: ClusterCommonLBConfig(), - HealthChecks: edshealthcheck(service), + HealthChecks: edshealthcheck(tcpService), } - if anyPositive(service.MaxConnections, service.MaxPendingRequests, service.MaxRequests, service.MaxRetries) { + if anyPositive(tcpService.MaxConnections, tcpService.MaxPendingRequests, tcpService.MaxRequests, tcpService.MaxRetries) { c.CircuitBreakers = &envoy_cluster.CircuitBreakers{ Thresholds: []*envoy_cluster.CircuitBreakers_Thresholds{{ - MaxConnections: u32nil(service.MaxConnections), - MaxPendingRequests: u32nil(service.MaxPendingRequests), - MaxRequests: u32nil(service.MaxRequests), - MaxRetries: u32nil(service.MaxRetries), + MaxConnections: u32nil(tcpService.MaxConnections), + MaxPendingRequests: u32nil(tcpService.MaxPendingRequests), + MaxRequests: u32nil(tcpService.MaxRequests), + MaxRetries: u32nil(tcpService.MaxRetries), }}, } } @@ -126,9 +128,20 @@ func edshealthcheck(s *dag.TCPService) []*core.HealthCheck { } // Clustername returns the name of the CDS cluster for this service. -func Clustername(service *dag.TCPService) string { - buf := service.LoadBalancerStrategy - if hc := service.HealthCheck; hc != nil { +func Clustername(service dag.Service) string { + var buf string + + if httpService, ok := service.(*dag.HTTPService); ok { + if httpService.CACertificate != nil { + buf += httpService.CACertificate.Name() + buf += httpService.CACertificate.Namespace() + } + buf += httpService.Hostname + } + + tcpService := service.GetTCPService() + buf += tcpService.LoadBalancerStrategy + if hc := tcpService.HealthCheck; hc != nil { if hc.TimeoutSeconds > 0 { buf += (time.Duration(hc.TimeoutSeconds) * time.Second).String() } @@ -145,9 +158,10 @@ func Clustername(service *dag.TCPService) string { } hash := sha1.Sum([]byte(buf)) - ns := service.Namespace - name := service.Name - return hashname(60, ns, name, strconv.Itoa(int(service.Port)), fmt.Sprintf("%x", hash[:5])) + ns := tcpService.Namespace + name := tcpService.Name + port := strconv.Itoa(int(tcpService.Port)) + return hashname(60, ns, name, port, fmt.Sprintf("%x", hash[:5])) } // altStatName generates an alternative stat name for the service diff --git a/internal/envoy/cluster_test.go b/internal/envoy/cluster_test.go index 3aad4ef2a15..332a1fba58c 100644 --- a/internal/envoy/cluster_test.go +++ b/internal/envoy/cluster_test.go @@ -242,7 +242,7 @@ func TestCluster(t *testing.T) { func TestClustername(t *testing.T) { tests := map[string]struct { - service *dag.TCPService + service dag.Service want string }{ "simple": { @@ -292,6 +292,29 @@ func TestClustername(t *testing.T) { }, want: "default/backend/80/32737eb011", }, + "various TLS verification params": { + service: &dag.HTTPService{ + TCPService: dag.TCPService{ + Name: "backend", + Namespace: "default", + ServicePort: &v1.ServicePort{ + Name: "https", + Protocol: "TCP", + Port: 443, + TargetPort: intstr.FromInt(8443), + }, + }, + CACertificate: &dag.ConfigMap{ + Object: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca", + Namespace: "default", + }, + }, + }, + }, + want: "default/backend/443/f717bfc27a", + }, } for name, tc := range tests { diff --git a/internal/envoy/route.go b/internal/envoy/route.go index 48473927127..6c42c7f1e64 100644 --- a/internal/envoy/route.go +++ b/internal/envoy/route.go @@ -37,7 +37,7 @@ func RouteRoute(r *dag.Route, services []*dag.HTTPService) *route.Route_Route { switch len(services) { case 1: ra.ClusterSpecifier = &route.RouteAction_Cluster{ - Cluster: Clustername(&services[0].TCPService), + Cluster: Clustername(services[0]), } ra.RequestHeadersToAdd = headers( appendHeader("x-request-start", "t=%START_TIME(%s.%3f)%"), @@ -99,7 +99,7 @@ func weightedClusters(services []*dag.HTTPService) *route.WeightedCluster { for _, service := range services { total += service.Weight wc.Clusters = append(wc.Clusters, &route.WeightedCluster_ClusterWeight{ - Name: Clustername(&service.TCPService), + Name: Clustername(service), Weight: u32(service.Weight), RequestHeadersToAdd: headers( appendHeader("x-request-start", "t=%START_TIME(%s.%3f)%"),