Expose JWKS cluster config through JWTProviderConfigEntry (#17978) (#18002)

* Expose JWKS cluster config through JWTProviderConfigEntry

* fix typos, rename trustedCa to trustedCA
This commit is contained in:
Ronald 2023-07-04 09:53:12 -04:00 committed by GitHub
parent 1b88ffef33
commit f9f2a5037f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1384 additions and 322 deletions

3
.changelog/17978.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
mesh: Expose remote jwks cluster configuration through jwt-provider config entry
```

View File

@ -54,6 +54,26 @@ func (o *ConfigSnapshot) DeepCopy() *ConfigSnapshot {
*cp_JWTProviders_v2.JSONWebKeySet.Remote.RetryPolicy.RetryPolicyBackOff = *v2.JSONWebKeySet.Remote.RetryPolicy.RetryPolicyBackOff *cp_JWTProviders_v2.JSONWebKeySet.Remote.RetryPolicy.RetryPolicyBackOff = *v2.JSONWebKeySet.Remote.RetryPolicy.RetryPolicyBackOff
} }
} }
if v2.JSONWebKeySet.Remote.JWKSCluster != nil {
cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster = new(structs.JWKSCluster)
*cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster = *v2.JSONWebKeySet.Remote.JWKSCluster
if v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates != nil {
cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates = new(structs.JWKSTLSCertificate)
*cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates = *v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates
if v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.CaCertificateProviderInstance != nil {
cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.CaCertificateProviderInstance = new(structs.JWKSTLSCertProviderInstance)
*cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.CaCertificateProviderInstance = *v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.CaCertificateProviderInstance
}
if v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA != nil {
cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA = new(structs.JWKSTLSCertTrustedCA)
*cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA = *v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA
if v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA.InlineBytes != nil {
cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA.InlineBytes = make([]byte, len(v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA.InlineBytes))
copy(cp_JWTProviders_v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA.InlineBytes, v2.JSONWebKeySet.Remote.JWKSCluster.TLSCertificates.TrustedCA.InlineBytes)
}
}
}
}
} }
} }
if v2.Audiences != nil { if v2.Audiences != nil {

View File

@ -15,6 +15,12 @@ import (
const ( const (
DefaultClockSkewSeconds = 30 DefaultClockSkewSeconds = 30
DiscoveryTypeStrictDNS ClusterDiscoveryType = "STRICT_DNS"
DiscoveryTypeStatic ClusterDiscoveryType = "STATIC"
DiscoveryTypeLogicalDNS ClusterDiscoveryType = "LOGICAL_DNS"
DiscoveryTypeEDS ClusterDiscoveryType = "EDS"
DiscoveryTypeOriginalDST ClusterDiscoveryType = "ORIGINAL_DST"
) )
type JWTProviderConfigEntry struct { type JWTProviderConfigEntry struct {
@ -97,7 +103,7 @@ func (location *JWTLocation) Validate() error {
hasCookie := location.Cookie != nil hasCookie := location.Cookie != nil
if countTrue(hasHeader, hasQueryParam, hasCookie) != 1 { if countTrue(hasHeader, hasQueryParam, hasCookie) != 1 {
return fmt.Errorf("Must set exactly one of: JWT location header, query param or cookie") return fmt.Errorf("must set exactly one of: JWT location header, query param or cookie")
} }
if hasHeader { if hasHeader {
@ -205,7 +211,7 @@ func (ks *LocalJWKS) Validate() error {
hasJWKS := ks.JWKS != "" hasJWKS := ks.JWKS != ""
if countTrue(hasFilename, hasJWKS) != 1 { if countTrue(hasFilename, hasJWKS) != 1 {
return fmt.Errorf("Must specify exactly one of String or filename for local keyset") return fmt.Errorf("must specify exactly one of String or filename for local keyset")
} }
if hasJWKS { if hasJWKS {
@ -245,6 +251,9 @@ type RemoteJWKS struct {
// //
// There is no retry by default. // There is no retry by default.
RetryPolicy *JWKSRetryPolicy `json:",omitempty" alias:"retry_policy"` RetryPolicy *JWKSRetryPolicy `json:",omitempty" alias:"retry_policy"`
// JWKSCluster defines how the specified Remote JWKS URI is to be fetched.
JWKSCluster *JWKSCluster `json:",omitempty" alias:"jwks_cluster"`
} }
func (ks *RemoteJWKS) Validate() error { func (ks *RemoteJWKS) Validate() error {
@ -257,9 +266,127 @@ func (ks *RemoteJWKS) Validate() error {
} }
if ks.RetryPolicy != nil && ks.RetryPolicy.RetryPolicyBackOff != nil { if ks.RetryPolicy != nil && ks.RetryPolicy.RetryPolicyBackOff != nil {
return ks.RetryPolicy.RetryPolicyBackOff.Validate() err := ks.RetryPolicy.RetryPolicyBackOff.Validate()
if err != nil {
return err
}
} }
if ks.JWKSCluster != nil {
return ks.JWKSCluster.Validate()
}
return nil
}
type JWKSCluster struct {
// DiscoveryType refers to the service discovery type to use for resolving the cluster.
//
// This defaults to STRICT_DNS.
// Other options include STATIC, LOGICAL_DNS, EDS or ORIGINAL_DST.
DiscoveryType ClusterDiscoveryType `json:",omitempty" alias:"discovery_type"`
// TLSCertificates refers to the data containing certificate authority certificates to use
// in verifying a presented peer certificate.
// If not specified and a peer certificate is presented it will not be verified.
//
// Must be either CaCertificateProviderInstance or TrustedCA.
TLSCertificates *JWKSTLSCertificate `json:",omitempty" alias:"tls_certificates"`
// The timeout for new network connections to hosts in the cluster.
// If not set, a default value of 5s will be used.
ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"`
}
type ClusterDiscoveryType string
func (d ClusterDiscoveryType) Validate() error {
switch d {
case DiscoveryTypeStatic, DiscoveryTypeStrictDNS, DiscoveryTypeLogicalDNS, DiscoveryTypeEDS, DiscoveryTypeOriginalDST:
return nil
default:
return fmt.Errorf("unsupported jwks cluster discovery type: %q", d)
}
}
func (c *JWKSCluster) Validate() error {
if c.DiscoveryType != "" {
err := c.DiscoveryType.Validate()
if err != nil {
return err
}
}
if c.TLSCertificates != nil {
return c.TLSCertificates.Validate()
}
return nil
}
// JWKSTLSCertificate refers to the data containing certificate authority certificates to use
// in verifying a presented peer certificate.
// If not specified and a peer certificate is presented it will not be verified.
//
// Must be either CaCertificateProviderInstance or TrustedCA.
type JWKSTLSCertificate struct {
// CaCertificateProviderInstance Certificate provider instance for fetching TLS certificates.
CaCertificateProviderInstance *JWKSTLSCertProviderInstance `json:",omitempty" alias:"ca_certificate_provider_instance"`
// TrustedCA defines TLS certificate data containing certificate authority certificates
// to use in verifying a presented peer certificate.
//
// Exactly one of Filename, EnvironmentVariable, InlineString or InlineBytes must be specified.
TrustedCA *JWKSTLSCertTrustedCA `json:",omitempty" alias:"trusted_ca"`
}
func (c *JWKSTLSCertificate) Validate() error {
hasProviderInstance := c.CaCertificateProviderInstance != nil
hasTrustedCA := c.TrustedCA != nil
if countTrue(hasProviderInstance, hasTrustedCA) != 1 {
return fmt.Errorf("must specify exactly one of: CaCertificateProviderInstance or TrustedCA for JKWS' TLSCertificates")
}
if c.TrustedCA != nil {
return c.TrustedCA.Validate()
}
return nil
}
type JWKSTLSCertProviderInstance struct {
// InstanceName refers to the certificate provider instance name
//
// The default value is "default".
InstanceName string `json:",omitempty" alias:"instance_name"`
// CertificateName is used to specify certificate instances or types. For example, "ROOTCA" to specify
// a root-certificate (validation context) or "example.com" to specify a certificate for a
// particular domain.
//
// The default value is the empty string.
CertificateName string `json:",omitempty" alias:"certificate_name"`
}
// JWKSTLSCertTrustedCA defines TLS certificate data containing certificate authority certificates
// to use in verifying a presented peer certificate.
//
// Exactly one of Filename, EnvironmentVariable, InlineString or InlineBytes must be specified.
type JWKSTLSCertTrustedCA struct {
Filename string `json:",omitempty" alias:"filename"`
EnvironmentVariable string `json:",omitempty" alias:"environment_variable"`
InlineString string `json:",omitempty" alias:"inline_string"`
InlineBytes []byte `json:",omitempty" alias:"inline_bytes"`
}
func (c *JWKSTLSCertTrustedCA) Validate() error {
hasFilename := c.Filename != ""
hasEnv := c.EnvironmentVariable != ""
hasInlineBytes := len(c.InlineBytes) > 0
hasInlineString := c.InlineString != ""
if countTrue(hasFilename, hasEnv, hasInlineString, hasInlineBytes) != 1 {
return fmt.Errorf("must specify exactly one of: Filename, EnvironmentVariable, InlineString or InlineBytes for JWKS' TrustedCA")
}
return nil return nil
} }
@ -293,7 +420,7 @@ type RetryPolicyBackOff struct {
func (r *RetryPolicyBackOff) Validate() error { func (r *RetryPolicyBackOff) Validate() error {
if (r.MaxInterval != 0) && (r.BaseInterval > r.MaxInterval) { if (r.MaxInterval != 0) && (r.BaseInterval > r.MaxInterval) {
return fmt.Errorf("Retry policy backoff's MaxInterval should be greater or equal to BaseInterval") return fmt.Errorf("retry policy backoff's MaxInterval should be greater or equal to BaseInterval")
} }
return nil return nil
@ -339,7 +466,7 @@ func (jwks *JSONWebKeySet) Validate() error {
hasRemoteKeySet := jwks.Remote != nil hasRemoteKeySet := jwks.Remote != nil
if countTrue(hasLocalKeySet, hasRemoteKeySet) != 1 { if countTrue(hasLocalKeySet, hasRemoteKeySet) != 1 {
return fmt.Errorf("Must specify exactly one of Local or Remote JSON Web key set") return fmt.Errorf("must specify exactly one of Local or Remote JSON Web key set")
} }
if hasRemoteKeySet { if hasRemoteKeySet {

View File

@ -22,6 +22,7 @@ func newTestAuthz(t *testing.T, src string) acl.Authorizer {
var tenSeconds time.Duration = 10 * time.Second var tenSeconds time.Duration = 10 * time.Second
var hundredSeconds time.Duration = 100 * time.Second var hundredSeconds time.Duration = 100 * time.Second
var connectTimeout = time.Duration(5) * time.Second
func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) { func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
defaultMeta := DefaultEnterpriseMetaInDefaultPartition() defaultMeta := DefaultEnterpriseMetaInDefaultPartition()
@ -113,7 +114,7 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
Name: "okta", Name: "okta",
JSONWebKeySet: &JSONWebKeySet{}, JSONWebKeySet: &JSONWebKeySet{},
}, },
validateErr: "Must specify exactly one of Local or Remote JSON Web key set", validateErr: "must specify exactly one of Local or Remote JSON Web key set",
}, },
"invalid jwt-provider - local jwks with non-encoded base64 jwks": { "invalid jwt-provider - local jwks with non-encoded base64 jwks": {
entry: &JWTProviderConfigEntry{ entry: &JWTProviderConfigEntry{
@ -138,7 +139,7 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
Remote: &RemoteJWKS{}, Remote: &RemoteJWKS{},
}, },
}, },
validateErr: "Must specify exactly one of Local or Remote JSON Web key set", validateErr: "must specify exactly one of Local or Remote JSON Web key set",
}, },
"invalid jwt-provider - local jwks string and filename both set": { "invalid jwt-provider - local jwks string and filename both set": {
entry: &JWTProviderConfigEntry{ entry: &JWTProviderConfigEntry{
@ -151,7 +152,7 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
}, },
}, },
}, },
validateErr: "Must specify exactly one of String or filename for local keyset", validateErr: "must specify exactly one of String or filename for local keyset",
}, },
"invalid jwt-provider - remote jwks missing uri": { "invalid jwt-provider - remote jwks missing uri": {
entry: &JWTProviderConfigEntry{ entry: &JWTProviderConfigEntry{
@ -202,7 +203,7 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
}, },
}, },
}, },
validateErr: "Must set exactly one of: JWT location header, query param or cookie", validateErr: "must set exactly one of: JWT location header, query param or cookie",
}, },
"invalid jwt-provider - Remote JWKS retry policy maxinterval < baseInterval": { "invalid jwt-provider - Remote JWKS retry policy maxinterval < baseInterval": {
entry: &JWTProviderConfigEntry{ entry: &JWTProviderConfigEntry{
@ -221,7 +222,63 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
}, },
}, },
}, },
validateErr: "Retry policy backoff's MaxInterval should be greater or equal to BaseInterval", validateErr: "retry policy backoff's MaxInterval should be greater or equal to BaseInterval",
},
"invalid jwt-provider - Remote JWKS cluster wrong discovery type": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
JWKSCluster: &JWKSCluster{
DiscoveryType: "FAKE",
},
},
},
},
validateErr: "unsupported jwks cluster discovery type: \"FAKE\"",
},
"invalid jwt-provider - Remote JWKS cluster with both trustedCa and provider instance": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
JWKSCluster: &JWKSCluster{
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{},
CaCertificateProviderInstance: &JWKSTLSCertProviderInstance{},
},
},
},
},
},
validateErr: "must specify exactly one of: CaCertificateProviderInstance or TrustedCA for JKWS' TLSCertificates",
},
"invalid jwt-provider - Remote JWKS cluster with multiple trustedCa options": {
entry: &JWTProviderConfigEntry{
Kind: JWTProvider,
Name: "okta",
JSONWebKeySet: &JSONWebKeySet{
Remote: &RemoteJWKS{
FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
JWKSCluster: &JWKSCluster{
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
InlineString: "*****",
},
},
},
},
},
},
validateErr: "must specify exactly one of: Filename, EnvironmentVariable, InlineString or InlineBytes for JWKS' TrustedCA",
}, },
"invalid jwt-provider - JWT location with 2 fields": { "invalid jwt-provider - JWT location with 2 fields": {
entry: &JWTProviderConfigEntry{ entry: &JWTProviderConfigEntry{
@ -244,7 +301,7 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
}, },
}, },
}, },
validateErr: "Must set exactly one of: JWT location header, query param or cookie", validateErr: "must set exactly one of: JWT location header, query param or cookie",
}, },
"valid jwt-provider - with all possible fields": { "valid jwt-provider - with all possible fields": {
entry: &JWTProviderConfigEntry{ entry: &JWTProviderConfigEntry{
@ -265,6 +322,15 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
MaxInterval: hundredSeconds, MaxInterval: hundredSeconds,
}, },
}, },
JWKSCluster: &JWKSCluster{
DiscoveryType: "STATIC",
ConnectTimeout: connectTimeout,
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
},
},
},
}, },
}, },
Forwarding: &JWTForwardingConfig{ Forwarding: &JWTForwardingConfig{
@ -297,6 +363,15 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
MaxInterval: hundredSeconds, MaxInterval: hundredSeconds,
}, },
}, },
JWKSCluster: &JWKSCluster{
DiscoveryType: "STATIC",
ConnectTimeout: connectTimeout,
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
},
},
},
}, },
}, },
Forwarding: &JWTForwardingConfig{ Forwarding: &JWTForwardingConfig{

View File

@ -211,13 +211,9 @@ func makeJWTProviderCluster(p *structs.JWTProviderConfigEntry) (*envoy_cluster_v
return nil, err return nil, err
} }
// TODO: expose additional fields: eg. ConnectTimeout, through
// JWTProviderConfigEntry to allow user to configure cluster
cluster := &envoy_cluster_v3.Cluster{ cluster := &envoy_cluster_v3.Cluster{
Name: makeJWKSClusterName(p.Name), Name: makeJWKSClusterName(p.Name),
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{ ClusterDiscoveryType: makeJWKSDiscoveryClusterType(p.JSONWebKeySet.Remote),
Type: envoy_cluster_v3.Cluster_STRICT_DNS,
},
LoadAssignment: &envoy_endpoint_v3.ClusterLoadAssignment{ LoadAssignment: &envoy_endpoint_v3.ClusterLoadAssignment{
ClusterName: makeJWKSClusterName(p.Name), ClusterName: makeJWKSClusterName(p.Name),
Endpoints: []*envoy_endpoint_v3.LocalityLbEndpoints{ Endpoints: []*envoy_endpoint_v3.LocalityLbEndpoints{
@ -230,14 +226,19 @@ func makeJWTProviderCluster(p *structs.JWTProviderConfigEntry) (*envoy_cluster_v
}, },
} }
if c := p.JSONWebKeySet.Remote.JWKSCluster; c != nil {
connectTimeout := int64(c.ConnectTimeout / time.Second)
if connectTimeout > 0 {
cluster.ConnectTimeout = &durationpb.Duration{Seconds: connectTimeout}
}
}
if scheme == "https" { if scheme == "https" {
// TODO: expose this configuration through JWTProviderConfigEntry to allow
// user to configure certs
jwksTLSContext, err := makeUpstreamTLSTransportSocket( jwksTLSContext, err := makeUpstreamTLSTransportSocket(
&envoy_tls_v3.UpstreamTlsContext{ &envoy_tls_v3.UpstreamTlsContext{
CommonTlsContext: &envoy_tls_v3.CommonTlsContext{ CommonTlsContext: &envoy_tls_v3.CommonTlsContext{
ValidationContextType: &envoy_tls_v3.CommonTlsContext_ValidationContext{ ValidationContextType: &envoy_tls_v3.CommonTlsContext_ValidationContext{
ValidationContext: &envoy_tls_v3.CertificateValidationContext{}, ValidationContext: makeJWTCertValidationContext(p.JSONWebKeySet.Remote.JWKSCluster),
}, },
}, },
}, },
@ -251,6 +252,76 @@ func makeJWTProviderCluster(p *structs.JWTProviderConfigEntry) (*envoy_cluster_v
return cluster, nil return cluster, nil
} }
func makeJWKSDiscoveryClusterType(r *structs.RemoteJWKS) *envoy_cluster_v3.Cluster_Type {
ct := &envoy_cluster_v3.Cluster_Type{}
if r == nil || r.JWKSCluster == nil {
return ct
}
switch r.JWKSCluster.DiscoveryType {
case structs.DiscoveryTypeStatic:
ct.Type = envoy_cluster_v3.Cluster_STATIC
case structs.DiscoveryTypeLogicalDNS:
ct.Type = envoy_cluster_v3.Cluster_LOGICAL_DNS
case structs.DiscoveryTypeEDS:
ct.Type = envoy_cluster_v3.Cluster_EDS
case structs.DiscoveryTypeOriginalDST:
ct.Type = envoy_cluster_v3.Cluster_ORIGINAL_DST
case structs.DiscoveryTypeStrictDNS:
fallthrough // default case so uses the default option
default:
ct.Type = envoy_cluster_v3.Cluster_STRICT_DNS
}
return ct
}
func makeJWTCertValidationContext(p *structs.JWKSCluster) *envoy_tls_v3.CertificateValidationContext {
vc := &envoy_tls_v3.CertificateValidationContext{}
if p == nil || p.TLSCertificates == nil {
return vc
}
if tc := p.TLSCertificates.TrustedCA; tc != nil {
vc.TrustedCa = &envoy_core_v3.DataSource{}
if tc.Filename != "" {
vc.TrustedCa.Specifier = &envoy_core_v3.DataSource_Filename{
Filename: tc.Filename,
}
}
if tc.EnvironmentVariable != "" {
vc.TrustedCa.Specifier = &envoy_core_v3.DataSource_EnvironmentVariable{
EnvironmentVariable: tc.EnvironmentVariable,
}
}
if tc.InlineString != "" {
vc.TrustedCa.Specifier = &envoy_core_v3.DataSource_InlineString{
InlineString: tc.InlineString,
}
}
if len(tc.InlineBytes) > 0 {
vc.TrustedCa.Specifier = &envoy_core_v3.DataSource_InlineBytes{
InlineBytes: tc.InlineBytes,
}
}
}
if pi := p.TLSCertificates.CaCertificateProviderInstance; pi != nil {
vc.CaCertificateProviderInstance = &envoy_tls_v3.CertificateProviderPluginInstance{}
if pi.InstanceName != "" {
vc.CaCertificateProviderInstance.InstanceName = pi.InstanceName
}
if pi.CertificateName != "" {
vc.CaCertificateProviderInstance.CertificateName = pi.CertificateName
}
}
return vc
}
// parseJWTRemoteURL splits the URI into domain, scheme and port. // parseJWTRemoteURL splits the URI into domain, scheme and port.
// It will default to port 80 for http and 443 for https for any // It will default to port 80 for http and 443 for https for any
// URI that does not specify a port. // URI that does not specify a port.

View File

@ -1037,11 +1037,104 @@ func makeTestProviderWithJWKS(uri string) *structs.JWTProviderConfigEntry {
RequestTimeoutMs: 1000, RequestTimeoutMs: 1000,
FetchAsynchronously: true, FetchAsynchronously: true,
URI: uri, URI: uri,
JWKSCluster: &structs.JWKSCluster{
DiscoveryType: structs.DiscoveryTypeStatic,
ConnectTimeout: time.Duration(5) * time.Second,
TLSCertificates: &structs.JWKSTLSCertificate{
TrustedCA: &structs.JWKSTLSCertTrustedCA{
Filename: "mycert.crt",
},
},
},
}, },
}, },
} }
} }
func TestMakeJWKSDiscoveryClusterType(t *testing.T) {
tests := map[string]struct {
remoteJWKS *structs.RemoteJWKS
expectedClusterType *envoy_cluster_v3.Cluster_Type
}{
"nil remote jwks": {
remoteJWKS: nil,
expectedClusterType: &envoy_cluster_v3.Cluster_Type{},
},
"nil jwks cluster": {
remoteJWKS: &structs.RemoteJWKS{},
expectedClusterType: &envoy_cluster_v3.Cluster_Type{},
},
"jwks cluster defaults to Strict DNS": {
remoteJWKS: &structs.RemoteJWKS{
JWKSCluster: &structs.JWKSCluster{},
},
expectedClusterType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_STRICT_DNS,
},
},
"jwks with cluster EDS": {
remoteJWKS: &structs.RemoteJWKS{
JWKSCluster: &structs.JWKSCluster{
DiscoveryType: "EDS",
},
},
expectedClusterType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_EDS,
},
},
"jwks with static dns": {
remoteJWKS: &structs.RemoteJWKS{
JWKSCluster: &structs.JWKSCluster{
DiscoveryType: "STATIC",
},
},
expectedClusterType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_STATIC,
},
},
"jwks with original dst": {
remoteJWKS: &structs.RemoteJWKS{
JWKSCluster: &structs.JWKSCluster{
DiscoveryType: "ORIGINAL_DST",
},
},
expectedClusterType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_ORIGINAL_DST,
},
},
"jwks with strict dns": {
remoteJWKS: &structs.RemoteJWKS{
JWKSCluster: &structs.JWKSCluster{
DiscoveryType: "STRICT_DNS",
},
},
expectedClusterType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_STRICT_DNS,
},
},
"jwks with logical dns": {
remoteJWKS: &structs.RemoteJWKS{
JWKSCluster: &structs.JWKSCluster{
DiscoveryType: "LOGICAL_DNS",
},
},
expectedClusterType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_LOGICAL_DNS,
},
},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
clusterType := makeJWKSDiscoveryClusterType(tt.remoteJWKS)
require.Equal(t, tt.expectedClusterType, clusterType)
})
}
}
func TestParseJWTRemoteURL(t *testing.T) { func TestParseJWTRemoteURL(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
uri string uri string

View File

@ -19,5 +19,6 @@
] ]
}, },
"name": "jwks_cluster_okta", "name": "jwks_cluster_okta",
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -19,5 +19,6 @@
] ]
}, },
"name": "jwks_cluster_okta", "name": "jwks_cluster_okta",
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -19,5 +19,6 @@
] ]
}, },
"name": "jwks_cluster_okta", "name": "jwks_cluster_okta",
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -19,5 +19,6 @@
] ]
}, },
"name": "jwks_cluster_okta", "name": "jwks_cluster_okta",
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -24,9 +24,14 @@
"typedConfig": { "typedConfig": {
"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", "@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": { "commonTlsContext": {
"validationContext": {} "validationContext": {
"trustedCa": {
"filename": "mycert.crt"
}
}
} }
} }
}, },
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -24,9 +24,14 @@
"typedConfig": { "typedConfig": {
"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", "@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": { "commonTlsContext": {
"validationContext": {} "validationContext": {
"trustedCa": {
"filename": "mycert.crt"
}
}
} }
} }
}, },
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -24,9 +24,14 @@
"typedConfig": { "typedConfig": {
"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", "@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": { "commonTlsContext": {
"validationContext": {} "validationContext": {
"trustedCa": {
"filename": "mycert.crt"
}
}
} }
} }
}, },
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -24,9 +24,14 @@
"typedConfig": { "typedConfig": {
"@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext", "@type":"type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": { "commonTlsContext": {
"validationContext": {} "validationContext": {
"trustedCa": {
"filename": "mycert.crt"
}
}
} }
} }
}, },
"type": "STRICT_DNS" "connectTimeout": "5s",
"type": "STATIC"
} }

