Authorization bypass in Contour
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/projectcontour/contourGo | < 1.14.2 | 1.14.2 |
github.com/projectcontour/contourGo | >= 1.15.0, < 1.15.2 | 1.15.2 |
github.com/projectcontour/contourGo | >= 1.16.0, < 1.16.1 | 1.16.1 |
github.com/projectcontour/contourGo | >= 1.17.0, < 1.17.1 | 1.17.1 |
Affected products
1- Range: < 1.17.1
Patches
2b53a5c4fd927cherrypicks for v1.17.1 (#3909)
19 files changed · +393 −35
cmd/contour/serve.go+15 −9 modified@@ -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"), }) }
examples/contour/01-contour-config.yaml+7 −0 modified@@ -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
examples/render/contour-gateway.yaml+7 −0 modified@@ -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
examples/render/contour.yaml+7 −0 modified@@ -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
go.mod+1 −0 modified@@ -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
internal/dag/accessors.go+38 −1 modified@@ -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]
internal/dag/accessors_test.go+73 −5 modified@@ -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) })
internal/dag/builder_test.go+65 −3 modified@@ -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,
internal/dag/extension_processor.go+2 −0 modified@@ -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)
internal/dag/gatewayapi_processor.go+7 −2 modified@@ -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
internal/dag/httpproxy_processor.go+8 −3 modified@@ -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 }
internal/dag/ingress_processor.go+6 −1 modified@@ -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()).
internal/dag/status_test.go+4 −4 modified@@ -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),
internal/featuretests/v3/externalname_test.go+26 −1 modified@@ -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{}, + } + } +}
internal/featuretests/v3/headerpolicy_test.go+4 −1 modified@@ -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").
pkg/config/parameters.go+5 −0 modified@@ -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"`
site/content/docs/main/configuration.md+1 −0 modified@@ -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
test/e2e/httpproxy/018_external_name_test.go+68 −2 modified@@ -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) + }) +}
test/e2e/httpproxy/httpproxy_test.go+49 −3 modified@@ -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 "" }
5f3e6d0ab1d4Merge pull request from GHSA-5ph6-qq5x-7jwc
19 files changed · +396 −35
cmd/contour/serve.go+15 −9 modified@@ -715,29 +715,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"), }) }
examples/contour/01-contour-config.yaml+7 −0 modified@@ -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 reenable 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
examples/render/contour-gateway.yaml+7 −0 modified@@ -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 reenable 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
examples/render/contour.yaml+7 −0 modified@@ -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 reenable 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
go.mod+1 −0 modified@@ -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
internal/dag/accessors.go+38 −1 modified@@ -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]
internal/dag/accessors_test.go+73 −5 modified@@ -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) })
internal/dag/builder_test.go+65 −3 modified@@ -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,
internal/dag/extension_processor.go+2 −0 modified@@ -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)
internal/dag/gatewayapi_processor.go+7 −2 modified@@ -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. @@ -743,9 +748,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
internal/dag/httpproxy_processor.go+8 −3 modified@@ -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 }
internal/dag/ingress_processor.go+6 −1 modified@@ -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()).
internal/dag/status_test.go+4 −4 modified@@ -1770,7 +1770,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`), }, }) @@ -1799,7 +1799,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`), }, }) @@ -2967,7 +2967,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), @@ -3758,7 +3758,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),
internal/featuretests/v3/externalname_test.go+26 −1 modified@@ -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{}, + } + } +}
internal/featuretests/v3/headerpolicy_test.go+4 −1 modified@@ -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").
pkg/config/parameters.go+5 −0 modified@@ -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"`
site/content/docs/main/configuration.md+1 −0 modified@@ -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
test/e2e/httpproxy/external_name_test.go+68 −2 modified@@ -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) + }) +}
test/e2e/httpproxy/httpproxy_test.go+52 −3 modified@@ -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("httpproxy-host-header-rewrite", testHostHeaderRewrite) - f.NamespacedTest("httpproxy-external-name-service-insecure", testExternalNameServiceInsecure) + f.NamespacedTest("httpproxy-external-name-service-insecure", func(namespace string) { + Context("with ExternalName Services enabled", func() { + BeforeEach(func() { + contourConfig.EnableExternalNameService = true + }) + testExternalNameServiceInsecure(namespace) + }) + }) - f.NamespacedTest("httpproxy-external-name-service-tls", testExternalNameServiceTLS) + f.NamespacedTest("httpproxy-external-name-service-tls", func(namespace string) { + Context("with ExternalName Services enabled", func() { + BeforeEach(func() { + contourConfig.EnableExternalNameService = true + }) + testExternalNameServiceTLS(namespace) + }) + }) + f.NamespacedTest("httpproxy-external-name-service-localhost", func(namespace string) { + Context("with ExternalName Services enabled", func() { + BeforeEach(func() { + contourConfig.EnableExternalNameService = true + }) + testExternalNameServiceLocalhostInvalid(namespace) + }) + }) f.NamespacedTest("httpproxy-local-rate-limiting-vhost", testLocalRateLimitingVirtualHost) f.NamespacedTest("httpproxy-local-rate-limiting-route", testLocalRateLimitingRoute) @@ -278,5 +301,31 @@ 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") + if cond.Status == "True" { + return true + } + return false + +} + +// httpProxyErrors provides a pretty summary of any Errors on the HTTPProxy Valid conditon. +// 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 "" }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-5ph6-qq5x-7jwcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-32783ghsaADVISORY
- github.com/projectcontour/contour/commit/5f3e6d0ab1d48e64bae46400c85c490b200393a3ghsaWEB
- github.com/projectcontour/contour/commit/b53a5c4fd927f4ea2c6cf02f1359d8e28bef852eghsax_refsource_MISCWEB
- github.com/projectcontour/contour/releases/tag/v1.14.2ghsaWEB
- github.com/projectcontour/contour/releases/tag/v1.15.2ghsaWEB
- github.com/projectcontour/contour/releases/tag/v1.16.1ghsaWEB
- github.com/projectcontour/contour/releases/tag/v1.17.1ghsax_refsource_MISCWEB
- github.com/projectcontour/contour/security/advisories/GHSA-5ph6-qq5x-7jwcghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.