Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 34 additions & 26 deletions internal/xds/translator/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,31 +53,39 @@ func (*jwt) patchHCM(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListen
return nil
}

// Return early if filter already exists.
for _, httpFilter := range mgr.HttpFilters {
var jwtAuthn jwtauthnv3.JwtAuthentication
jwtAuthnFilterIndex := -1
// Add providers to existing JwtAuthentication if present.
for index, httpFilter := range mgr.HttpFilters {
Copy link
Contributor

@arkodg arkodg Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use policy name as filter name, like we do in other places ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is not changed in this PR - we use the original filter name when only one instance is needed in the HCM.

if httpFilter.Name == egv1a1.EnvoyFilterJWTAuthn.String() {
return nil
jwtAuthnFilterIndex = index
if err := httpFilter.GetTypedConfig().UnmarshalTo(&jwtAuthn); err != nil {
return err
}
}
}

jwtFilter, err := buildHCMJWTFilter(irListener)
if err := buildJWTAuthn(irListener, &jwtAuthn); err != nil {
return err
}

jwtFilter, err := buildHCMJWTFilter(&jwtAuthn)
if err != nil {
return err
}

mgr.HttpFilters = append([]*hcmv3.HttpFilter{jwtFilter}, mgr.HttpFilters...)
if exist := jwtAuthnFilterIndex != -1; exist {
mgr.HttpFilters[jwtAuthnFilterIndex] = jwtFilter
} else {
mgr.HttpFilters = append([]*hcmv3.HttpFilter{jwtFilter}, mgr.HttpFilters...)
}

return nil
}

