CVE-2021-32783 (published 2021-07-23T22:15Z)

Contour is a Kubernetes ingress controller using Envoy proxy. In Contour before version 1.17.1 a specially crafted ExternalName type Service may be used to access Envoy's admin interface, which Contour normally prevents from access outside the Envoy container. This can be used to shut down Envoy remotely (a denial of service), or to expose the existence of any Secret that Envoy is using for its configuration, including most notably TLS Keypairs. However, it *cannot* be used to get the *content* of those secrets. Since this attack allows access to the administration interface, a variety of administration options are available, such as shutting down the Envoy or draining traffic. In general, the Envoy admin interface cannot easily be used for making changes to the cluster, in-flight requests, or backend services, but it could be used to shut down or drain Envoy, change traffic routing, or to retrieve secret metadata, as mentioned above. The issue will be addressed in Contour v1.18.0 and a cherry-picked patch release, v1.17.1, has been released to cover users who cannot upgrade at this time. For more details refer to the linked GitHub Security Advisory.

Severity: MEDIUM
CVSS v2 base score: 5.5 (AV:N/AC:L/Au:S/C:P/I:N/A:P)
CVSS v3 base score: 8.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:H)

Weakness information:
cwe_id: CWE-610
cwe_name: Externally Controlled Reference to a Resource in Another Sphere
description: The product uses an externally controlled name or reference that resolves to a resource that is outside of the intended control sphere.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/projectcontour/contour
commit_details: repo_url: https://github.com/projectcontour/contour
author: Steve Kriss
author_date: 2021-07-22 12:04:46-06:00
msg: cherrypicks for v1.17.1 (#3909)

* Merge pull request from GHSA-5ph6-qq5x-7jwc

Signed-off-by: Nick Young <ynick@vmware.com>

* Fix spelling and lint errors that slipped into the ExternalName PR (#3908)

Signed-off-by: Nick Young <ynick@vmware.com>

Co-authored-by: Nick Young <ynick@vmware.com>
repository: repo_url: https://github.com/projectcontour/contour
repo_name: projectcontour/contour
description: Contour is a Kubernetes ingress controller using Envoy proxy.
repo_language: Go
owner: projectcontour
file_changes: filename: serve.go
change_type: ModificationType.MODIFY
diff: @@ -714,29 +714,35 @@ func getDAGBuilder(ctx *serveContext, clients *k8s.Clients, clientCert, fallback
 		responseHeadersPolicy.Remove = append(responseHeadersPolicy.Remove, ctx.Config.Policy.ResponseHeadersPolicy.Remove...)
 	}
 
+	log.Debugf("EnableExternalNameService is set to %t", ctx.Config.EnableExternalNameService)
 	// Get the appropriate DAG processors.
 	dagProcessors := []dag.Processor{
 		&dag.IngressProcessor{
-			FieldLogger:       log.WithField("context", "IngressProcessor"),
-			ClientCertificate: clientCert,
+			EnableExternalNameService: ctx.Config.EnableExternalNameService,
+			FieldLogger:               log.WithField("context", "IngressProcessor"),
+			ClientCertificate:         clientCert,
 		},
 		&dag.ExtensionServiceProcessor{
+			// Note that ExtensionService does not support ExternalName, if it does get added,
+			// need to bring EnableExternalNameService in here too.
 			FieldLogger:       log.WithField("context", "ExtensionServiceProcessor"),
 			ClientCertificate: clientCert,
 		},
 		&dag.HTTPProxyProcessor{
-			DisablePermitInsecure: ctx.Config.DisablePermitInsecure,
-			FallbackCertificate:   fallbackCert,
-			DNSLookupFamily:       ctx.Config.Cluster.DNSLookupFamily,
-			ClientCertificate:     clientCert,
-			RequestHeadersPolicy:  &requestHeadersPolicy,
-			ResponseHeadersPolicy: &responseHeadersPolicy,
+			EnableExternalNameService: ctx.Config.EnableExternalNameService,
+			DisablePermitInsecure:     ctx.Config.DisablePermitInsecure,
+			FallbackCertificate:       fallbackCert,
+			DNSLookupFamily:           ctx.Config.Cluster.DNSLookupFamily,
+			ClientCertificate:         clientCert,
+			RequestHeadersPolicy:      &requestHeadersPolicy,
+			ResponseHeadersPolicy:     &responseHeadersPolicy,
 		},
 	}
 
 	if ctx.Config.GatewayConfig != nil && clients.ResourcesExist(k8s.GatewayAPIResources()...) {
 		dagProcessors = append(dagProcessors, &dag.GatewayAPIProcessor{
-			FieldLogger: log.WithField("context", "GatewayAPIProcessor"),
+			EnableExternalNameService: ctx.Config.EnableExternalNameService,
+			FieldLogger:               log.WithField("context", "GatewayAPIProcessor"),
 		})
 	}
 

programming_language: Go
filename: 01-contour-config.yaml
change_type: ModificationType.MODIFY
diff: @@ -51,6 +51,13 @@ data:
     # leaderelection:
     #   configmap-name: leader-elect
     #   configmap-namespace: projectcontour
+    ####
+    # ExternalName Services are disabled by default due to CVE-2021-XXXXX
+    # You can re-enable them by setting this setting to `true`.
+    # This is not recommended without understanding the security implications.
+    # Please see the advisory at https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for the details.
+    # enableExternalNameService: false
+    ## 
     ### Logging options
     # Default setting
     accesslog-format: envoy

programming_language: CoffeeScript
filename: contour-gateway.yaml
change_type: ModificationType.MODIFY
diff: @@ -88,6 +88,13 @@ data:
     # leaderelection:
     #   configmap-name: leader-elect
     #   configmap-namespace: projectcontour
+    ####
+    # ExternalName Services are disabled by default due to CVE-2021-XXXXX
+    # You can re-enable them by setting this setting to `true`.
+    # This is not recommended without understanding the security implications.
+    # Please see the advisory at https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for the details.
+    # enableExternalNameService: false
+    ##
     ### Logging options
     # Default setting
     accesslog-format: envoy

programming_language: CoffeeScript
filename: contour.yaml
change_type: ModificationType.MODIFY
diff: @@ -85,6 +85,13 @@ data:
     # leaderelection:
     #   configmap-name: leader-elect
     #   configmap-namespace: projectcontour
+    ####
+    # ExternalName Services are disabled by default due to CVE-2021-XXXXX
+    # You can re-enable them by setting this setting to `true`.
+    # This is not recommended without understanding the security implications.
+    # Please see the advisory at https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for the details.
+    # enableExternalNameService: false
+    ##
     ### Logging options
     # Default setting
     accesslog-format: envoy

programming_language: CoffeeScript
filename: go.mod
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,7 @@ go 1.15
 require (
 	github.com/ahmetb/gen-crd-api-reference-docs v0.3.0
 	github.com/bombsimon/logrusr v1.0.0
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/envoyproxy/go-control-plane v0.9.9-0.20210111201334-f1f47757da33
 	github.com/go-logr/logr v0.4.0
 	github.com/golang/protobuf v1.5.2

programming_language: Go
filename: accessors.go
change_type: ModificationType.MODIFY
diff: @@ -51,12 +51,17 @@ func (dag *DAG) GetService(meta types.NamespacedName, port int32) *Service {
 // EnsureService looks for a Kubernetes service in the cache matching the provided
 // namespace, name and port, and returns a DAG service for it. If a matching service
 // cannot be found in the cache, an error is returned.
-func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache) (*Service, error) {
+func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString, cache *KubernetesCache, enableExternalNameSvc bool) (*Service, error) {
 	svc, svcPort, err := cache.LookupService(meta, port)
 	if err != nil {
 		return nil, err
 	}
 
+	err = validateExternalName(svc, enableExternalNameSvc)
+	if err != nil {
+		return nil, err
+	}
+
 	if dagSvc := dag.GetService(k8s.NamespacedNameOf(svc), svcPort.Port); dagSvc != nil {
 		return dagSvc, nil
 	}
@@ -78,6 +83,38 @@ func (dag *DAG) EnsureService(meta types.NamespacedName, port intstr.IntOrString
 	return dagSvc, nil
 }
 
+func validateExternalName(svc *v1.Service, enableExternalNameSvc bool) error {
+
+	// If this isn't an ExternalName Service, we're all good here.
+	en := externalName(svc)
+	if en == "" {
+		return nil
+	}
+
+	// If ExternalNames are disabled, then we don't want to add this to the DAG.
+	if !enableExternalNameSvc {
+		return fmt.Errorf("%s/%s is an ExternalName service, these are not currently enabled. See the config.enableExternalNameService config file setting", svc.Namespace, svc.Name)
+	}
+
+	// Check against a list of known localhost names, using a map to approximate a set.
+	// TODO(youngnick) This is a very porous hack, and we should probably look into doing a DNS
+	// lookup to check what the externalName resolves to, but I'm worried about the
+	// performance impact of doing one or more DNS lookups per DAG run, so we're
+	// going to go with a specific blocklist for now.
+	localhostNames := map[string]struct{}{
+		"localhost":               {},
+		"localhost.localdomain":   {},
+		"local.projectcontour.io": {},
+	}
+
+	_, localhost := localhostNames[en]
+	if localhost {
+		return fmt.Errorf("%s/%s is an ExternalName service that points to localhost, this is not allowed", svc.Namespace, svc.Name)
+	}
+
+	return nil
+}
+
 func upstreamProtocol(svc *v1.Service, port v1.ServicePort) string {
 	up := annotation.ParseUpstreamProtocols(svc.Annotations)
 	protocol := up[port.Name]

programming_language: Go
filename: accessors_test.go
change_type: ModificationType.MODIFY
diff: @@ -40,15 +40,53 @@ func TestBuilderLookupService(t *testing.T) {
 			}},
 		},
 	}
+
+	externalNameValid := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "externalnamevalid",
+			Namespace: "default",
+		},
+		Spec: v1.ServiceSpec{
+			Type:         v1.ServiceTypeExternalName,
+			ExternalName: "external.projectcontour.io",
+			Ports: []v1.ServicePort{{
+				Name:       "http",
+				Protocol:   "TCP",
+				Port:       80,
+				TargetPort: intstr.FromInt(80),
+			}},
+		},
+	}
+
+	externalNameLocalhost := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "externalnamelocalhost",
+			Namespace: "default",
+		},
+		Spec: v1.ServiceSpec{
+			Type:         v1.ServiceTypeExternalName,
+			ExternalName: "localhost",
+			Ports: []v1.ServicePort{{
+				Name:       "http",
+				Protocol:   "TCP",
+				Port:       80,
+				TargetPort: intstr.FromInt(80),
+			}},
+		},
+	}
+
 	services := map[types.NamespacedName]*v1.Service{
-		{Name: "service1", Namespace: "default"}: s1,
+		{Name: "service1", Namespace: "default"}:              s1,
+		{Name: "externalnamevalid", Namespace: "default"}:     externalNameValid,
+		{Name: "externalnamelocalhost", Namespace: "default"}: externalNameLocalhost,
 	}
 
 	tests := map[string]struct {
 		types.NamespacedName
-		port    intstr.IntOrString
-		want    *Service
-		wantErr error
+		port                  intstr.IntOrString
+		enableExternalNameSvc bool
+		want                  *Service
+		wantErr               error
 	}{
 		"lookup service by port number": {
 			NamespacedName: types.NamespacedName{Name: "service1", Namespace: "default"},
@@ -80,6 +118,36 @@ func TestBuilderLookupService(t *testing.T) {
 			port:           intstr.FromString("9999"),
 			wantErr:        errors.New(`port "9999" on service "default/service1" not matched`),
 		},
+		"When ExternalName Services are not disabled no error is returned": {
+			NamespacedName: types.NamespacedName{Name: "externalnamevalid", Namespace: "default"},
+			port:           intstr.FromString("80"),
+			want: &Service{
+				Weighted: WeightedService{
+					Weight:           1,
+					ServiceName:      "externalnamevalid",
+					ServiceNamespace: "default",
+					ServicePort: v1.ServicePort{
+						Name:       "http",
+						Protocol:   "TCP",
+						Port:       80,
+						TargetPort: intstr.FromInt(80),
+					},
+				},
+				ExternalName: "external.projectcontour.io",
+			},
+			enableExternalNameSvc: true,
+		},
+		"When ExternalName Services are disabled an error is returned": {
+			NamespacedName: types.NamespacedName{Name: "externalnamevalid", Namespace: "default"},
+			port:           intstr.FromString("80"),
+			wantErr:        errors.New(`default/externalnamevalid is an ExternalName service, these are not currently enabled. See the config.enableExternalNameService config file setting`),
+		},
+		"When ExternalName Services are enabled but a localhost ExternalName is used an error is returned": {
+			NamespacedName:        types.NamespacedName{Name: "externalnamelocalhost", Namespace: "default"},
+			port:                  intstr.FromString("80"),
+			wantErr:               errors.New(`default/externalnamelocalhost is an ExternalName service that points to localhost, this is not allowed`),
+			enableExternalNameSvc: true,
+		},
 	}
 
 	for name, tc := range tests {
@@ -93,7 +161,7 @@ func TestBuilderLookupService(t *testing.T) {
 
 			var dag DAG
 
-			got, gotErr := dag.EnsureService(tc.NamespacedName, tc.port, &b.Source)
+			got, gotErr := dag.EnsureService(tc.NamespacedName, tc.port, &b.Source, tc.enableExternalNameSvc)
 			assert.Equal(t, tc.want, got)
 			assert.Equal(t, tc.wantErr, gotErr)
 		})

programming_language: Go
filename: builder_test.go
change_type: ModificationType.MODIFY
diff: @@ -6536,6 +6536,25 @@ func TestDAGInsert(t *testing.T) {
 		},
 	}
 
