diff --git a/internal/gatewayapi/testdata/securitypolicy-with-basic-auth.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-basic-auth.out.yaml index 96c93cd3e93..9ffd4a01b5a 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-basic-auth.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-basic-auth.out.yaml @@ -155,3 +155,14 @@ xdsIR: distinct: false name: "" prefix: /foo + - backendWeights: + invalid: 0 + valid: 0 + directResponse: + statusCode: 404 + hostname: gateway.envoyproxy.io + name: catch-all + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/securitypolicy-with-extauth.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-extauth.out.yaml index 412c5643809..a587e4a4a09 100755 --- a/internal/gatewayapi/testdata/securitypolicy-with-extauth.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-extauth.out.yaml @@ -272,3 +272,25 @@ xdsIR: distinct: false name: "" prefix: /bar + - backendWeights: + invalid: 0 + valid: 0 + directResponse: + statusCode: 404 + hostname: www.foo.com + name: catch-all + pathMatch: + distinct: false + name: "" + prefix: / + - backendWeights: + invalid: 0 + valid: 0 + directResponse: + statusCode: 404 + hostname: www.bar.com + name: catch-all + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml index bd2496ecb77..dade31ee00a 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml @@ -193,9 +193,10 @@ securityPolicies: status: conditions: - lastTransitionTime: null - message: SecurityPolicy has been accepted. - reason: Accepted - status: "True" + message: 'Error fetching endpoints from issuer: Get "https://accounts.google.com/.well-known/openid-configuration": + net/http: TLS handshake timeout' + reason: Invalid + status: "False" type: Accepted xdsIR: envoy-gateway/gateway-1: @@ -258,18 +259,18 @@ xdsIR: weight: 1 hostname: www.example.com name: httproute/default/httproute-2/rule/0/match/0/www_example_com - oidc: - clientID: client1.apps.googleusercontent.com - clientSecret: Y2xpZW50MTpzZWNyZXQK - logoutPath: /bar/logout - provider: - authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth - tokenEndpoint: https://oauth2.googleapis.com/token - redirectPath: /bar/oauth2/callback - redirectURL: https://www.example.com/bar/oauth2/callback - scopes: - - openid pathMatch: distinct: false name: "" prefix: /bar + - backendWeights: + invalid: 0 + valid: 0 + directResponse: + statusCode: 404 + hostname: www.example.com + name: catch-all + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index fbbd0490066..004d1b4cef6 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -8,6 +8,7 @@ package gatewayapi import ( "golang.org/x/exp/maps" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" @@ -201,12 +202,60 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult { // Sort xdsIR based on the Gateway API spec sortXdsIRMap(xdsIR) + // Add a catch-all route for each HTTP listener if needed + addCatchAllRoute(xdsIR) + return newTranslateResult(gateways, httpRoutes, grpcRoutes, tlsRoutes, tcpRoutes, udpRoutes, clientTrafficPolicies, backendTrafficPolicies, securityPolicies, xdsIR, infraIR) } +// For filters without native per-route support, we need to add a catch-all route +// to ensure that these filters are disabled for non-matching requests. +// https://github.com/envoyproxy/gateway/issues/2507 +func addCatchAllRoute(xdsIR map[string]*ir.Xds) { + for _, i := range xdsIR { + for _, http := range i.HTTP { + var needCatchAllRoutePerHost = make(map[string]bool) + for _, r := range http.Routes { + if r.ExtAuth != nil || r.BasicAuth != nil || r.OIDC != nil { + needCatchAllRoutePerHost[r.Hostname] = true + } + } + + // skip if there is already a catch-all route + for host, _ := range needCatchAllRoutePerHost { + for _, r := range http.Routes { + if (r.Hostname == host && + r.PathMatch != nil && + r.PathMatch.Prefix != nil && + *r.PathMatch.Prefix == "/") && + len(r.HeaderMatches) == 0 && + len(r.QueryParamMatches) == 0 { + needCatchAllRoutePerHost[host] = false + } + } + } + + for host, needCatchAllRoute := range needCatchAllRoutePerHost { + if needCatchAllRoute { + http.Routes = append(http.Routes, &ir.HTTPRoute{ + Name: "catch-all", + PathMatch: &ir.StringMatch{ + Prefix: ptr.To("/"), + }, + DirectResponse: &ir.DirectResponse{ + StatusCode: 404, + }, + Hostname: host, + }) + } + } + } + } +} + // GetRelevantGateways returns GatewayContexts, containing a copy of the original // Gateway with the Listener statuses reset. func (t *Translator) GetRelevantGateways(gateways []*gwapiv1.Gateway) []*GatewayContext { diff --git a/site/content/en/latest/user/oidc.md b/site/content/en/latest/user/oidc.md index 034d39b5c04..41926603355 100644 --- a/site/content/en/latest/user/oidc.md +++ b/site/content/en/latest/user/oidc.md @@ -34,7 +34,11 @@ providers, including Auth0, Azure AD, Keycloak, Okta, OneLogin, Salesforce, UAA, Follow the steps in the [Google OIDC documentation][google-oidc] to register an OIDC application. Please make sure the redirect URL is set to the one you configured in the SecurityPolicy that you will create in the step below. If you don't -specify a redirect URL in the SecurityPolicy, the default redirect URL is `https:///oauth2/callback`. +specify a redirect URL in the SecurityPolicy, the default redirect URL is `https://${GATEWAY_HOST}/oauth2/callback`. +Please notice that the `redirectURL` and `logoutPath` must be caught by the target HTTPRoute. For example, if the +HTTPRoute is configured to match the host `www.example.com` and the path `/foo`, the `redirectURL` must +be prefixed with `https://www.example.com/foo`, and `logoutPath` must be prefixed with`/foo`, for example, +`https://www.example.com/foo/oauth2/callback` and `/foo/logout`, otherwise the OIDC authentication will fail. After registering the application, you should have the following information: * Client ID: The client ID of the OIDC application. @@ -73,6 +77,8 @@ spec: clientID: "${CLIENT_ID}.apps.googleusercontent.com" clientSecret: name: "my-app-client-secret" + redirectURI: "https://www.example.com/oauth2/callback" + logoutPath: "/logout" EOF ``` diff --git a/test/e2e/tests/basic-auth.go b/test/e2e/tests/basic-auth.go index cecac930216..e5b16e6c307 100644 --- a/test/e2e/tests/basic-auth.go +++ b/test/e2e/tests/basic-auth.go @@ -156,6 +156,35 @@ var BasicAuthTest = suite.ConformanceTest{ t.Errorf("failed to compare request and response: %v", err) } }) + + // https://github.com/envoyproxy/gateway/issues/2507 + t.Run("request without matching routes ", func(t *testing.T) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "http-with-basic-auth-1", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + SecurityPolicyMustBeAccepted(t, suite.Client, types.NamespacedName{Name: "basic-auth-1", Namespace: ns}) + // TODO: We should wait for the `programmed` condition to be true before sending traffic. + expectedResponse := http.ExpectedResponse{ + Request: http.Request{ + Path: "/not-matching-route", + }, + Response: http.Response{ + StatusCode: 404, + }, + Namespace: ns, + } + + req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http") + cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req) + if err != nil { + t.Errorf("failed to get expected response: %v", err) + } + + if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil { + t.Errorf("failed to compare request and response: %v", err) + } + }) }, }