Skip to content

Commit f360c5e

Browse files
authored
feature: Send SNI in RemoteJWKS over TLS (#22177)
* update: send hostname in SNI for RemoteJWKS envoy request over TLS * update: add sni in golden files for JWT * add: UseSNI flag in JWT Provider Config entry * add: testcases for SNI in JWT provider * update: add UseSNI for JWKS in website doc
1 parent 952f237 commit f360c5e

14 files changed

+553
-306
lines changed

.changelog/22177.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
```release-note:feature
2+
config: add UseSNI flag in remote JSONWebKeySet
3+
agent: send TLS SNI in remote JSONWebKeySet
4+
```

agent/structs/config_entry_jwt_provider.go

+6
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@ type RemoteJWKS struct {
256256
// Default value is false.
257257
FetchAsynchronously bool `json:",omitempty" alias:"fetch_asynchronously"`
258258

259+
// UseSNI determines whether the hostname should be set in SNI
260+
// header for TLS connection.
261+
//
262+
// Default value is false.
263+
UseSNI bool `json:",omitempty" alias:"use_sni"`
264+
259265
// RetryPolicy defines a retry policy for fetching JWKS.
260266
//
261267
// There is no retry by default.

agent/xds/clusters.go

+17-5
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,16 @@ func makeJWTProviderCluster(p *structs.JWTProviderConfigEntry) (*envoy_cluster_v
240240
}
241241

242242
if scheme == "https" {
243+
sni := ""
244+
245+
// Set SNI to hostname when enabled.
246+
if p.JSONWebKeySet.Remote.UseSNI {
247+
sni = hostname
248+
}
249+
243250
jwksTLSContext, err := makeUpstreamTLSTransportSocket(
244251
&envoy_tls_v3.UpstreamTlsContext{
252+
Sni: sni,
245253
CommonTlsContext: &envoy_tls_v3.CommonTlsContext{
246254
ValidationContextType: &envoy_tls_v3.CommonTlsContext_ValidationContext{
247255
ValidationContext: makeJWTCertValidationContext(p.JSONWebKeySet.Remote.JWKSCluster),
@@ -1807,19 +1815,23 @@ func (s *ResourceGenerator) makeGatewayCluster(snap *proxycfg.ConfigSnapshot, op
18071815
return cluster
18081816
}
18091817

1818+
// configureClusterWithHostnames configures the Envoy cluster for service instance addressed by hostname.
1819+
// We have Envoy do the DNS resolution by setting a DNS cluster type and passing the hostname endpoints via CDS.
1820+
//
1821+
// logger represents the hclog.Logger for logging.
1822+
// cluster represents the Envoy cluster configuration.
1823+
// dnsDiscoveryType indicates the DNS service discovery type.
1824+
// hostnameEndpoints is a list of endpoints with a hostname as their address.
1825+
// isRemote determines whether the cluster is in a remote DC or partition and we should prefer a WAN address.
1826+
// onlyPassing determines whether endpoints that do not have a passing status should be considered unhealthy.
18101827
func configureClusterWithHostnames(
18111828
logger hclog.Logger,
18121829
cluster *envoy_cluster_v3.Cluster,
18131830
dnsDiscoveryType string,
1814-
// hostnameEndpoints is a list of endpoints with a hostname as their address
18151831
hostnameEndpoints structs.CheckServiceNodes,
1816-
// isRemote determines whether the cluster is in a remote DC or partition and we should prefer a WAN address
18171832
isRemote bool,
1818-
// onlyPassing determines whether endpoints that do not have a passing status should be considered unhealthy
18191833
onlyPassing bool,
18201834
) {
1821-
// When a service instance is addressed by a hostname we have Envoy do the DNS resolution
1822-
// by setting a DNS cluster type and passing the hostname endpoints via CDS.
18231835
rate := 10 * time.Second
18241836
cluster.DnsRefreshRate = durationpb.New(rate)
18251837
cluster.DnsLookupFamily = envoy_cluster_v3.Cluster_V4_ONLY

agent/xds/clusters_test.go

+25-9
Original file line numberDiff line numberDiff line change
@@ -230,29 +230,44 @@ func TestMakeJWTProviderCluster(t *testing.T) {
230230
},
231231
expectedError: "cannot create JWKS cluster for non remote JWKS. Provider Name: okta",
232232
},
233+
"https-provider-with-hostname-no-port-with-sni": {
234+
provider: makeTestProviderWithJWKS("https://example-okta.com/.well-known/jwks.json", true),
235+
},
233236
"https-provider-with-hostname-no-port": {
234-
provider: makeTestProviderWithJWKS("https://example-okta.com/.well-known/jwks.json"),
237+
provider: makeTestProviderWithJWKS("https://example-okta.com/.well-known/jwks.json", false),
235238
},
236239
"http-provider-with-hostname-no-port": {
237-
provider: makeTestProviderWithJWKS("http://example-okta.com/.well-known/jwks.json"),
240+
provider: makeTestProviderWithJWKS("http://example-okta.com/.well-known/jwks.json", true),
241+
},
242+
"http-provider-with-hostname-no-port-with-sni": {
243+
provider: makeTestProviderWithJWKS("http://example-okta.com/.well-known/jwks.json", true),
238244
},
239245
"https-provider-with-hostname-and-port": {
240-
provider: makeTestProviderWithJWKS("https://example-okta.com:90/.well-known/jwks.json"),
246+
provider: makeTestProviderWithJWKS("https://example-okta.com:90/.well-known/jwks.json", false),
241247
},
242248
"http-provider-with-hostname-and-port": {
243-
provider: makeTestProviderWithJWKS("http://example-okta.com:90/.well-known/jwks.json"),
249+
provider: makeTestProviderWithJWKS("http://example-okta.com:90/.well-known/jwks.json", false),
250+
},
251+
"http-provider-with-hostname-and-port-with-sni": {
252+
provider: makeTestProviderWithJWKS("http://example-okta.com:90/.well-known/jwks.json", true),
253+
},
254+
"https-provider-with-ip-no-port-with-sni": {
255+
provider: makeTestProviderWithJWKS("https://127.0.0.1", true),
244256
},
245257
"https-provider-with-ip-no-port": {
246-
provider: makeTestProviderWithJWKS("https://127.0.0.1"),
258+
provider: makeTestProviderWithJWKS("https://127.0.0.1", false),
247259
},
248260
"http-provider-with-ip-no-port": {
249-
provider: makeTestProviderWithJWKS("http://127.0.0.1"),
261+
provider: makeTestProviderWithJWKS("http://127.0.0.1", false),
262+
},
263+
"https-provider-with-ip-and-port-with-sni": {
264+
provider: makeTestProviderWithJWKS("https://127.0.0.1:9091", true),
250265
},
251266
"https-provider-with-ip-and-port": {
252-
provider: makeTestProviderWithJWKS("https://127.0.0.1:9091"),
267+
provider: makeTestProviderWithJWKS("https://127.0.0.1:9091", false),
253268
},
254269
"http-provider-with-ip-and-port": {
255-
provider: makeTestProviderWithJWKS("http://127.0.0.1:9091"),
270+
provider: makeTestProviderWithJWKS("http://127.0.0.1:9091", true),
256271
},
257272
}
258273

@@ -272,7 +287,7 @@ func TestMakeJWTProviderCluster(t *testing.T) {
272287
}
273288
}
274289

275-
func makeTestProviderWithJWKS(uri string) *structs.JWTProviderConfigEntry {
290+
func makeTestProviderWithJWKS(uri string, useSNI bool) *structs.JWTProviderConfigEntry {
276291
return &structs.JWTProviderConfigEntry{
277292
Kind: "jwt-provider",
278293
Name: "okta",
@@ -282,6 +297,7 @@ func makeTestProviderWithJWKS(uri string) *structs.JWTProviderConfigEntry {
282297
RequestTimeoutMs: 1000,
283298
FetchAsynchronously: true,
284299
URI: uri,
300+
UseSNI: useSNI,
285301
JWKSCluster: &structs.JWKSCluster{
286302
DiscoveryType: structs.DiscoveryTypeStatic,
287303
ConnectTimeout: time.Duration(5) * time.Second,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"connectTimeout": "5s",
3+
"loadAssignment": {
4+
"clusterName": "jwks_cluster_okta",
5+
"endpoints": [
6+
{
7+
"lbEndpoints": [
8+
{
9+
"endpoint": {
10+
"address": {
11+
"socketAddress": {
12+
"address": "example-okta.com",
13+
"portValue": 90
14+
}
15+
}
16+
}
17+
}
18+
]
19+
}
20+
]
21+
},
22+
"name": "jwks_cluster_okta",
23+
"type": "STATIC"
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"connectTimeout": "5s",
3+
"loadAssignment": {
4+
"clusterName": "jwks_cluster_okta",
5+
"endpoints": [
6+
{
7+
"lbEndpoints": [
8+
{
9+
"endpoint": {
10+
"address": {
11+
"socketAddress": {
12+
"address": "example-okta.com",
13+
"portValue": 80
14+
}
15+
}
16+
}
17+
}
18+
]
19+
}
20+
]
21+
},
22+
"name": "jwks_cluster_okta",
23+
"type": "STATIC"
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"connectTimeout": "5s",
3+
"loadAssignment": {
4+
"clusterName": "jwks_cluster_okta",
5+
"endpoints": [
6+
{
7+
"lbEndpoints": [
8+
{
9+
"endpoint": {
10+
"address": {
11+
"socketAddress": {
12+
"address": "example-okta.com",
13+
"portValue": 443
14+
}
15+
}
16+
}
17+
}
18+
]
19+
}
20+
]
21+
},
22+
"name": "jwks_cluster_okta",
23+
"transportSocket": {
24+
"name": "tls",
25+
"typedConfig": {
26+
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
27+
"commonTlsContext": {
28+
"validationContext": {
29+
"trustedCa": {
30+
"filename": "mycert.crt"
31+
}
32+
}
33+
},
34+
"sni": "example-okta.com"
35+
}
36+
},
37+
"type": "STATIC"
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"connectTimeout": "5s",
3+
"loadAssignment": {
4+
"clusterName": "jwks_cluster_okta",
5+
"endpoints": [
6+
{
7+
"lbEndpoints": [
8+
{
9+
"endpoint": {
10+
"address": {
11+
"socketAddress": {
12+
"address": "127.0.0.1",
13+
"portValue": 9091
14+
}
15+
}
16+
}
17+
}
18+
]
19+
}
20+
]
21+
},
22+
"name": "jwks_cluster_okta",
23+
"transportSocket": {
24+
"name": "tls",
25+
"typedConfig": {
26+
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
27+
"commonTlsContext": {
28+
"validationContext": {
29+
"trustedCa": {
30+
"filename": "mycert.crt"
31+
}
32+
}
33+
},
34+
"sni": "127.0.0.1"
35+
}
36+
},
37+
"type": "STATIC"
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"connectTimeout": "5s",
3+
"loadAssignment": {
4+
"clusterName": "jwks_cluster_okta",
5+
"endpoints": [
6+
{
7+
"lbEndpoints": [
8+
{
9+
"endpoint": {
10+
"address": {
11+
"socketAddress": {
12+
"address": "127.0.0.1",
13+
"portValue": 443
14+
}
15+
}
16+
}
17+
}
18+
]
19+
}
20+
]
21+
},
22+
"name": "jwks_cluster_okta",
23+
"transportSocket": {
24+
"name": "tls",
25+
"typedConfig": {
26+
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
27+
"commonTlsContext": {
28+
"validationContext": {
29+
"trustedCa": {
30+
"filename": "mycert.crt"
31+
}
32+
}
33+
},
34+
"sni": "127.0.0.1"
35+
}
36+
},
37+
"type": "STATIC"
38+
}

api/config_entry_jwt_provider.go

+6
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ type RemoteJWKS struct {
192192
// Default value is false.
193193
FetchAsynchronously bool `json:",omitempty" alias:"fetch_asynchronously"`
194194

195+
// UseSNI determines whether the hostname should be set in SNI
196+
// header for TLS connection.
197+
//
198+
// Default value is false.
199+
UseSNI bool `json:",omitempty" alias:"use_sni"`
200+
195201
// RetryPolicy defines a retry policy for fetching JWKS.
196202
//
197203
// There is no retry by default.

proto/private/pbconfigentry/config_entry.gen.go

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)