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
This commit is contained in:
John Maguire 2023-08-31 12:23:59 -04:00 committed by GitHub
parent d45c3c2755
commit 9876923e23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 695 additions and 19 deletions

View File

@ -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) return reconcileEntry(r.fsm.State(), r.logger, ctx, req, r.reconcileTCPRoute, r.cleanupRoute)
case structs.InlineCertificate: case structs.InlineCertificate:
return r.enqueueCertificateReferencedGateways(r.fsm.State(), ctx, req) return r.enqueueCertificateReferencedGateways(r.fsm.State(), ctx, req)
case structs.JWTProvider:
return r.enqueueJWTProviderReferencedGatewaysAndHTTPRoutes(r.fsm.State(), ctx, req)
default: default:
return nil return nil
} }
@ -233,6 +235,7 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
logger.Warn("error retrieving bound api gateway", "error", err) logger.Warn("error retrieving bound api gateway", "error", err)
return err return err
} }
meta := newGatewayMeta(gateway, bound) meta := newGatewayMeta(gateway, bound)
certificateErrors, err := meta.checkCertificates(store) certificateErrors, err := meta.checkCertificates(store)
@ -241,6 +244,12 @@ func (r *apiGatewayReconciler) reconcileGateway(_ context.Context, req controlle
return err 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 // set each listener as having valid certs, then overwrite that status condition
// if there are any certificate errors // if there are any certificate errors
meta.eachListener(func(listener *structs.APIGatewayListener, bound *structs.BoundAPIGatewayListener) error { 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 { if len(certificateErrors) > 0 {
updater.SetCondition(invalidCertificates()) updater.SetCondition(invalidCertificates())
} else { }
if len(jwtErrors) > 0 {
updater.SetCondition(invalidJWTProviders())
}
if len(certificateErrors) == 0 && len(jwtErrors) == 0 {
updater.SetCondition(gatewayAccepted()) updater.SetCondition(gatewayAccepted())
} }
@ -463,6 +477,13 @@ func (r *apiGatewayReconciler) reconcileRoute(_ context.Context, req controller.
updater.SetCondition(routeNoUpstreams()) 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 // the route is valid, attempt to bind it to all gateways
r.logger.Trace("binding routes to gateway") r.logger.Trace("binding routes to gateway")
modifiedGateways, boundRefs, bindErrors := bindRoutesToGateways(route, meta...) modifiedGateways, boundRefs, bindErrors := bindRoutesToGateways(route, meta...)
@ -536,6 +557,11 @@ func NewAPIGatewayController(fsm *fsm.FSM, publisher state.EventPublisher, updat
&stream.SubscribeRequest{ &stream.SubscribeRequest{
Topic: state.EventTopicInlineCertificate, Topic: state.EventTopicInlineCertificate,
Subject: stream.SubjectWildcard, 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 // gatewayListenerNoConflicts marks an APIGateway listener as having no conflicts within its
// bound routes // bound routes
func gatewayListenerNoConflicts(ref structs.ResourceReference) structs.Condition { 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 // routeUnbound marks the route as having failed to bind to the referenced APIGateway
func routeUnbound(ref structs.ResourceReference, err error) structs.Condition { func routeUnbound(ref structs.ResourceReference, err error) structs.Condition {
return structs.NewRouteCondition( return structs.NewRouteCondition(

View File

@ -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
}

View File

@ -54,6 +54,11 @@ func (h *handlerAPIGateway) initialize(ctx context.Context) (ConfigSnapshot, err
return snap, err return snap, err
} }
err = watchJWTProviders(ctx, h)
if err != nil {
return snap, err
}
snap.APIGateway.Listeners = make(map[string]structs.APIGatewayListener) snap.APIGateway.Listeners = make(map[string]structs.APIGatewayListener)
snap.APIGateway.BoundListeners = make(map[string]structs.BoundAPIGatewayListener) snap.APIGateway.BoundListeners = make(map[string]structs.BoundAPIGatewayListener)
snap.APIGateway.HTTPRoutes = watch.NewMap[structs.ResourceReference, *structs.HTTPRouteConfigEntry]() 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) return fmt.Errorf("error filling agent cache: %v", u.Err)
} }
switch { switch u.CorrelationID {
case u.CorrelationID == rootsWatchID: case rootsWatchID:
// Handle change in the CA roots // Handle change in the CA roots
if err := h.handleRootCAUpdate(u, snap); err != nil { if err := h.handleRootCAUpdate(u, snap); err != nil {
return err return err
} }
case u.CorrelationID == apiGatewayConfigWatchID || u.CorrelationID == boundGatewayConfigWatchID: case apiGatewayConfigWatchID, boundGatewayConfigWatchID:
// Handle change in the api-gateway or bound-api-gateway config entry // Handle change in the api-gateway or bound-api-gateway config entry
if err := h.handleGatewayConfigUpdate(ctx, u, snap, u.CorrelationID); err != nil { if err := h.handleGatewayConfigUpdate(ctx, u, snap, u.CorrelationID); err != nil {
return err return err
} }
case u.CorrelationID == inlineCertificateConfigWatchID: case inlineCertificateConfigWatchID:
// Handle change in an attached inline-certificate config entry // Handle change in an attached inline-certificate config entry
if err := h.handleInlineCertConfigUpdate(ctx, u, snap); err != nil { if err := h.handleInlineCertConfigUpdate(ctx, u, snap); err != nil {
return err return err
} }
case u.CorrelationID == routeConfigWatchID: case routeConfigWatchID:
// Handle change in an attached http-route or tcp-route config entry // Handle change in an attached http-route or tcp-route config entry
if err := h.handleRouteConfigUpdate(ctx, u, snap); err != nil { if err := h.handleRouteConfigUpdate(ctx, u, snap); err != nil {
return err return err
} }
case jwtProviderID:
err := setJWTProvider(u, snap)
if err != nil {
return err
}
default: default:
if err := (*handlerUpstreams)(h).handleUpdateUpstreams(ctx, u, snap); err != nil { if err := (*handlerUpstreams)(h).handleUpdateUpstreams(ctx, u, snap); err != nil {
return err return err

View File

@ -343,6 +343,38 @@ type HTTPMatch struct {
Query []HTTPQueryMatch 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 // HTTPMatchMethod specifies which type of HTTP verb should
// be used for matching a given request. // be used for matching a given request.
type HTTPMatchMethod string type HTTPMatchMethod string

View File

@ -437,3 +437,476 @@ func TestHTTPRoute(t *testing.T) {
} }
testConfigEntryNormalizeAndValidate(t, cases) 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)
}
})
}
}

View File

@ -19,6 +19,6 @@ type perRouteFilterBuilder struct {
route *structs.HTTPRouteConfigEntry 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 return nil, nil
} }

View File

@ -8,7 +8,10 @@ import (
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 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_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" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
"github.com/hashicorp/consul/agent/xds/naming" "github.com/hashicorp/consul/agent/xds/naming"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -137,6 +140,45 @@ func (s *ResourceGenerator) makeAPIGatewayListeners(address string, cfgSnap *pro
logger: s.Logger, logger: s.Logger,
} }
listener := makeListener(listenerOpts) 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{ filterOpts := listenerFilterOpts{
useRDS: true, useRDS: true,
protocol: listenerKey.Protocol, protocol: listenerKey.Protocol,
@ -145,7 +187,7 @@ func (s *ResourceGenerator) makeAPIGatewayListeners(address string, cfgSnap *pro
cluster: "", cluster: "",
statPrefix: "ingress_upstream_", statPrefix: "ingress_upstream_",
routePath: "", routePath: "",
httpAuthzFilters: nil, httpAuthzFilters: authFilters,
accessLogs: &cfgSnap.Proxy.AccessLogs, accessLogs: &cfgSnap.Proxy.AccessLogs,
logger: s.Logger, 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 // getReadyListeners returns a map containing the list of upstreams for each listener that is ready
func getReadyListeners(cfgSnap *proxycfg.ConfigSnapshot) map[string]readyListener { func getReadyListeners(cfgSnap *proxycfg.ConfigSnapshot) map[string]readyListener {
ready := map[string]readyListener{} ready := map[string]readyListener{}
for _, l := range cfgSnap.APIGateway.Listeners { for _, l := range cfgSnap.APIGateway.Listeners {
// Only include upstreams for listeners that are ready // 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) { func makeCommonTLSContextFromSnapshotAPIGatewayListenerConfig(cfgSnap *proxycfg.ConfigSnapshot, listenerCfg structs.APIGatewayListener) (*envoy_tls_v3.CommonTlsContext, error) {
var tlsContext *envoy_tls_v3.CommonTlsContext 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) tlsCfg, err := resolveAPIListenerTLSConfig(listenerCfg.TLS)
if err != nil { if err != nil {
return nil, err return nil, err
@ -321,8 +362,8 @@ func makeInlineOverrideFilterChains(cfgSnap *proxycfg.ConfigSnapshot,
tlsCfg structs.GatewayTLSConfig, tlsCfg structs.GatewayTLSConfig,
protocol string, protocol string,
filterOpts listenerFilterOpts, filterOpts listenerFilterOpts,
certs []structs.InlineCertificateConfigEntry) ([]*envoy_listener_v3.FilterChain, error) { certs []structs.InlineCertificateConfigEntry,
) ([]*envoy_listener_v3.FilterChain, error) {
var chains []*envoy_listener_v3.FilterChain var chains []*envoy_listener_v3.FilterChain
constructChain := func(name string, hosts []string, tlsContext *envoy_tls_v3.CommonTlsContext) error { constructChain := func(name string, hosts []string, tlsContext *envoy_tls_v3.CommonTlsContext) error {

View File

@ -58,7 +58,7 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh
continue continue
} }
virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, []string{"*"}, false) virtualHost, err := s.makeUpstreamRouteForDiscoveryChain(cfgSnap, uid, chain, []string{"*"}, false, perRouteFilterBuilder{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -94,12 +94,12 @@ func (s *ResourceGenerator) routesForConnectProxy(cfgSnap *proxycfg.ConfigSnapsh
addressesMap[routeName] = make(map[string]string) addressesMap[routeName] = make(map[string]string)
} }
// cluster name is unique per address/port so we should not be doing any override here // 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()) clusterName := clusterNameForDestination(cfgSnap, svcConfig.Name, address, svcConfig.NamespaceOrDefault(), svcConfig.PartitionOrDefault())
addressesMap[routeName][clusterName] = address addressesMap[routeName][clusterName] = address
} }
return nil return nil
}) })
if err != nil { if err != nil {
return nil, err 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) { func (s *ResourceGenerator) makeRoutesForAddresses(routeName string, addresses map[string]string) ([]proto.Message, error) {
var resources []proto.Message var resources []proto.Message
route, err := makeNamedAddressesRoute(routeName, addresses) route, err := makeNamedAddressesRoute(routeName, addresses)
@ -201,7 +200,8 @@ func (s *ResourceGenerator) makeRoutes(
cfgSnap *proxycfg.ConfigSnapshot, cfgSnap *proxycfg.ConfigSnapshot,
svc structs.ServiceName, svc structs.ServiceName,
clusterName string, clusterName string,
autoHostRewrite bool) ([]proto.Message, error) { autoHostRewrite bool,
) ([]proto.Message, error) {
resolver, hasResolver := cfgSnap.TerminatingGateway.ServiceResolvers[svc] resolver, hasResolver := cfgSnap.TerminatingGateway.ServiceResolvers[svc]
if !hasResolver { if !hasResolver {
@ -255,6 +255,7 @@ func (s *ResourceGenerator) routesForMeshGateway(cfgSnap *proxycfg.ConfigSnapsho
chain, chain,
[]string{"*"}, []string{"*"},
true, true,
perRouteFilterBuilder{},
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -378,7 +379,7 @@ func (s *ResourceGenerator) routesForIngressGateway(cfgSnap *proxycfg.ConfigSnap
} }
domains := generateUpstreamIngressDomains(listenerKey, u) 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 { if err != nil {
return nil, err return nil, err
} }
@ -435,6 +436,7 @@ func (s *ResourceGenerator) routesForAPIGateway(cfgSnap *proxycfg.ConfigSnapshot
readyUpstreamsList := getReadyListeners(cfgSnap) readyUpstreamsList := getReadyListeners(cfgSnap)
for _, readyUpstreams := range readyUpstreamsList { for _, readyUpstreams := range readyUpstreamsList {
readyUpstreams := readyUpstreams
listenerCfg := readyUpstreams.listenerCfg listenerCfg := readyUpstreams.listenerCfg
// Do not create any route configuration for TCP listeners // Do not create any route configuration for TCP listeners
if listenerCfg.Protocol != structs.ListenerProtocolHTTP { 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 // 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. // convert our route to use the same naming convention, we won't find any chains below.
reformatedRoutes := discoverychain.ReformatHTTPRoute(route, &listenerCfg, cfgSnap.APIGateway.GatewayConfig) reformatedRoutes := discoverychain.ReformatHTTPRoute(route, &listenerCfg, cfgSnap.APIGateway.GatewayConfig)
filterBuilder := perRouteFilterBuilder{providerMap: cfgSnap.JWTProviders, listener: &listenerCfg, route: route}
for _, reformatedRoute := range reformatedRoutes { for _, reformatedRoute := range reformatedRoutes {
reformatedRoute := reformatedRoute reformatedRoute := reformatedRoute
upstream := buildHTTPRouteUpstream(reformatedRoute, listenerCfg) upstream := buildHTTPRouteUpstream(reformatedRoute, listenerCfg)
uid := proxycfg.NewUpstreamID(&upstream) uid := proxycfg.NewUpstreamID(&upstream)
chain := cfgSnap.APIGateway.DiscoveryChain[uid] chain := cfgSnap.APIGateway.DiscoveryChain[uid]
if chain == nil { if chain == nil {
// Note that if we continue here we must also do this in the cluster generation // 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) 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 { if err != nil {
return nil, err return nil, err
} }
@ -605,6 +608,7 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain(
chain *structs.CompiledDiscoveryChain, chain *structs.CompiledDiscoveryChain,
serviceDomains []string, serviceDomains []string,
forMeshGateway bool, forMeshGateway bool,
filterBuilder perRouteFilterBuilder,
) (*envoy_route_v3.VirtualHost, error) { ) (*envoy_route_v3.VirtualHost, error) {
routeName := uid.EnvoyID() routeName := uid.EnvoyID()
var routes []*envoy_route_v3.Route var routes []*envoy_route_v3.Route
@ -624,6 +628,7 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain(
routes = make([]*envoy_route_v3.Route, 0, len(startNode.Routes)) routes = make([]*envoy_route_v3.Route, 0, len(startNode.Routes))
for _, discoveryRoute := range startNode.Routes { for _, discoveryRoute := range startNode.Routes {
discoveryRoute := discoveryRoute
routeMatch := makeRouteMatchForDiscoveryRoute(discoveryRoute) routeMatch := makeRouteMatchForDiscoveryRoute(discoveryRoute)
var ( 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.Match = routeMatch
route.Action = routeAction route.Action = routeAction
route.TypedPerFilterConfig = filter
routes = append(routes, route) routes = append(routes, route)
} }

View File

@ -106,6 +106,10 @@ const (
// certificates and cannot bind to any routes // certificates and cannot bind to any routes
GatewayReasonInvalidCertificates GatewayConditionReason = "InvalidCertificates" 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 // This condition indicates that the gateway was unable to resolve
// conflicting specification requirements for this Listener. If a // conflicting specification requirements for this Listener. If a
// Listener is conflicted, its network port should not be configured // 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 // If the reference is not allowed, the reason RefNotPermitted must be used
// instead. // instead.
GatewayListenerReasonInvalidCertificateRef GatewayConditionReason = "InvalidCertificateRef" 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{ var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[ConditionStatus][]GatewayConditionReason{
@ -172,6 +184,7 @@ var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[Condition
}, },
ConditionStatusFalse: { ConditionStatusFalse: {
GatewayReasonInvalidCertificates, GatewayReasonInvalidCertificates,
GatewayReasonInvalidJWTProviders,
}, },
ConditionStatusUnknown: {}, ConditionStatusUnknown: {},
}, },
@ -190,6 +203,7 @@ var validGatewayConditionReasonsMapping = map[GatewayConditionType]map[Condition
}, },
ConditionStatusFalse: { ConditionStatusFalse: {
GatewayListenerReasonInvalidCertificateRef, GatewayListenerReasonInvalidCertificateRef,
GatewayListenerReasonInvalidJWTProviderRef,
}, },
ConditionStatusUnknown: {}, ConditionStatusUnknown: {},
}, },
@ -282,6 +296,10 @@ const (
// This reason is used with the "Bound" condition when the route fails // This reason is used with the "Bound" condition when the route fails
// to find the gateway // to find the gateway
RouteReasonGatewayNotFound RouteConditionReason = "GatewayNotFound" 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{ var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStatus][]RouteConditionReason{
@ -302,6 +320,7 @@ var validRouteConditionReasonsMapping = map[RouteConditionType]map[ConditionStat
ConditionStatusFalse: { ConditionStatusFalse: {
RouteReasonGatewayNotFound, RouteReasonGatewayNotFound,
RouteReasonFailedToBind, RouteReasonFailedToBind,
RouteReasonJWTProvidersNotFound,
}, },
ConditionStatusUnknown: {}, ConditionStatusUnknown: {},
}, },