// buildHCMJWTFilter returns a JWT authn HTTP filter from the provided IR listener.
func buildHCMJWTFilter(irListener *ir.HTTPListener) (*hcmv3.HttpFilter, error) {
jwtAuthnProto, err := buildJWTAuthn(irListener)
if err != nil {
return nil, err
}

jwtAuthnAny, err := proto.ToAnyWithValidation(jwtAuthnProto)
func buildHCMJWTFilter(jwtAuthn *jwtauthnv3.JwtAuthentication) (*hcmv3.HttpFilter, error) {
jwtAuthnAny, err := proto.ToAnyWithValidation(jwtAuthn)
if err != nil {
return nil, err
}
Expand All @@ -91,15 +99,19 @@ func buildHCMJWTFilter(irListener *ir.HTTPListener) (*hcmv3.HttpFilter, error) {
}

// buildJWTAuthn returns a JwtAuthentication based on the provided IR HTTPListener.
func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, error) {
jwtProviders := make(map[string]*jwtauthnv3.JwtProvider)
reqMap := make(map[string]*jwtauthnv3.JwtRequirement)

func buildJWTAuthn(irListener *ir.HTTPListener, jwtAuthn *jwtauthnv3.JwtAuthentication) error {
for _, route := range irListener.Routes {
if route == nil || !routeContainsJWTAuthn(route) {
continue
}

if jwtAuthn.Providers == nil {
jwtAuthn.Providers = make(map[string]*jwtauthnv3.JwtProvider, len(route.Security.JWT.Providers))
}
if jwtAuthn.RequirementMap == nil {
jwtAuthn.RequirementMap = make(map[string]*jwtauthnv3.JwtRequirement, len(route.Security.JWT.Providers))
}

var reqs []*jwtauthnv3.JwtRequirement
for i := range route.Security.JWT.Providers {
var (
Expand Down Expand Up @@ -144,7 +156,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
} else {
var cluster *urlCluster
if cluster, err = url2Cluster(jwks.URI); err != nil {
return nil, err
return err
}
jwksCluster = cluster.name
}
Expand All @@ -169,7 +181,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
if jwks.Traffic != nil && jwks.Traffic.Retry != nil {
var rp *corev3.RetryPolicy
if rp, err = buildNonRouteRetryPolicy(jwks.Traffic.Retry); err != nil {
return nil, err
return err
}
remote.RemoteJwks.RetryPolicy = rp
}
Expand All @@ -187,7 +199,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
}

providerKey := fmt.Sprintf("%s/%s", route.Name, irProvider.Name)
jwtProviders[providerKey] = jwtProvider
jwtAuthn.Providers[providerKey] = jwtProvider
reqs = append(reqs, &jwtauthnv3.JwtRequirement{
RequiresType: &jwtauthnv3.JwtRequirement_ProviderName{
ProviderName: providerKey,
Expand All @@ -204,7 +216,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
}

if len(reqs) == 1 {
reqMap[route.Name] = reqs[0]
jwtAuthn.RequirementMap[route.Name] = reqs[0]
} else {
orListReqs := &jwtauthnv3.JwtRequirement{
RequiresType: &jwtauthnv3.JwtRequirement_RequiresAny{
Expand All @@ -213,14 +225,10 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication,
},
},
}
reqMap[route.Name] = orListReqs
jwtAuthn.RequirementMap[route.Name] = orListReqs
}
}

return &jwtauthnv3.JwtAuthentication{
RequirementMap: reqMap,
Providers: jwtProviders,
}, nil
return nil
}

// buildXdsUpstreamTLSSocket returns an xDS TransportSocket that uses envoyTrustBundle
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# This file tests JWT configuration from multiple HTTP listeners sharing the same port won't overlap.
http:
- address: 0.0.0.0
externalPort: 80
hostnames:
- domain1.example.com
isHTTP2: false
metadata:
kind: Gateway
name: external-gateway
namespace: envoy-gateway-system
sectionName: domain1-example-com-http
name: envoy-gateway-system/external-gateway/domain1-example-com-http
path:
escapedSlashesAction: UnescapeAndRedirect
mergeSlashes: true
port: 10080
routes:
- destination:
metadata:
kind: HTTPRoute
name: domain1
namespace: ns1
name: httproute/ns1/domain1/rule/0
settings:
- addressType: IP
endpoints:
- host: 7.7.7.7
port: 80
metadata:
kind: Service
name: app1
namespace: ns1
sectionName: "80"
name: httproute/ns1/domain1/rule/0/backend/0
protocol: HTTP
weight: 1
hostname: domain1.example.com
isHTTP2: false
metadata:
kind: HTTPRoute
name: domain1
namespace: ns1
name: httproute/ns1/domain1/rule/0/match/0/domain1_example_com
pathMatch:
distinct: false
name: ""
prefix: /
security:
jwt:
allowMissing: true
providers:
- extractFrom:
cookies:
- AccessTokenDomain1
issuer: https://accounts.google.com
name: jwt1
remoteJWKS:
uri: https://www.googleapis.com/oauth2/v3/certs
- address: 0.0.0.0
externalPort: 80
hostnames:
- domain2.example.com
isHTTP2: false
metadata:
kind: Gateway
name: external-gateway
namespace: envoy-gateway-system
sectionName: domain2-example-com-http
name: envoy-gateway-system/external-gateway/domain2-example-com-http
path:
escapedSlashesAction: UnescapeAndRedirect
mergeSlashes: true
port: 10080
routes:
- destination:
metadata:
kind: HTTPRoute
name: domain2
namespace: ns2
name: httproute/ns2/domain2/rule/0
settings:
- addressType: IP
endpoints:
- host: 9.9.9.9
port: 80
metadata:
kind: Service
name: app2
namespace: ns2
sectionName: "80"
name: httproute/ns2/domain2/rule/0/backend/0
protocol: HTTP
weight: 1
hostname: domain2.example.com
isHTTP2: false
metadata:
kind: HTTPRoute
name: domain2
namespace: ns2
name: httproute/ns2/domain2/rule/0/match/0/domain2_example_com
pathMatch:
distinct: false
name: ""
prefix: /
security:
jwt:
allowMissing: true
providers:
- extractFrom:
cookies:
- AccessTokenDomain2
issuer: https://accounts.google.com
name: jwt2
remoteJWKS:
uri: https://www.googleapis.com/oauth2/v3/certs
readyListener:
address: 0.0.0.0
ipFamily: IPv4
path: /ready
port: 19003
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
- circuitBreakers:
thresholds:
- maxRetries: 1024
commonLbConfig: {}
connectTimeout: 10s
dnsLookupFamily: V4_PREFERRED
edsClusterConfig:
edsConfig:
ads: {}
resourceApiVersion: V3
serviceName: httproute/ns1/domain1/rule/0
ignoreHealthOnHostRemoval: true
lbPolicy: LEAST_REQUEST
loadBalancingPolicy:
policies:
- typedExtensionConfig:
name: envoy.load_balancing_policies.least_request
typedConfig:
'@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
localityLbConfig:
localityWeightedLbConfig: {}
metadata:
filterMetadata:
envoy-gateway:
resources:
- kind: HTTPRoute
name: domain1
namespace: ns1
name: httproute/ns1/domain1/rule/0
perConnectionBufferLimitBytes: 32768
type: EDS
- circuitBreakers:
thresholds:
- maxRetries: 1024
commonLbConfig: {}
connectTimeout: 10s
dnsLookupFamily: V4_PREFERRED
dnsRefreshRate: 30s
ignoreHealthOnHostRemoval: true
lbPolicy: LEAST_REQUEST
loadAssignment:
clusterName: www_googleapis_com_443
endpoints:
- lbEndpoints:
- endpoint:
address:
socketAddress:
address: www.googleapis.com
portValue: 443
loadBalancingWeight: 1
loadBalancingWeight: 1
locality:
region: www_googleapis_com_443/backend/-1
loadBalancingPolicy:
policies:
- typedExtensionConfig:
name: envoy.load_balancing_policies.least_request
typedConfig:
'@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
localityLbConfig:
localityWeightedLbConfig: {}
name: www_googleapis_com_443
perConnectionBufferLimitBytes: 32768
respectDnsTtl: true
transportSocket:
name: envoy.transport_sockets.tls
typedConfig:
'@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
commonTlsContext:
validationContext:
trustedCa:
filename: /etc/ssl/certs/ca-certificates.crt
sni: www.googleapis.com
type: STRICT_DNS
- circuitBreakers:
thresholds:
- maxRetries: 1024
commonLbConfig: {}
connectTimeout: 10s
dnsLookupFamily: V4_PREFERRED
edsClusterConfig:
edsConfig:
ads: {}
resourceApiVersion: V3
serviceName: httproute/ns2/domain2/rule/0
ignoreHealthOnHostRemoval: true
lbPolicy: LEAST_REQUEST
loadBalancingPolicy:
policies:
- typedExtensionConfig:
name: envoy.load_balancing_policies.least_request
typedConfig:
'@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
localityLbConfig:
localityWeightedLbConfig: {}
metadata:
filterMetadata:
envoy-gateway:
resources:
- kind: HTTPRoute
name: domain2
namespace: ns2
name: httproute/ns2/domain2/rule/0
perConnectionBufferLimitBytes: 32768
type: EDS
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
- clusterName: httproute/ns1/domain1/rule/0
endpoints:
- lbEndpoints:
- endpoint:
address:
socketAddress:
address: 7.7.7.7
portValue: 80
loadBalancingWeight: 1
loadBalancingWeight: 1
locality:
region: httproute/ns1/domain1/rule/0/backend/0
metadata:
filterMetadata:
envoy-gateway:
resources:
- kind: Service
name: app1
namespace: ns1
sectionName: "80"
- clusterName: httproute/ns2/domain2/rule/0
endpoints:
- lbEndpoints:
- endpoint:
address:
socketAddress:
address: 9.9.9.9
portValue: 80
loadBalancingWeight: 1
loadBalancingWeight: 1
locality:
region: httproute/ns2/domain2/rule/0/backend/0
metadata:
filterMetadata:
envoy-gateway:
resources:
- kind: Service
name: app2
namespace: ns2
sectionName: "80"
Loading