From 9876923e230647a9b0ae636466988dad152509dd Mon Sep 17 00:00:00 2001 From: John Maguire Date: Thu, 31 Aug 2023 12:23:59 -0400 Subject: [PATCH] Add the plumbing for APIGW JWT work (#18609) * Add the plumbing for APIGW JWT work * Remove unneeded import * Add deep equal function for HTTPMatch * Added plumbing for status conditions * Remove unneeded comment * Fix comments * Add calls in xds listener for apigateway to setup listener jwt auth --- agent/consul/gateways/controller_gateways.go | 65 ++- .../consul/gateways/controller_gateways_ce.go | 27 + agent/proxycfg/api_gateway.go | 21 +- agent/structs/config_entry_routes.go | 32 ++ agent/structs/config_entry_routes_test.go | 473 ++++++++++++++++++ agent/xds/gw_per_route_filters_ce.go | 2 +- agent/xds/listeners_apigateway.go | 51 +- agent/xds/routes.go | 24 +- api/config_entry_status.go | 19 + 9 files changed, 695 insertions(+), 19 deletions(-) create mode 100644 agent/consul/gateways/controller_gateways_ce.go diff --git a/agent/consul/gateways/controller_gateways.go b/agent/consul/gateways/controller_gateways.go index fe8ddbcedc..f5adc58ae2 100644 --- a/agent/consul/gateways/controller_gateways.go +++ b/agent/consul/gateways/controller_gateways.go @@ -63,6 +63,8 @@ func (r *apiGatewayReconciler) Reconcile(ctx context.Context, req controller.Req return reconcileEntry(r.fsm.State(), r.logger, ctx, req, r.reconcileTCPRoute, r.cleanupRoute) case structs.InlineCertificate: return r.enqueueCertificateReferencedGateways(r.fsm.State(), ctx, req) + case structs.JWTProvider: + return r.enqueueJWTProviderReferencedGatewaysAndHTTPRoutes(r.fsm.State(), ctx, req) default: return nil } @@ -233,6 +235,7 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle logger.Warn("error retrieving bound api gateway", "error", err) return err } + meta := newGatewayMeta(gateway, bound) certificateErrors, err := meta.checkCertificates(store) @@ -241,6 +244,12 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle return err } + jwtErrors, err := meta.checkJWTProviders(store) + if err != nil { + logger.Warn("error checking gateway JWT Providers", "error", err) + return err + } + // set each listener as having valid certs, then overwrite that status condition // if there are any certificate errors meta.eachListener(func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error { @@ -260,7 +269,12 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle if len(certificateErrors) > 0 { updater.SetCondition(invalidCertificates()) - } else { + } + if len(jwtErrors) > 0 { + updater.SetCondition(invalidJWTProviders()) + } + + if len(certificateErrors) == 0 && len(jwtErrors) == 0 { updater.SetCondition(gatewayAccepted()) } @@ -463,6 +477,13 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller. updater.SetCondition(routeNoUpstreams()) } + if httpRoute, ok := route.(*structs.HTTPRouteConfigEntry); ok { + err := validateJWTForRoute(store, updater, httpRoute) + if err != nil { + return err + } + } + // the route is valid, attempt to bind it to all gateways r.logger.Trace("binding routes to gateway") modifiedGateways, boundRefs, bindErrors := bindRoutesToGateways(route, meta...) @@ -536,6 +557,11 @@ func NewAPIGatewayController(fsm *fsm.FSM, publisher state.EventPublisher, updat &stream.SubscribeRequest{ Topic: state.EventTopicInlineCertificate, Subject: stream.SubjectWildcard, + }, + ).Subscribe( + &stream.SubscribeRequest{ + Topic: state.EventTopicJWTProvider, + Subject: stream.SubjectWildcard, }) } @@ -897,6 +923,31 @@ func invalidCertificates() structs.Condition { ) } +// invalidJWTProvider returns a condition used when a gateway listener references +// a JWTProvider that does not exist. It takes a ref used to scope the condition +// to a given APIGateway listener. +func invalidJWTProvider(ref structs.ResourceReference, err error) structs.Condition { + return structs.NewGatewayCondition( + api.GatewayConditionResolvedRefs, + api.ConditionStatusFalse, + api.GatewayListenerReasonInvalidJWTProviderRef, + err.Error(), + ref, + ) +} + +// invalidJWTProviders is used to set the overall condition of the APIGateway +// to invalid due to missing JWT providers that it references. +func invalidJWTProviders() structs.Condition { + return structs.NewGatewayCondition( + api.GatewayConditionAccepted, + api.ConditionStatusFalse, + api.GatewayReasonInvalidJWTProviders, + "gateway references invalid JWT Providers", + structs.ResourceReference{}, + ) +} + // gatewayListenerNoConflicts marks an APIGateway listener as having no conflicts within its // bound routes func gatewayListenerNoConflicts(ref structs.ResourceReference) structs.Condition { @@ -944,6 +995,18 @@ func gatewayNotFound(ref structs.ResourceReference) structs.Condition { ) } +// jwtProviderNotFound marks a Route as having failed to bind to a referenced APIGateway due to +// one or more of the referenced JWT providers not existing (or having not been reconciled yet) +func jwtProviderNotFound(ref structs.ResourceReference, err error) structs.Condition { + return structs.NewRouteCondition( + api.RouteConditionBound, + api.ConditionStatusFalse, + api.RouteReasonGatewayNotFound, + err.Error(), + ref, + ) +} + // routeUnbound marks the route as having failed to bind to the referenced APIGateway func routeUnbound(ref structs.ResourceReference, err error) structs.Condition { return structs.NewRouteCondition( diff --git a/agent/consul/gateways/controller_gateways_ce.go b/agent/consul/gateways/controller_gateways_ce.go new file mode 100644 index 0000000000..d5c83bff80 --- /dev/null +++ b/agent/consul/gateways/controller_gateways_ce.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !consulent +// +build !consulent + +package gateways + +import ( + "context" + + "github.com/hashicorp/consul/agent/consul/controller" + "github.com/hashicorp/consul/agent/consul/state" + "github.com/hashicorp/consul/agent/structs" +) + +func (r *apiGatewayReconciler) enqueueJWTProviderReferencedGatewaysAndHTTPRoutes(_ *state.Store, _ context.Context, _ controller.Request) error { + return nil +} + +func (m *gatewayMeta) checkJWTProviders(_ *state.Store) (map[structs.ResourceReference]error, error) { + return nil, nil +} + +func validateJWTForRoute(_ *state.Store, _ *structs.StatusUpdater, _ *structs.HTTPRouteConfigEntry) error { + return nil +} diff --git a/agent/proxycfg/api_gateway.go b/agent/proxycfg/api_gateway.go index 3ed3948120..43798239a3 100644 --- a/agent/proxycfg/api_gateway.go +++ b/agent/proxycfg/api_gateway.go @@ -54,6 +54,11 @@ func (h *handlerAPIGateway) initialize(ctx context.Context) (ConfigSnapshot, err return snap, err } + err = watchJWTProviders(ctx, h) + if err != nil { + return snap, err + } + snap.APIGateway.Listeners = make(map[string]structs.APIGatewayListener) snap.APIGateway.BoundListeners = make(map[string]structs.BoundAPIGatewayListener) snap.APIGateway.HTTPRoutes = watch.NewMap[structs.ResourceReference, *structs.HTTPRouteConfigEntry]() @@ -97,27 +102,33 @@ func (h *handlerAPIGateway) handleUpdate(ctx context.Context, u UpdateEvent, sna return fmt.Errorf("error filling agent cache: %v", u.Err) } - switch { - case u.CorrelationID == rootsWatchID: + switch u.CorrelationID { + case rootsWatchID: // Handle change in the CA roots if err := h.handleRootCAUpdate(u, snap); err != nil { return err } - case u.CorrelationID == apiGatewayConfigWatchID || u.CorrelationID == boundGatewayConfigWatchID: + case apiGatewayConfigWatchID, boundGatewayConfigWatchID: // Handle change in the api-gateway or bound-api-gateway config entry if err := h.handleGatewayConfigUpdate(ctx, u, snap, u.CorrelationID); err != nil { return err } - case u.CorrelationID == inlineCertificateConfigWatchID: + case inlineCertificateConfigWatchID: // Handle change in an attached inline-certificate config entry if err := h.handleInlineCertConfigUpdate(ctx, u, snap); err != nil { return err } - case u.CorrelationID == routeConfigWatchID: + case routeConfigWatchID: // Handle change in an attached http-route or tcp-route config entry if err := h.handleRouteConfigUpdate(ctx, u, snap); err != nil { return err } + case jwtProviderID: + err := setJWTProvider(u, snap) + if err != nil { + return err + } + default: if err := (*handlerUpstreams)(h).handleUpdateUpstreams(ctx, u, snap); err != nil { return err diff --git a/agent/structs/config_entry_routes.go b/agent/structs/config_entry_routes.go index d285a40624..d02d923988 100644 --- a/agent/structs/config_entry_routes.go +++ b/agent/structs/config_entry_routes.go @@ -343,6 +343,38 @@ type HTTPMatch struct { Query []HTTPQueryMatch } +func (m HTTPMatch) DeepEqual(other HTTPMatch) bool { + if m.Method != other.Method { + return false + } + + if m.Path != other.Path { + return false + } + + if len(m.Headers) != len(other.Headers) { + return false + } + + if len(m.Query) != len(other.Query) { + return false + } + + for i := 0; i < len(m.Headers); i++ { + if m.Headers[i] != other.Headers[i] { + return false + } + } + + for i := 0; i < len(m.Query); i++ { + if m.Query[i] != other.Query[i] { + return false + } + } + + return true +} + // HTTPMatchMethod specifies which type of HTTP verb should // be used for matching a given request. type HTTPMatchMethod string diff --git a/agent/structs/config_entry_routes_test.go b/agent/structs/config_entry_routes_test.go index 5ab85e5977..2aac0051a3 100644 --- a/agent/structs/config_entry_routes_test.go +++ b/agent/structs/config_entry_routes_test.go @@ -437,3 +437,476 @@ func TestHTTPRoute(t *testing.T) { } testConfigEntryNormalizeAndValidate(t, cases) } + +func TestHTTPMatch_DeepEqual(t *testing.T) { + type fields struct { + Headers []HTTPHeaderMatch + Method HTTPMatchMethod + Path HTTPPathMatch + Query []HTTPQueryMatch + } + type args struct { + other HTTPMatch + } + tests := map[string]struct { + match HTTPMatch + other HTTPMatch + want bool + }{ + "all fields equal": { + match: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + other: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + want: true, + }, + "differing number of header matches": { + match: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + other: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + want: false, + }, + "differing header matches": { + match: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h4", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + other: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + want: false, + }, + "different path matching": { + match: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/zoidberg", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + other: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + want: false, + }, + "differing methods": { + match: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodConnect, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + other: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + want: false, + }, + "differing number of query matches": { + match: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + other: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + want: false, + }, + "different query matches": { + match: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "another", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + other: HTTPMatch{ + Headers: []HTTPHeaderMatch{ + { + Match: HTTPHeaderMatchExact, + Name: "h1", + Value: "a", + }, + { + Match: HTTPHeaderMatchPrefix, + Name: "h2", + Value: "b", + }, + }, + Method: HTTPMatchMethodGet, + Path: HTTPPathMatch{ + Match: HTTPPathMatchType(HTTPHeaderMatchPrefix), + Value: "/bender", + }, + Query: []HTTPQueryMatch{ + { + Match: HTTPQueryMatchExact, + Name: "q", + Value: "nibbler", + }, + { + Match: HTTPQueryMatchPresent, + Name: "ship", + Value: "planet express", + }, + }, + }, + want: false, + }, + } + for name, tt := range tests { + name := name + tt := tt + t.Run(name, func(t *testing.T) { + t.Parallel() + if got := tt.match.DeepEqual(tt.other); got != tt.want { + t.Errorf("HTTPMatch.DeepEqual() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/agent/xds/gw_per_route_filters_ce.go b/agent/xds/gw_per_route_filters_ce.go index cbf406cd07..ba14359b79 100644 --- a/agent/xds/gw_per_route_filters_ce.go +++ b/agent/xds/gw_per_route_filters_ce.go @@ -19,6 +19,6 @@ type perRouteFilterBuilder struct { route *structs.HTTPRouteConfigEntry } -func (p perRouteFilterBuilder) buildFilter(match *envoy_route_v3.RouteMatch) (map[string]*anypb.Any, error) { +func (p perRouteFilterBuilder) buildTypedPerFilterConfig(match *envoy_route_v3.RouteMatch, routeAction *envoy_route_v3.Route_Route) (map[string]*anypb.Any, error) { return nil, nil } diff --git a/agent/xds/listeners_apigateway.go b/agent/xds/listeners_apigateway.go index 07566017ce..496d3d3697 100644 --- a/agent/xds/listeners_apigateway.go +++ b/agent/xds/listeners_apigateway.go @@ -8,7 +8,10 @@ import ( envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_http_jwt_authn_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" + envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "github.com/hashicorp/consul/agent/xds/naming" "google.golang.org/protobuf/proto" @@ -137,6 +140,45 @@ func (s *ResourceGenerator) makeAPIGatewayListeners(address string, cfgSnap *pro logger: s.Logger, } listener := makeListener(listenerOpts) + + route, _ := cfgSnap.APIGateway.HTTPRoutes.Get(readyListener.routeReference) + foundJWT := false + if listenerCfg.Override != nil && listenerCfg.Override.JWT != nil { + foundJWT = true + } + + if !foundJWT && listenerCfg.Default != nil && listenerCfg.Default.JWT != nil { + foundJWT = true + } + + if !foundJWT { + for _, rule := range route.Rules { + if rule.Filters.JWT != nil { + foundJWT = true + break + } + for _, svc := range rule.Services { + if svc.Filters.JWT != nil { + foundJWT = true + break + } + } + } + } + + var authFilters []*envoy_http_v3.HttpFilter + if foundJWT { + builder := &GatewayAuthFilterBuilder{ + listener: listenerCfg, + route: route, + providers: cfgSnap.JWTProviders, + envoyProviders: make(map[string]*envoy_http_jwt_authn_v3.JwtProvider, len(cfgSnap.JWTProviders)), + } + authFilters, err = builder.makeGatewayAuthFilters() + if err != nil { + return nil, err + } + } filterOpts := listenerFilterOpts{ useRDS: true, protocol: listenerKey.Protocol, @@ -145,7 +187,7 @@ func (s *ResourceGenerator) makeAPIGatewayListeners(address string, cfgSnap *pro cluster: "", statPrefix: "ingress_upstream_", routePath: "", - httpAuthzFilters: nil, + httpAuthzFilters: authFilters, accessLogs: &cfgSnap.Proxy.AccessLogs, logger: s.Logger, } @@ -210,7 +252,6 @@ type readyListener struct { // getReadyListeners returns a map containing the list of upstreams for each listener that is ready func getReadyListeners(cfgSnap *proxycfg.ConfigSnapshot) map[string]readyListener { - ready := map[string]readyListener{} for _, l := range cfgSnap.APIGateway.Listeners { // Only include upstreams for listeners that are ready @@ -278,7 +319,7 @@ func makeDownstreamTLSContextFromSnapshotAPIListenerConfig(cfgSnap *proxycfg.Con func makeCommonTLSContextFromSnapshotAPIGatewayListenerConfig(cfgSnap *proxycfg.ConfigSnapshot, listenerCfg structs.APIGatewayListener) (*envoy_tls_v3.CommonTlsContext, error) { var tlsContext *envoy_tls_v3.CommonTlsContext - //API Gateway TLS config is per listener + // API Gateway TLS config is per listener tlsCfg, err := resolveAPIListenerTLSConfig(listenerCfg.TLS) if err != nil { return nil, err @@ -321,8 +362,8 @@ func makeInlineOverrideFilterChains(cfgSnap *proxycfg.ConfigSnapshot, tlsCfg structs.GatewayTLSConfig, protocol string, filterOpts listenerFilterOpts, - certs []structs.InlineCertificateConfigEntry) ([]*envoy_listener_v3.FilterChain, error) { - + certs []structs.InlineCertificateConfigEntry, +) ([]*envoy_listener_v3.FilterChain, error) { var chains []*envoy_listener_v3.FilterChain constructChain := func(name string, hosts []string, tlsContext *envoy_tls_v3.CommonTlsContext) error { diff --git a/agent/xds/routes.go b/agent/xds/routes.go index 6f0d18b05d..18e642cf16 100644 --- a/agent/xds/routes.go +++ b/agent/xds/routes.go @@ -58,7 +58,7 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh continue } - virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, []string{"*"}, false) + virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, []string{"*"}, false, perRouteFilterBuilder{}) if err != nil { return nil, err } @@ -94,12 +94,12 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh addressesMap[routeName] = make(map[string]string) } // cluster name is unique per address/port so we should not be doing any override here + clusterName := clusterNameForDestination(cfgSnap, svcConfig.Name, address, svcConfig.NamespaceOrDefault(), svcConfig.PartitionOrDefault()) addressesMap[routeName][clusterName] = address } return nil }) - if err != nil { return nil, err } @@ -119,7 +119,6 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh } func (s *ResourceGenerator) makeRoutesForAddresses(routeName string, addresses map[string]string) ([]proto.Message, error) { - var resources []proto.Message route, err := makeNamedAddressesRoute(routeName, addresses) @@ -201,7 +200,8 @@ func (s *ResourceGenerator) makeRoutes( cfgSnap *proxycfg.ConfigSnapshot, svc structs.ServiceName, clusterName string, - autoHostRewrite bool) ([]proto.Message, error) { + autoHostRewrite bool, +) ([]proto.Message, error) { resolver, hasResolver := cfgSnap.TerminatingGateway.ServiceResolvers[svc] if !hasResolver { @@ -255,6 +255,7 @@ func (s *ResourceGenerator) routesForMeshGateway(cfgSnap *proxycfg.ConfigSnapsho chain, []string{"*"}, true, + perRouteFilterBuilder{}, ) if err != nil { return nil, err @@ -378,7 +379,7 @@ func (s *ResourceGenerator) routesForIngressGateway(cfgSnap *proxycfg.ConfigSnap } domains := generateUpstreamIngressDomains(listenerKey, u) - virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false) + virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false, perRouteFilterBuilder{}) if err != nil { return nil, err } @@ -435,6 +436,7 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot readyUpstreamsList := getReadyListeners(cfgSnap) for _, readyUpstreams := range readyUpstreamsList { + readyUpstreams := readyUpstreams listenerCfg := readyUpstreams.listenerCfg // Do not create any route configuration for TCP listeners if listenerCfg.Protocol != structs.ListenerProtocolHTTP { @@ -461,12 +463,13 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot // specific naming convention in discoverychain.consolidateHTTPRoutes. If we don't // convert our route to use the same naming convention, we won't find any chains below. reformatedRoutes := discoverychain.ReformatHTTPRoute(route, &listenerCfg, cfgSnap.APIGateway.GatewayConfig) - + filterBuilder := perRouteFilterBuilder{providerMap: cfgSnap.JWTProviders, listener: &listenerCfg, route: route} for _, reformatedRoute := range reformatedRoutes { reformatedRoute := reformatedRoute upstream := buildHTTPRouteUpstream(reformatedRoute, listenerCfg) uid := proxycfg.NewUpstreamID(&upstream) + chain := cfgSnap.APIGateway.DiscoveryChain[uid] if chain == nil { // Note that if we continue here we must also do this in the cluster generation @@ -476,7 +479,7 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot domains := generateUpstreamAPIsDomains(listenerKey, upstream, reformatedRoute.Hostnames) - virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false) + virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, domains, false, filterBuilder) if err != nil { return nil, err } @@ -605,6 +608,7 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain( chain *structs.CompiledDiscoveryChain, serviceDomains []string, forMeshGateway bool, + filterBuilder perRouteFilterBuilder, ) (*envoy_route_v3.VirtualHost, error) { routeName := uid.EnvoyID() var routes []*envoy_route_v3.Route @@ -624,6 +628,7 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain( routes = make([]*envoy_route_v3.Route, 0, len(startNode.Routes)) for _, discoveryRoute := range startNode.Routes { + discoveryRoute := discoveryRoute routeMatch := makeRouteMatchForDiscoveryRoute(discoveryRoute) var ( @@ -688,8 +693,13 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain( } } + filter, err := filterBuilder.buildTypedPerFilterConfig(routeMatch, routeAction) + if err != nil { + return nil, err + } route.Match = routeMatch route.Action = routeAction + route.TypedPerFilterConfig = filter routes = append(routes, route) } diff --git a/api/config_entry_status.go b/api/config_entry_status.go index 2d16ea0fc4..997066f24f 100644 --- a/api/config_entry_status.go +++ b/api/config_entry_status.go @@ -106,6 +106,10 @@ const ( // certificates and cannot bind to any routes GatewayReasonInvalidCertificates GatewayConditionReason = "InvalidCertificates" + // This reason is used with the "Accepted" condition when the gateway has multiple invalid + // JWT providers and cannot bind to any routes + GatewayReasonInvalidJWTProviders GatewayConditionReason = "InvalidJWTProviders" + // This condition indicates that the gateway was unable to resolve // conflicting specification requirements for this Listener. If a // Listener is conflicted, its network port should not be configured @@ -163,6 +167,14 @@ const ( // If the reference is not allowed, the reason RefNotPermitted must be used // instead. GatewayListenerReasonInvalidCertificateRef GatewayConditionReason = "InvalidCertificateRef" + + // This reason is used with the "ResolvedRefs" condition when a + // Listener has a JWT configuration with at least one JWTProvider + // that is invalid or does not exist. + // A JWTProvider is considered invalid when it refers to a nonexistent + // or unsupported resource or kind, or when the data within that resource + // is malformed. + GatewayListenerReasonInvalidJWTProviderRef GatewayConditionReason = "InvalidJWTProviderRef" ) var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[ConditionStatus][]GatewayConditionReason{ @@ -172,6 +184,7 @@ var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[Condition }, ConditionStatusFalse: { GatewayReasonInvalidCertificates, + GatewayReasonInvalidJWTProviders, }, ConditionStatusUnknown: {}, }, @@ -190,6 +203,7 @@ var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[Condition }, ConditionStatusFalse: { GatewayListenerReasonInvalidCertificateRef, + GatewayListenerReasonInvalidJWTProviderRef, }, ConditionStatusUnknown: {}, }, @@ -282,6 +296,10 @@ const ( // This reason is used with the "Bound" condition when the route fails // to find the gateway RouteReasonGatewayNotFound RouteConditionReason = "GatewayNotFound" + + // This reason is used with the "Accepted" condition when the route references non-existent + // JWTProviders + RouteReasonJWTProvidersNotFound RouteConditionReason = "JWTProvidersNotFound" ) var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStatus][]RouteConditionReason{ @@ -302,6 +320,7 @@ var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStat ConditionStatusFalse: { RouteReasonGatewayNotFound, RouteReasonFailedToBind, + RouteReasonJWTProvidersNotFound, }, ConditionStatusUnknown: {}, },