+	ingressExternalNameService := &networking_v1.Ingress{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "externalname",
+			Namespace: "default",
+		},
+		Spec: networking_v1.IngressSpec{
+			Rules: []networking_v1.IngressRule{{
+				Host: "example.com",
+				IngressRuleValue: networking_v1.IngressRuleValue{
+					HTTP: &networking_v1.HTTPIngressRuleValue{
+						Paths: []networking_v1.HTTPIngressPath{{
+							Backend: *backendv1(s14.GetName(), intstr.FromInt(80)),
+						}},
+					},
+				},
+			}},
+		},
+	}
+
 	proxyExternalNameService := &contour_api_v1.HTTPProxy{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "example-com",
@@ -6582,6 +6601,7 @@ func TestDAGInsert(t *testing.T) {
 	tests := map[string]struct {
 		objs                         []interface{}
 		disablePermitInsecure        bool
+		enableExternalNameSvc        bool
 		fallbackCertificateName      string
 		fallbackCertificateNamespace string
 		want                         []Vertex
@@ -8980,11 +9000,48 @@ func TestDAGInsert(t *testing.T) {
 				},
 			),
 		},
+		"insert ingress with externalName service": {
+			objs: []interface{}{
+				ingressExternalNameService,
+				s14,
+			},
+			enableExternalNameSvc: true,
+			want: listeners(
+				&Listener{
+					Port: 80,
+					VirtualHosts: virtualhosts(
+						virtualhost("example.com", &Route{
+							PathMatchCondition: prefixString("/"),
+							Clusters: []*Cluster{{
+								Upstream: &Service{
+									ExternalName: "externalservice.io",
+									Weighted: WeightedService{
+										Weight:           1,
+										ServiceName:      s14.Name,
+										ServiceNamespace: s14.Namespace,
+										ServicePort:      s14.Spec.Ports[0],
+									},
+								},
+							}},
+						}),
+					),
+				},
+			),
+		},
+		"insert ingress with externalName service, but externalName services disabled": {
+			objs: []interface{}{
+				ingressExternalNameService,
+				s14,
+			},
+			enableExternalNameSvc: false,
+			want:                  listeners(),
+		},
 		"insert proxy with externalName service": {
 			objs: []interface{}{
 				proxyExternalNameService,
 				s14,
 			},
+			enableExternalNameSvc: true,
 			want: listeners(
 				&Listener{
 					Port: 80,
@@ -9014,6 +9071,7 @@ func TestDAGInsert(t *testing.T) {
 				s14,
 				sec1,
 			},
+			enableExternalNameSvc: true,
 			want: listeners(
 				&Listener{
 					Port: 443,
@@ -9073,6 +9131,7 @@ func TestDAGInsert(t *testing.T) {
 				proxyReplaceHostHeaderRoute,
 				s14,
 			},
+			enableExternalNameSvc: true,
 			want: listeners(
 				&Listener{
 					Port: 80,
@@ -9111,7 +9170,8 @@ func TestDAGInsert(t *testing.T) {
 				proxyReplaceHostHeaderService,
 				s14,
 			},
-			want: listeners(),
+			enableExternalNameSvc: true,
+			want:                  listeners(),
 		},
 		"insert proxy with response header policy - route - host header": {
 			objs: []interface{}{
@@ -9754,10 +9814,12 @@ func TestDAGInsert(t *testing.T) {
 				},
 				Processors: []Processor{
 					&IngressProcessor{
-						FieldLogger: fixture.NewTestLogger(t),
+						FieldLogger:               fixture.NewTestLogger(t),
+						EnableExternalNameService: tc.enableExternalNameSvc,
 					},
 					&HTTPProxyProcessor{
-						DisablePermitInsecure: tc.disablePermitInsecure,
+						EnableExternalNameService: tc.enableExternalNameSvc,
+						DisablePermitInsecure:     tc.disablePermitInsecure,
 						FallbackCertificate: &types.NamespacedName{
 							Name:      tc.fallbackCertificateName,
 							Namespace: tc.fallbackCertificateNamespace,

programming_language: Go
filename: extension_processor.go
change_type: ModificationType.MODIFY
diff: @@ -177,6 +177,8 @@ func (p *ExtensionServiceProcessor) buildExtensionService(
 		}
 
 		// TODO(jpeach): Add ExternalName support in https://github.com/projectcontour/contour/issues/2875.
+		// TODO(youngnick): If ExternalName support is added, we must pass down the EnableExternalNameService bool
+		// and check it first.
 		if svc.Spec.ExternalName != "" {
 			validCondition.AddErrorf(contour_api_v1.ConditionTypeServiceError, "UnsupportedServiceType",
 				"Service %q is of unsupported type %q.", svcName, corev1.ServiceTypeExternalName)

programming_language: Go
filename: gatewayapi_processor.go
change_type: ModificationType.MODIFY
diff: @@ -47,6 +47,11 @@ type GatewayAPIProcessor struct {
 
 	dag    *DAG
 	source *KubernetesCache
+
+	// EnableExternalNameService allows processing of ExternalNameServices
+	// This is normally disabled for security reasons.
+	// See https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for details.
+	EnableExternalNameService bool
 }
 
 // matchConditions holds match rules.
@@ -744,9 +749,9 @@ func (p *GatewayAPIProcessor) validateForwardTo(serviceName *string, port *gatew
 	meta := types.NamespacedName{Name: *serviceName, Namespace: namespace}
 
 	// TODO: Refactor EnsureService to take an int32 so conversion to intstr is not needed.
-	service, err := p.dag.EnsureService(meta, intstr.FromInt(int(*port)), p.source)
+	service, err := p.dag.EnsureService(meta, intstr.FromInt(int(*port)), p.source, p.EnableExternalNameService)
 	if err != nil {
-		return nil, fmt.Errorf("service %q does not exist", meta.Name)
+		return nil, fmt.Errorf("service %q is invalid: %s", meta.Name, err)
 	}
 
 	return service, nil

programming_language: Go
filename: httpproxy_processor.go
change_type: ModificationType.MODIFY
diff: @@ -57,6 +57,11 @@ type HTTPProxyProcessor struct {
 	// request.
 	FallbackCertificate *types.NamespacedName
 
+	// EnableExternalNameService allows processing of ExternalNameServices
+	// This is normally disabled for security reasons.
+	// See https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for details.
+	EnableExternalNameService bool
+
 	// DNSLookupFamily defines how external names are looked up
 	// When configured as V4, the DNS resolver will only perform a lookup
 	// for addresses in the IPv4 family. If V6 is configured, the DNS resolver
@@ -563,7 +568,7 @@ func (p *HTTPProxyProcessor) computeRoutes(
 				return nil
 			}
 			m := types.NamespacedName{Name: service.Name, Namespace: proxy.Namespace}
-			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source)
+			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source, p.EnableExternalNameService)
 			if err != nil {
 				validCond.AddErrorf(contour_api_v1.ConditionTypeServiceError, "ServiceUnresolvedReference",
 					"Spec.Routes unresolved service reference: %s", err)
@@ -688,9 +693,9 @@ func (p *HTTPProxyProcessor) processHTTPProxyTCPProxy(validCond *contour_api_v1.
 		var proxy TCPProxy
 		for _, service := range httpproxy.Spec.TCPProxy.Services {
 			m := types.NamespacedName{Name: service.Name, Namespace: httpproxy.Namespace}
-			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source)
+			s, err := p.dag.EnsureService(m, intstr.FromInt(service.Port), p.source, p.EnableExternalNameService)
 			if err != nil {
-				validCond.AddErrorf(contour_api_v1.ConditionTypeTCPProxyError, "UnresolvedServiceRef",
+				validCond.AddErrorf(contour_api_v1.ConditionTypeTCPProxyError, "ServiceUnresolvedReference",
 					"Spec.TCPProxy unresolved service reference: %s", err)
 				return false
 			}

programming_language: Go
filename: ingress_processor.go
change_type: ModificationType.MODIFY
diff: @@ -37,6 +37,11 @@ type IngressProcessor struct {
 	// ClientCertificate is the optional identifier of the TLS secret containing client certificate and
 	// private key to be used when establishing TLS connection to upstream cluster.
 	ClientCertificate *types.NamespacedName
+
+	// EnableExternalNameService allows processing of ExternalNameServices
+	// This is normally disabled for security reasons.
+	// See https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc for details.
+	EnableExternalNameService bool
 }
 
 // Run translates Ingresses into DAG objects and
@@ -144,7 +149,7 @@ func (p *IngressProcessor) computeIngressRule(ing *networking_v1.Ingress, rule n
 			port = intstr.FromInt(int(be.Service.Port.Number))
 		}
 
-		s, err := p.dag.EnsureService(m, port, p.source)
+		s, err := p.dag.EnsureService(m, port, p.source, p.EnableExternalNameService)
 		if err != nil {
 			p.WithError(err).
 				WithField("name", ing.GetName()).

programming_language: Go
filename: status_test.go
change_type: ModificationType.MODIFY
diff: @@ -1758,7 +1758,7 @@ func TestDAGStatus(t *testing.T) {
 		objs: []interface{}{proxyTCPInvalidMissingService},
 		want: map[types.NamespacedName]contour_api_v1.DetailedCondition{
 			{Name: proxyTCPInvalidMissingService.Name, Namespace: proxyTCPInvalidMissingService.Namespace}: fixture.NewValidCondition().
-				WithError(contour_api_v1.ConditionTypeTCPProxyError, "UnresolvedServiceRef", `Spec.TCPProxy unresolved service reference: service "roots/not-found" not found`),
+				WithError(contour_api_v1.ConditionTypeTCPProxyError, "ServiceUnresolvedReference", `Spec.TCPProxy unresolved service reference: service "roots/not-found" not found`),
 		},
 	})
 
@@ -1787,7 +1787,7 @@ func TestDAGStatus(t *testing.T) {
 		objs: []interface{}{proxyTCPInvalidPortNotMatched, fixture.ServiceRootsKuard},
 		want: map[types.NamespacedName]contour_api_v1.DetailedCondition{
 			{Name: proxyTCPInvalidPortNotMatched.Name, Namespace: proxyTCPInvalidPortNotMatched.Namespace}: fixture.NewValidCondition().
-				WithError(contour_api_v1.ConditionTypeTCPProxyError, "UnresolvedServiceRef", `Spec.TCPProxy unresolved service reference: port "9999" on service "roots/kuard" not matched`),
+				WithError(contour_api_v1.ConditionTypeTCPProxyError, "ServiceUnresolvedReference", `Spec.TCPProxy unresolved service reference: port "9999" on service "roots/kuard" not matched`),
 		},
 	})
 
@@ -2931,7 +2931,7 @@ func TestGatewayAPIHTTPRouteDAGStatus(t *testing.T) {
 					Type:    string(status.ConditionResolvedRefs),
 					Status:  contour_api_v1.ConditionFalse,
 					Reason:  string(status.ReasonDegraded),
-					Message: "service \"invalid-one\" does not exist, service \"invalid-two\" does not exist",
+					Message: "service \"invalid-one\" is invalid: service \"default/invalid-one\" not found, service \"invalid-two\" is invalid: service \"default/invalid-two\" not found",
 				},
 				gatewayapi_v1alpha1.ConditionRouteAdmitted: {
 					Type:    string(gatewayapi_v1alpha1.ConditionRouteAdmitted),
@@ -3661,7 +3661,7 @@ func TestGatewayAPITLSRouteDAGStatus(t *testing.T) {
 						Type:    string(status.ConditionResolvedRefs),
 						Status:  contour_api_v1.ConditionFalse,
 						Reason:  string(status.ReasonDegraded),
-						Message: "service \"invalid-one\" does not exist, service \"invalid-two\" does not exist",
+						Message: "service \"invalid-one\" is invalid: service \"default/invalid-one\" not found, service \"invalid-two\" is invalid: service \"default/invalid-two\" not found",
 					},
 					gatewayapi_v1alpha1.ConditionRouteAdmitted: {
 						Type:    string(gatewayapi_v1alpha1.ConditionRouteAdmitted),

programming_language: Go
filename: externalname_test.go
change_type: ModificationType.MODIFY
diff: @@ -16,7 +16,10 @@ package v3
 import (
 	"testing"
 
+	"github.com/projectcontour/contour/internal/contour"
+	"github.com/projectcontour/contour/internal/dag"
 	"github.com/projectcontour/contour/internal/featuretests"
+	"github.com/sirupsen/logrus"
 
 	envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
 	envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
@@ -37,7 +40,7 @@ import (
 // Assert that services of type v1.ServiceTypeExternalName can be
 // referenced by an Ingress, or HTTPProxy document.
 func TestExternalNameService(t *testing.T) {
-	rh, c, done := setup(t)
+	rh, c, done := setup(t, enableExternalNameService(t))
 	defer done()
 
 	s1 := fixture.NewService("kuard").
@@ -317,3 +320,25 @@ func TestExternalNameService(t *testing.T) {
 		),
 	})
 }
+
+func enableExternalNameService(t *testing.T) func(eh *contour.EventHandler) {
+	return func(eh *contour.EventHandler) {
+
+		log := fixture.NewTestLogger(t)
+		log.SetLevel(logrus.DebugLevel)
+
+		eh.Builder.Processors = []dag.Processor{
+			&dag.IngressProcessor{
+				EnableExternalNameService: true,
+				FieldLogger:               log.WithField("context", "IngressProcessor"),
+			},
+			&dag.HTTPProxyProcessor{
+				EnableExternalNameService: true,
+			},
+			&dag.ExtensionServiceProcessor{
+				FieldLogger: log.WithField("context", "ExtensionServiceProcessor"),
+			},
+			&dag.ListenerProcessor{},
+		}
+	}
+}

programming_language: Go
filename: headerpolicy_test.go
change_type: ModificationType.MODIFY
diff: @@ -30,7 +30,10 @@ import (
 )
 
 func TestHeaderPolicy_ReplaceHeader_HTTProxy(t *testing.T) {
-	rh, c, done := setup(t)
+	// Enable ExternalName processing here because
+	// we need to check that host rewrites work in combination
+	// with ExternalName.
+	rh, c, done := setup(t, enableExternalNameService(t))
 	defer done()
 
 	rh.OnAdd(fixture.NewService("svc1").

programming_language: Go
filename: parameters.go
change_type: ModificationType.MODIFY
diff: @@ -536,6 +536,11 @@ type Parameters struct {
 	// See: https://github.com/projectcontour/contour/issues/3221
 	DisableAllowChunkedLength bool `yaml:"disableAllowChunkedLength,omitempty"`
 
+	// EnableExternalNameService allows processing of ExternalNameServices
+	// Defaults to disabled for security reasons.
+	// TODO(youngnick): put a link to the issue and CVE here.
+	EnableExternalNameService bool `yaml:"enableExternalNameService,omitempty"`
+
 	// LeaderElection contains leader election parameters.
 	LeaderElection LeaderElectionParameters `yaml:"leaderelection,omitempty"`
 

programming_language: Go
filename: configuration.md
change_type: ModificationType.MODIFY
diff: @@ -84,6 +84,7 @@ Where Contour settings can also be specified with command-line flags, the comman
 | server | ServerConfig |  | The [server configuration](#server-configuration) for `contour serve` command. |
 | gateway | GatewayConfig |  | The [gateway-api Gateway configuration](#gateway-configuration). |
 | rateLimitService | RateLimitServiceConfig | | The [rate limit service configuration](#rate-limit-service-configuration). |
+| enableExternalNameService | boolean | `false` | Enable ExternalName Service processing. Enabling this has security implications. Please see the [advisory](https://github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwc) for more details. |
 
 ### TLS Configuration
 

programming_language: Markdown
filename: 018_external_name_test.go
change_type: ModificationType.MODIFY
diff: @@ -79,7 +79,10 @@ func testExternalNameServiceInsecure(namespace string) {
 				},
 			},
 		}
-		f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
+		proxy, ok := f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
+		if !ok {
+			t.Fatalf("The HTTPProxy did not become valid, here are the Valid condition's Errors: %s", httpProxyErrors(proxy))
+		}
 
 		res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{
 			Host:      p.Spec.VirtualHost.Fqdn,
@@ -146,7 +149,10 @@ func testExternalNameServiceTLS(namespace string) {
 				},
 			},
 		}
-		f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
+		proxy, ok := f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
+		if !ok {
+			t.Fatalf("The HTTPProxy did not become valid, here are the Valid condition's Errors: %s", httpProxyErrors(proxy))
+		}
 
 		res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{
 			Host:      p.Spec.VirtualHost.Fqdn,
@@ -159,3 +165,63 @@ func testExternalNameServiceTLS(namespace string) {
 func stringPtr(s string) *string {
 	return &s
 }
+
+func testExternalNameServiceLocalhostInvalid(namespace string) {
+	Specify("external name services with localhost are rejected", func() {
+		t := f.T()
+
+		f.Fixtures.Echo.Deploy(namespace, "ingress-conformance-echo")
+
+		externalNameService := &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: namespace,
+				Name:      "external-name-service-localhost",
+			},
+			Spec: corev1.ServiceSpec{
+				Type: corev1.ServiceTypeExternalName,
+				// The unit tests test just `localhost`, so test another item from that
+				// list.
+				ExternalName: "localhost.localdomain",
+				Ports: []corev1.ServicePort{
+					{
+						Name: "http",
+						Port: 80,
+					},
+				},
+			},
+		}
+		require.NoError(t, f.Client.Create(context.TODO(), externalNameService))
+
+		p := &contourv1.HTTPProxy{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: namespace,
+				Name:      "external-name-proxy",
+			},
+			Spec: contourv1.HTTPProxySpec{
+				VirtualHost: &contourv1.VirtualHost{
+					Fqdn: "externalnameservice.projectcontour.io",
+				},
+				Routes: []contourv1.Route{
+					{
+						Services: []contourv1.Service{
+							{
+								Name: externalNameService.Name,
+								Port: 80,
+							},
+						},
+						RequestHeadersPolicy: &contourv1.HeadersPolicy{
+							Set: []contourv1.HeaderValue{
+								{
+									Name:  "Host",
+									Value: externalNameService.Spec.ExternalName,
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+		_, ok := f.CreateHTTPProxyAndWaitFor(p, httpProxyValid)
+		require.Falsef(t, ok, "ExternalName with hostname %s was accepted by Contour.", externalNameService.Spec.ExternalName)
+	})
+}

programming_language: Go
filename: httpproxy_test.go
change_type: ModificationType.MODIFY
diff: @@ -20,6 +20,7 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/davecgh/go-spew/spew"
 	certmanagerv1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
 	certmanagermetav1 "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
 	. "github.com/onsi/ginkgo"
@@ -214,10 +215,32 @@ var _ = Describe("HTTPProxy", func() {
 
 	f.NamespacedTest("017-host-header-rewrite", testHostHeaderRewrite)
 
-	f.NamespacedTest("018-external-name-service-insecure", testExternalNameServiceInsecure)
+	f.NamespacedTest("018-external-name-service-insecure", func(namespace string) {
+		Context("with ExternalName Services enabled", func() {
+			BeforeEach(func() {
+				contourConfig.EnableExternalNameService = true
+			})
+			testExternalNameServiceInsecure(namespace)
+		})
+	})
 
-	f.NamespacedTest("018-external-name-service-tls", testExternalNameServiceTLS)
+	f.NamespacedTest("018-external-name-service-tls", func(namespace string) {
+		Context("with ExternalName Services enabled", func() {
+			BeforeEach(func() {
+				contourConfig.EnableExternalNameService = true
+			})
+			testExternalNameServiceTLS(namespace)
+		})
+	})
 
+	f.NamespacedTest("018-external-name-service-localhost", func(namespace string) {
+		Context("with ExternalName Services enabled", func() {
+			BeforeEach(func() {
+				contourConfig.EnableExternalNameService = true
+			})
+			testExternalNameServiceLocalhostInvalid(namespace)
+		})
+	})
 	f.NamespacedTest("019-local-rate-limiting-vhost", testLocalRateLimitingVirtualHost)
 
 	f.NamespacedTest("019-local-rate-limiting-route", testLocalRateLimitingRoute)
@@ -278,5 +301,28 @@ descriptors:
 // httpProxyValid returns true if the proxy has a .status.currentStatus
 // of "valid".
 func httpProxyValid(proxy *contourv1.HTTPProxy) bool {
-	return proxy != nil && proxy.Status.CurrentStatus == "valid"
+
+	if proxy == nil {
+		return false
+	}
+
+	if len(proxy.Status.Conditions) == 0 {
+		return false
+	}
+
+	cond := proxy.Status.GetConditionFor("Valid")
+	return cond.Status == "True"
+
+}
+
+// httpProxyErrors provides a pretty summary of any Errors on the HTTPProxy Valid condition.
+// If there are no errors, the return value will be empty.
+func httpProxyErrors(proxy *contourv1.HTTPProxy) string {
+	cond := proxy.Status.GetConditionFor("Valid")
+	errors := cond.Errors
+	if len(errors) > 0 {
+		return spew.Sdump(errors)
+	}
+
+	return ""
 }

programming_language: Go