View File

@ -7,6 +7,14 @@ import (
"time" "time"
) )
const (
DiscoveryTypeStrictDNS ClusterDiscoveryType = "STRICT_DNS"
DiscoveryTypeStatic ClusterDiscoveryType = "STATIC"
DiscoveryTypeLogicalDNS ClusterDiscoveryType = "LOGICAL_DNS"
DiscoveryTypeEDS ClusterDiscoveryType = "EDS"
DiscoveryTypeOriginalDST ClusterDiscoveryType = "ORIGINAL_DST"
)
type JWTProviderConfigEntry struct { type JWTProviderConfigEntry struct {
// Kind is the kind of configuration entry and must be "jwt-provider". // Kind is the kind of configuration entry and must be "jwt-provider".
Kind string `json:",omitempty"` Kind string `json:",omitempty"`
@ -188,6 +196,71 @@ type RemoteJWKS struct {
// //
// There is no retry by default. // There is no retry by default.
RetryPolicy *JWKSRetryPolicy `json:",omitempty" alias:"retry_policy"` RetryPolicy *JWKSRetryPolicy `json:",omitempty" alias:"retry_policy"`
// JWKSCluster defines how the specified Remote JWKS URI is to be fetched.
JWKSCluster *JWKSCluster `json:",omitempty" alias:"jwks_cluster"`
}
type JWKSCluster struct {
// DiscoveryType refers to the service discovery type to use for resolving the cluster.
//
// This defaults to STRICT_DNS.
// Other options include STATIC, LOGICAL_DNS, EDS or ORIGINAL_DST.
DiscoveryType ClusterDiscoveryType `json:",omitempty" alias:"discovery_type"`
// TLSCertificates refers to the data containing certificate authority certificates to use
// in verifying a presented peer certificate.
// If not specified and a peer certificate is presented it will not be verified.
//
// Must be either CaCertificateProviderInstance or TrustedCA.
TLSCertificates *JWKSTLSCertificate `json:",omitempty" alias:"tls_certificates"`
// The timeout for new network connections to hosts in the cluster.
// If not set, a default value of 5s will be used.
ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"`
}
type ClusterDiscoveryType string
// JWKSTLSCertificate refers to the data containing certificate authority certificates to use
// in verifying a presented peer certificate.
// If not specified and a peer certificate is presented it will not be verified.
//
// Must be either CaCertificateProviderInstance or TrustedCA.
type JWKSTLSCertificate struct {
// CaCertificateProviderInstance Certificate provider instance for fetching TLS certificates.
CaCertificateProviderInstance *JWKSTLSCertProviderInstance `json:",omitempty" alias:"ca_certificate_provider_instance"`
// TrustedCA defines TLS certificate data containing certificate authority certificates
// to use in verifying a presented peer certificate.
//
// Exactly one of Filename, EnvironmentVariable, InlineString or InlineBytes must be specified.
TrustedCA *JWKSTLSCertTrustedCA `json:",omitempty" alias:"trusted_ca"`
}
// JWKSTLSCertTrustedCA defines TLS certificate data containing certificate authority certificates
// to use in verifying a presented peer certificate.
//
// Exactly one of Filename, EnvironmentVariable, InlineString or InlineBytes must be specified.
type JWKSTLSCertTrustedCA struct {
Filename string `json:",omitempty" alias:"filename"`
EnvironmentVariable string `json:",omitempty" alias:"environment_variable"`
InlineString string `json:",omitempty" alias:"inline_string"`
InlineBytes []byte `json:",omitempty" alias:"inline_bytes"`
}
type JWKSTLSCertProviderInstance struct {
// InstanceName refers to the certificate provider instance name
//
// The default value is "default".
InstanceName string `json:",omitempty" alias:"instance_name"`
// CertificateName is used to specify certificate instances or types. For example, "ROOTCA" to specify
// a root-certificate (validation context) or "example.com" to specify a certificate for a
// particular domain.
//
// The default value is the empty string.
CertificateName string `json:",omitempty" alias:"certificate_name"`
} }
type JWKSRetryPolicy struct { type JWKSRetryPolicy struct {

View File

@ -4,6 +4,7 @@ package api
import ( import (
"testing" "testing"
"time"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -17,12 +18,23 @@ func TestAPI_ConfigEntries_JWTProvider(t *testing.T) {
entries := c.ConfigEntries() entries := c.ConfigEntries()
testutil.RunStep(t, "set and get", func(t *testing.T) { testutil.RunStep(t, "set and get", func(t *testing.T) {
connectTimeout := time.Duration(5) * time.Second
jwtProvider := &JWTProviderConfigEntry{ jwtProvider := &JWTProviderConfigEntry{
Name: "okta", Name: "okta",
Kind: JWTProvider, Kind: JWTProvider,
JSONWebKeySet: &JSONWebKeySet{ JSONWebKeySet: &JSONWebKeySet{
Local: &LocalJWKS{ Remote: &RemoteJWKS{
Filename: "test.txt", FetchAsynchronously: true,
URI: "https://example.com/.well-known/jwks.json",
JWKSCluster: &JWKSCluster{
DiscoveryType: "STATIC",
ConnectTimeout: connectTimeout,
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
},
},
},
}, },
}, },
Meta: map[string]string{ Meta: map[string]string{

View File

@ -1082,6 +1082,30 @@ func JSONWebKeySetFromStructs(t *structs.JSONWebKeySet, s *JSONWebKeySet) {
s.Remote = &x s.Remote = &x
} }
} }
func JWKSClusterToStructs(s *JWKSCluster, t *structs.JWKSCluster) {
if s == nil {
return
}
t.DiscoveryType = structs.ClusterDiscoveryType(s.DiscoveryType)
if s.TLSCertificates != nil {
var x structs.JWKSTLSCertificate
JWKSTLSCertificateToStructs(s.TLSCertificates, &x)
t.TLSCertificates = &x
}
t.ConnectTimeout = structs.DurationFromProto(s.ConnectTimeout)
}
func JWKSClusterFromStructs(t *structs.JWKSCluster, s *JWKSCluster) {
if s == nil {
return
}
s.DiscoveryType = string(t.DiscoveryType)
if t.TLSCertificates != nil {
var x JWKSTLSCertificate
JWKSTLSCertificateFromStructs(t.TLSCertificates, &x)
s.TLSCertificates = &x
}
s.ConnectTimeout = structs.DurationToProto(t.ConnectTimeout)
}
func JWKSRetryPolicyToStructs(s *JWKSRetryPolicy, t *structs.JWKSRetryPolicy) { func JWKSRetryPolicyToStructs(s *JWKSRetryPolicy, t *structs.JWKSRetryPolicy) {
if s == nil { if s == nil {
return return
@ -1104,6 +1128,68 @@ func JWKSRetryPolicyFromStructs(t *structs.JWKSRetryPolicy, s *JWKSRetryPolicy)
s.RetryPolicyBackOff = &x s.RetryPolicyBackOff = &x
} }
} }
func JWKSTLSCertProviderInstanceToStructs(s *JWKSTLSCertProviderInstance, t *structs.JWKSTLSCertProviderInstance) {
if s == nil {
return
}
t.InstanceName = s.InstanceName
t.CertificateName = s.CertificateName
}
func JWKSTLSCertProviderInstanceFromStructs(t *structs.JWKSTLSCertProviderInstance, s *JWKSTLSCertProviderInstance) {
if s == nil {
return
}
s.InstanceName = t.InstanceName
s.CertificateName = t.CertificateName
}
func JWKSTLSCertTrustedCAToStructs(s *JWKSTLSCertTrustedCA, t *structs.JWKSTLSCertTrustedCA) {
if s == nil {
return
}
t.Filename = s.Filename
t.EnvironmentVariable = s.EnvironmentVariable
t.InlineString = s.InlineString
t.InlineBytes = s.InlineBytes
}
func JWKSTLSCertTrustedCAFromStructs(t *structs.JWKSTLSCertTrustedCA, s *JWKSTLSCertTrustedCA) {
if s == nil {
return
}
s.Filename = t.Filename
s.EnvironmentVariable = t.EnvironmentVariable
s.InlineString = t.InlineString
s.InlineBytes = t.InlineBytes
}
func JWKSTLSCertificateToStructs(s *JWKSTLSCertificate, t *structs.JWKSTLSCertificate) {
if s == nil {
return
}
if s.CaCertificateProviderInstance != nil {
var x structs.JWKSTLSCertProviderInstance
JWKSTLSCertProviderInstanceToStructs(s.CaCertificateProviderInstance, &x)
t.CaCertificateProviderInstance = &x
}
if s.TrustedCA != nil {
var x structs.JWKSTLSCertTrustedCA
JWKSTLSCertTrustedCAToStructs(s.TrustedCA, &x)
t.TrustedCA = &x
}
}
func JWKSTLSCertificateFromStructs(t *structs.JWKSTLSCertificate, s *JWKSTLSCertificate) {
if s == nil {
return
}
if t.CaCertificateProviderInstance != nil {
var x JWKSTLSCertProviderInstance
JWKSTLSCertProviderInstanceFromStructs(t.CaCertificateProviderInstance, &x)
s.CaCertificateProviderInstance = &x
}
if t.TrustedCA != nil {
var x JWKSTLSCertTrustedCA
JWKSTLSCertTrustedCAFromStructs(t.TrustedCA, &x)
s.TrustedCA = &x
}
}
func JWTCacheConfigToStructs(s *JWTCacheConfig, t *structs.JWTCacheConfig) { func JWTCacheConfigToStructs(s *JWTCacheConfig, t *structs.JWTCacheConfig) {
if s == nil { if s == nil {
return return
@ -1521,6 +1607,11 @@ func RemoteJWKSToStructs(s *RemoteJWKS, t *structs.RemoteJWKS) {
JWKSRetryPolicyToStructs(s.RetryPolicy, &x) JWKSRetryPolicyToStructs(s.RetryPolicy, &x)
t.RetryPolicy = &x t.RetryPolicy = &x
} }
if s.JWKSCluster != nil {
var x structs.JWKSCluster
JWKSClusterToStructs(s.JWKSCluster, &x)
t.JWKSCluster = &x
}
} }
func RemoteJWKSFromStructs(t *structs.RemoteJWKS, s *RemoteJWKS) { func RemoteJWKSFromStructs(t *structs.RemoteJWKS, s *RemoteJWKS) {
if s == nil { if s == nil {
@ -1535,6 +1626,11 @@ func RemoteJWKSFromStructs(t *structs.RemoteJWKS, s *RemoteJWKS) {
JWKSRetryPolicyFromStructs(t.RetryPolicy, &x) JWKSRetryPolicyFromStructs(t.RetryPolicy, &x)
s.RetryPolicy = &x s.RetryPolicy = &x
} }
if t.JWKSCluster != nil {
var x JWKSCluster
JWKSClusterFromStructs(t.JWKSCluster, &x)
s.JWKSCluster = &x
}
} }
func ResourceReferenceToStructs(s *ResourceReference, t *structs.ResourceReference) { func ResourceReferenceToStructs(s *ResourceReference, t *structs.ResourceReference) {
if s == nil { if s == nil {

View File

@ -727,6 +727,46 @@ func (msg *RemoteJWKS) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg) return proto.Unmarshal(b, msg)
} }
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *JWKSCluster) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *JWKSCluster) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *JWKSTLSCertificate) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *JWKSTLSCertificate) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *JWKSTLSCertProviderInstance) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *JWKSTLSCertProviderInstance) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *JWKSTLSCertTrustedCA) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *JWKSTLSCertTrustedCA) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler // MarshalBinary implements encoding.BinaryMarshaler
func (msg *JWKSRetryPolicy) MarshalBinary() ([]byte, error) { func (msg *JWKSRetryPolicy) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg) return proto.Marshal(msg)

File diff suppressed because it is too large Load Diff

View File

@ -1021,6 +1021,51 @@ message RemoteJWKS {
google.protobuf.Duration CacheDuration = 3; google.protobuf.Duration CacheDuration = 3;
bool FetchAsynchronously = 4; bool FetchAsynchronously = 4;
JWKSRetryPolicy RetryPolicy = 5; JWKSRetryPolicy RetryPolicy = 5;
JWKSCluster JWKSCluster = 6;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.JWKSCluster
// output=config_entry.gen.go
// name=Structs
message JWKSCluster {
string DiscoveryType = 1;
JWKSTLSCertificate TLSCertificates = 2;
// mog: func-to=structs.DurationFromProto func-from=structs.DurationToProto
google.protobuf.Duration ConnectTimeout = 3;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.JWKSTLSCertificate
// output=config_entry.gen.go
// name=Structs
message JWKSTLSCertificate {
JWKSTLSCertProviderInstance CaCertificateProviderInstance = 1;
JWKSTLSCertTrustedCA TrustedCA = 2;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.JWKSTLSCertProviderInstance
// output=config_entry.gen.go
// name=Structs
message JWKSTLSCertProviderInstance {
string InstanceName = 1;
string CertificateName = 2;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.JWKSTLSCertTrustedCA
// output=config_entry.gen.go
// name=Structs
message JWKSTLSCertTrustedCA {
string Filename = 1;
string EnvironmentVariable = 2;
string InlineString = 3;
bytes InlineBytes = 4;
} }
// mog annotation: // mog annotation: