Skip to content

Commit

Permalink
feature: Send SNI in RemoteJWKS over TLS (#22177)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sreeram77 authored Feb 24, 2025
1 parent 952f237 commit f360c5e
Show file tree
Hide file tree
Showing 14 changed files with 553 additions and 306 deletions.
4 changes: 4 additions & 0 deletions .changelog/22177.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
```release-note:feature
config: add UseSNI flag in remote JSONWebKeySet
agent: send TLS SNI in remote JSONWebKeySet
```
6 changes: 6 additions & 0 deletions agent/structs/config_entry_jwt_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ type RemoteJWKS struct {
// Default value is false.
FetchAsynchronously bool `json:",omitempty" alias:"fetch_asynchronously"`

// UseSNI determines whether the hostname should be set in SNI
// header for TLS connection.
//
// Default value is false.
UseSNI bool `json:",omitempty" alias:"use_sni"`

// RetryPolicy defines a retry policy for fetching JWKS.
//
// There is no retry by default.
Expand Down
22 changes: 17 additions & 5 deletions agent/xds/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,16 @@ func makeJWTProviderCluster(p *structs.JWTProviderConfigEntry) (*envoy_cluster_v
}

if scheme == "https" {
sni := ""

// Set SNI to hostname when enabled.
if p.JSONWebKeySet.Remote.UseSNI {
sni = hostname
}

jwksTLSContext, err := makeUpstreamTLSTransportSocket(
&envoy_tls_v3.UpstreamTlsContext{
Sni: sni,
CommonTlsContext: &envoy_tls_v3.CommonTlsContext{
ValidationContextType: &envoy_tls_v3.CommonTlsContext_ValidationContext{
ValidationContext: makeJWTCertValidationContext(p.JSONWebKeySet.Remote.JWKSCluster),
Expand Down Expand Up @@ -1807,19 +1815,23 @@ func (s *ResourceGenerator) makeGatewayCluster(snap *proxycfg.ConfigSnapshot, op
return cluster
}

// configureClusterWithHostnames configures the Envoy cluster for service instance addressed by hostname.
// We have Envoy do the DNS resolution by setting a DNS cluster type and passing the hostname endpoints via CDS.
//
// logger represents the hclog.Logger for logging.
// cluster represents the Envoy cluster configuration.
// dnsDiscoveryType indicates the DNS service discovery type.
// hostnameEndpoints is a list of endpoints with a hostname as their address.
// isRemote determines whether the cluster is in a remote DC or partition and we should prefer a WAN address.
// onlyPassing determines whether endpoints that do not have a passing status should be considered unhealthy.
func configureClusterWithHostnames(
logger hclog.Logger,
cluster *envoy_cluster_v3.Cluster,
dnsDiscoveryType string,
// hostnameEndpoints is a list of endpoints with a hostname as their address
hostnameEndpoints structs.CheckServiceNodes,
// isRemote determines whether the cluster is in a remote DC or partition and we should prefer a WAN address
isRemote bool,
// onlyPassing determines whether endpoints that do not have a passing status should be considered unhealthy
onlyPassing bool,
) {
// When a service instance is addressed by a hostname we have Envoy do the DNS resolution
// by setting a DNS cluster type and passing the hostname endpoints via CDS.
rate := 10 * time.Second
cluster.DnsRefreshRate = durationpb.New(rate)
cluster.DnsLookupFamily = envoy_cluster_v3.Cluster_V4_ONLY
Expand Down
34 changes: 25 additions & 9 deletions agent/xds/clusters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,29 +230,44 @@ func TestMakeJWTProviderCluster(t *testing.T) {
},
expectedError: "cannot create JWKS cluster for non remote JWKS. Provider Name: okta",
},
"https-provider-with-hostname-no-port-with-sni": {
provider: makeTestProviderWithJWKS("https://example-okta.com/.well-known/jwks.json", true),
},
"https-provider-with-hostname-no-port": {
provider: makeTestProviderWithJWKS("https://example-okta.com/.well-known/jwks.json"),
provider: makeTestProviderWithJWKS("https://example-okta.com/.well-known/jwks.json", false),
},
"http-provider-with-hostname-no-port": {
provider: makeTestProviderWithJWKS("http://example-okta.com/.well-known/jwks.json"),
provider: makeTestProviderWithJWKS("http://example-okta.com/.well-known/jwks.json", true),
},
"http-provider-with-hostname-no-port-with-sni": {
provider: makeTestProviderWithJWKS("http://example-okta.com/.well-known/jwks.json", true),
},
"https-provider-with-hostname-and-port": {
provider: makeTestProviderWithJWKS("https://example-okta.com:90/.well-known/jwks.json"),
provider: makeTestProviderWithJWKS("https://example-okta.com:90/.well-known/jwks.json", false),
},
"http-provider-with-hostname-and-port": {
provider: makeTestProviderWithJWKS("http://example-okta.com:90/.well-known/jwks.json"),
provider: makeTestProviderWithJWKS("http://example-okta.com:90/.well-known/jwks.json", false),
},
"http-provider-with-hostname-and-port-with-sni": {
provider: makeTestProviderWithJWKS("http://example-okta.com:90/.well-known/jwks.json", true),
},
"https-provider-with-ip-no-port-with-sni": {
provider: makeTestProviderWithJWKS("https://127.0.0.1", true),
},
"https-provider-with-ip-no-port": {
provider: makeTestProviderWithJWKS("https://127.0.0.1"),
provider: makeTestProviderWithJWKS("https://127.0.0.1", false),
},
"http-provider-with-ip-no-port": {
provider: makeTestProviderWithJWKS("http://127.0.0.1"),
provider: makeTestProviderWithJWKS("http://127.0.0.1", false),
},
"https-provider-with-ip-and-port-with-sni": {
provider: makeTestProviderWithJWKS("https://127.0.0.1:9091", true),
},
"https-provider-with-ip-and-port": {
provider: makeTestProviderWithJWKS("https://127.0.0.1:9091"),
provider: makeTestProviderWithJWKS("https://127.0.0.1:9091", false),
},
"http-provider-with-ip-and-port": {
provider: makeTestProviderWithJWKS("http://127.0.0.1:9091"),
provider: makeTestProviderWithJWKS("http://127.0.0.1:9091", true),
},
}

Expand All @@ -272,7 +287,7 @@ func TestMakeJWTProviderCluster(t *testing.T) {
}
}

func makeTestProviderWithJWKS(uri string) *structs.JWTProviderConfigEntry {
func makeTestProviderWithJWKS(uri string, useSNI bool) *structs.JWTProviderConfigEntry {
return &structs.JWTProviderConfigEntry{
Kind: "jwt-provider",
Name: "okta",
Expand All @@ -282,6 +297,7 @@ func makeTestProviderWithJWKS(uri string) *structs.JWTProviderConfigEntry {
RequestTimeoutMs: 1000,
FetchAsynchronously: true,
URI: uri,
UseSNI: useSNI,
JWKSCluster: &structs.JWKSCluster{
DiscoveryType: structs.DiscoveryTypeStatic,
ConnectTimeout: time.Duration(5) * time.Second,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "jwks_cluster_okta",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "example-okta.com",
"portValue": 90
}
}
}
}
]
}
]
},
"name": "jwks_cluster_okta",
"type": "STATIC"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "jwks_cluster_okta",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "example-okta.com",
"portValue": 80
}
}
}
}
]
}
]
},
"name": "jwks_cluster_okta",
"type": "STATIC"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "jwks_cluster_okta",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "example-okta.com",
"portValue": 443
}
}
}
}
]
}
]
},
"name": "jwks_cluster_okta",
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"validationContext": {
"trustedCa": {
"filename": "mycert.crt"
}
}
},
"sni": "example-okta.com"
}
},
"type": "STATIC"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "jwks_cluster_okta",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9091
}
}
}
}
]
}
]
},
"name": "jwks_cluster_okta",
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"validationContext": {
"trustedCa": {
"filename": "mycert.crt"
}
}
},
"sni": "127.0.0.1"
}
},
"type": "STATIC"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "jwks_cluster_okta",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 443
}
}
}
}
]
}
]
},
"name": "jwks_cluster_okta",
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"validationContext": {
"trustedCa": {
"filename": "mycert.crt"
}
}
},
"sni": "127.0.0.1"
}
},
"type": "STATIC"
}
6 changes: 6 additions & 0 deletions api/config_entry_jwt_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,12 @@ type RemoteJWKS struct {
// Default value is false.
FetchAsynchronously bool `json:",omitempty" alias:"fetch_asynchronously"`

// UseSNI determines whether the hostname should be set in SNI
// header for TLS connection.
//
// Default value is false.
UseSNI bool `json:",omitempty" alias:"use_sni"`

// RetryPolicy defines a retry policy for fetching JWKS.
//
// There is no retry by default.
Expand Down
2 changes: 2 additions & 0 deletions proto/private/pbconfigentry/config_entry.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f360c5e

Please sign in to comment.