From 63bba27762c22261c0893e906c6f49a66887b93a Mon Sep 17 00:00:00 2001 From: Ronald Ekambi Date: Thu, 31 Aug 2023 12:00:55 -0400 Subject: [PATCH] [NET-5346] Expose JWKCluster fields in jwt-provider config entry --- .changelog/2881.txt | 3 + charts/consul/templates/crd-jwtproviders.yaml | 60 +++++ .../api/v1alpha1/jwtprovider_types.go | 203 +++++++++++++- .../api/v1alpha1/jwtprovider_types_test.go | 253 +++++++++++++++++- .../api/v1alpha1/zz_generated.deepcopy.go | 90 +++++++ .../consul.hashicorp.com_jwtproviders.yaml | 60 +++++ .../configentry_controller_test.go | 62 +++++ 7 files changed, 724 insertions(+), 7 deletions(-) create mode 100644 .changelog/2881.txt diff --git a/.changelog/2881.txt b/.changelog/2881.txt new file mode 100644 index 0000000000..5d76975cd3 --- /dev/null +++ b/.changelog/2881.txt @@ -0,0 +1,3 @@ +```release-note:improvement +helm: Add `JWKSCluster` field to `JWTProvider` CRD. +``` \ No newline at end of file diff --git a/charts/consul/templates/crd-jwtproviders.yaml b/charts/consul/templates/crd-jwtproviders.yaml index 18b6e9bdcb..9f97922eb5 100644 --- a/charts/consul/templates/crd-jwtproviders.yaml +++ b/charts/consul/templates/crd-jwtproviders.yaml @@ -123,6 +123,66 @@ spec: the proxy listener will wait for the JWKS to be fetched before being activated. \n Default value is false." type: boolean + jwksCluster: + description: "JWKSCluster defines how the specified Remote JWKS + URI is to be fetched." + properties: + connectTimeout: + description: "The timeout for new network connections to hosts + in the cluster. \n If not set, a default value of 5s will be + used." + format: int64 + type: integer + discoveryType: + description: "DiscoveryType refers to the service discovery type + to use for resolving the cluster. \n Defaults to STRICT_DNS." + type: string + tlsCertificates: + description: "TLSCertificates refers to the data containing + certificate authority certificates to use in verifying a presented + peer certificate." + properties: + caCertificateProviderInstance: + description: "CaCertificateProviderInstance Certificate provider + instance for fetching TLS certificates." + properties: + instanceName: + description: "InstanceName refers to the certificate provider + instance name. \n The default value is 'default'." + type: string + certificateName: + description: "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. \n + The default value is the empty string." + type: string + type: object + trustedCA: + description: "TrustedCA defines TLS certificate data containing + certificate authority certificates to use in verifying a presented + peer certificate. \n Exactly one of Filename, EnvironmentVariable, + InlineString or InlineBytes must be specified." + properties: + filename: + description: "The name of the file on the local system to use a + data source for trusted CA certificates." + type: string + environmentVariable: + description: "The environment variable on the local system to use + a data source for trusted CA certificates." + type: string + inlineString: + description: "A string to inline in the configuration for use as + a data source for trusted CA certificates." + type: string + inlineBytes: + description: "A sequence of bytes to inline in the configuration + for use as a data source for trusted CA certificates." + type: string + type: object + type: object + type: object requestTimeoutMs: description: RequestTimeoutMs is the number of milliseconds to time out when making a request for the JWKS. diff --git a/control-plane/api/v1alpha1/jwtprovider_types.go b/control-plane/api/v1alpha1/jwtprovider_types.go index fee0ef9a78..8e80ece1dc 100644 --- a/control-plane/api/v1alpha1/jwtprovider_types.go +++ b/control-plane/api/v1alpha1/jwtprovider_types.go @@ -22,7 +22,12 @@ import ( ) const ( - JWTProviderKubeKind string = "jwtprovider" + JWTProviderKubeKind string = "jwtprovider" + DiscoveryTypeStrictDNS ClusterDiscoveryType = "STRICT_DNS" + DiscoveryTypeStatic ClusterDiscoveryType = "STATIC" + DiscoveryTypeLogicalDNS ClusterDiscoveryType = "LOGICAL_DNS" + DiscoveryTypeEDS ClusterDiscoveryType = "EDS" + DiscoveryTypeOriginalDST ClusterDiscoveryType = "ORIGINAL_DST" ) func init() { @@ -404,6 +409,9 @@ type RemoteJWKS struct { // // There is no retry by default. RetryPolicy *JWKSRetryPolicy `json:"retryPolicy,omitempty"` + + // JWKSCluster defines how the specified Remote JWKS URI is to be fetched. + JWKSCluster *JWKSCluster `json:"jwksCluster,omitempty"` } func (r *RemoteJWKS) toConsul() *capi.RemoteJWKS { @@ -416,6 +424,7 @@ func (r *RemoteJWKS) toConsul() *capi.RemoteJWKS { CacheDuration: r.CacheDuration, FetchAsynchronously: r.FetchAsynchronously, RetryPolicy: r.RetryPolicy.toConsul(), + JWKSCluster: r.JWKSCluster.toConsul(), } } @@ -432,9 +441,188 @@ func (r *RemoteJWKS) validate(path *field.Path) field.ErrorList { } errs = append(errs, r.RetryPolicy.validate(path.Child("retryPolicy"))...) + errs = append(errs, r.JWKSCluster.validate(path.Child("jwksCluster"))...) + return errs +} + +// JWKSCluster defines how the specified Remote JWKS URI is to be fetched. +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:"discoveryType,omitempty"` + + // 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:"tlsCertificates,omitempty"` + + // 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:"connectTimeout,omitempty"` +} + +func (c *JWKSCluster) toConsul() *capi.JWKSCluster { + if c == nil { + return nil + } + return &capi.JWKSCluster{ + DiscoveryType: c.DiscoveryType.toConsul(), + TLSCertificates: c.TLSCertificates.toConsul(), + ConnectTimeout: c.ConnectTimeout, + } +} + +func (c *JWKSCluster) validate(path *field.Path) field.ErrorList { + var errs field.ErrorList + if c == nil { + return errs + } + + errs = append(errs, c.DiscoveryType.validate(path.Child("discoveryType"))...) + errs = append(errs, c.TLSCertificates.validate(path.Child("tlsCertificates"))...) + + return errs +} + +type ClusterDiscoveryType string + +func (d ClusterDiscoveryType) validate(path *field.Path) field.ErrorList { + var errs field.ErrorList + + switch d { + case DiscoveryTypeStatic, DiscoveryTypeStrictDNS, DiscoveryTypeLogicalDNS, DiscoveryTypeEDS, DiscoveryTypeOriginalDST: + return errs + default: + errs = append(errs, field.Invalid(path, string(d), "unsupported jwks cluster discovery type.")) + } return errs } +func (d ClusterDiscoveryType) toConsul() capi.ClusterDiscoveryType { + return capi.ClusterDiscoveryType(string(d)) +} + +// 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:"caCertificateProviderInstance,omitempty"` + + // 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:"trustedCA,omitempty"` +} + +func (c *JWKSTLSCertificate) toConsul() *capi.JWKSTLSCertificate { + if c == nil { + return nil + } + + return &capi.JWKSTLSCertificate{ + TrustedCA: c.TrustedCA.toConsul(), + CaCertificateProviderInstance: c.CaCertificateProviderInstance.toConsul(), + } +} + +func (c *JWKSTLSCertificate) validate(path *field.Path) field.ErrorList { + var errs field.ErrorList + if c == nil { + return errs + } + + hasProviderInstance := c.CaCertificateProviderInstance != nil + hasTrustedCA := c.TrustedCA != nil + + if countTrue(hasTrustedCA, hasProviderInstance) != 1 { + asJSON, _ := json.Marshal(c) + errs = append(errs, field.Invalid(path, string(asJSON), "exactly one of 'trustedCa' or 'caCertificateProviderInstance' is required")) + } + + errs = append(errs, c.TrustedCA.validate(path.Child("trustedCa"))...) + + return errs +} + +// JWKSTLSCertProviderInstance Certificate provider instance for fetching TLS certificates. +type JWKSTLSCertProviderInstance struct { + // InstanceName refers to the certificate provider instance name. + // + // The default value is "default". + InstanceName string `json:"instanceName,omitempty"` + + // 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:"certificateName,omitempty"` +} + +func (c *JWKSTLSCertProviderInstance) toConsul() *capi.JWKSTLSCertProviderInstance { + if c == nil { + return nil + } + + return &capi.JWKSTLSCertProviderInstance{ + InstanceName: c.InstanceName, + CertificateName: c.CertificateName, + } +} + +// 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:"filename,omitempty"` + EnvironmentVariable string `json:"environmentVariable,omitempty"` + InlineString string `json:"inlineString,omitempty"` + InlineBytes []byte `json:"inlineBytes,omitempty"` +} + +func (c *JWKSTLSCertTrustedCA) toConsul() *capi.JWKSTLSCertTrustedCA { + if c == nil { + return nil + } + + return &capi.JWKSTLSCertTrustedCA{ + Filename: c.Filename, + EnvironmentVariable: c.EnvironmentVariable, + InlineBytes: c.InlineBytes, + InlineString: c.InlineString, + } +} + +func (c *JWKSTLSCertTrustedCA) validate(path *field.Path) field.ErrorList { + var errs field.ErrorList + if c == nil { + return errs + } + + hasFilename := c.Filename != "" + hasEnv := c.EnvironmentVariable != "" + hasInlineBytes := len(c.InlineBytes) > 0 + hasInlineString := c.InlineString != "" + + if countTrue(hasFilename, hasEnv, hasInlineString, hasInlineBytes) != 1 { + asJSON, _ := json.Marshal(c) + errs = append(errs, field.Invalid(path, string(asJSON), "exactly one of 'filename', 'environmentVariable', 'inlineString' or 'inlineBytes' is required")) + } + return errs +} + +// JWKSRetryPolicy defines a retry policy for fetching JWKS. +// +// There is no retry by default. type JWKSRetryPolicy struct { // NumRetries is the number of times to retry fetching the JWKS. // The retry strategy uses jittered exponential backoff with @@ -443,9 +631,9 @@ type JWKSRetryPolicy struct { // Default value is 0. NumRetries int `json:"numRetries,omitempty"` - // Backoff policy + // Retry's backoff policy. // - // Defaults to Envoy's backoff policy + // Defaults to Envoy's backoff policy. RetryPolicyBackOff *RetryPolicyBackOff `json:"retryPolicyBackOff,omitempty"` } @@ -468,16 +656,19 @@ func (j *JWKSRetryPolicy) validate(path *field.Path) field.ErrorList { return append(errs, j.RetryPolicyBackOff.validate(path.Child("retryPolicyBackOff"))...) } +// RetryPolicyBackOff defines retry's policy backoff. +// +// Defaults to Envoy's backoff policy. type RetryPolicyBackOff struct { - // BaseInterval to be used for the next back off computation + // BaseInterval to be used for the next back off computation. // - // The default value from envoy is 1s + // The default value from envoy is 1s. BaseInterval time.Duration `json:"baseInterval,omitempty"` // MaxInternal to be used to specify the maximum interval between retries. // Optional but should be greater or equal to BaseInterval. // - // Defaults to 10 times BaseInterval + // Defaults to 10 times BaseInterval. MaxInterval time.Duration `json:"maxInterval,omitempty"` } diff --git a/control-plane/api/v1alpha1/jwtprovider_types_test.go b/control-plane/api/v1alpha1/jwtprovider_types_test.go index 15a3e7a5d6..8f6219e8d6 100644 --- a/control-plane/api/v1alpha1/jwtprovider_types_test.go +++ b/control-plane/api/v1alpha1/jwtprovider_types_test.go @@ -64,6 +64,22 @@ func TestJWTProvider_MatchesConsul(t *testing.T) { MaxInterval: 456, }, }, + JWKSCluster: &JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &JWKSTLSCertificate{ + CaCertificateProviderInstance: &JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + CertificateName: "ROOTCA", + }, + TrustedCA: &JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + EnvironmentVariable: "env-variable", + InlineString: "inline-string", + InlineBytes: []byte("inline-bytes"), + }, + }, + ConnectTimeout: 890, + }, }, }, Issuer: "test-issuer", @@ -118,6 +134,22 @@ func TestJWTProvider_MatchesConsul(t *testing.T) { MaxInterval: 456, }, }, + JWKSCluster: &capi.JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &capi.JWKSTLSCertificate{ + CaCertificateProviderInstance: &capi.JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + CertificateName: "ROOTCA", + }, + TrustedCA: &capi.JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + EnvironmentVariable: "env-variable", + InlineString: "inline-string", + InlineBytes: []byte("inline-bytes"), + }, + }, + ConnectTimeout: 890, + }, }, }, Issuer: "test-issuer", @@ -215,6 +247,22 @@ func TestJWTProvider_ToConsul(t *testing.T) { MaxInterval: 456, }, }, + JWKSCluster: &JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &JWKSTLSCertificate{ + CaCertificateProviderInstance: &JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + CertificateName: "ROOTCA", + }, + TrustedCA: &JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + EnvironmentVariable: "env-variable", + InlineString: "inline-string", + InlineBytes: []byte("inline-bytes"), + }, + }, + ConnectTimeout: 890, + }, }, }, Issuer: "test-issuer", @@ -268,6 +316,22 @@ func TestJWTProvider_ToConsul(t *testing.T) { MaxInterval: 456, }, }, + JWKSCluster: &capi.JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &capi.JWKSTLSCertificate{ + CaCertificateProviderInstance: &capi.JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + CertificateName: "ROOTCA", + }, + TrustedCA: &capi.JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + EnvironmentVariable: "env-variable", + InlineString: "inline-string", + InlineBytes: []byte("inline-bytes"), + }, + }, + ConnectTimeout: 890, + }, }, }, Issuer: "test-issuer", @@ -366,7 +430,7 @@ func TestJWTProvider_Validate(t *testing.T) { expectedErrMsgs: nil, }, - "valid - remote jwks with all fields": { + "valid - remote jwks with all fields with trustedCa": { input: &JWTProvider{ ObjectMeta: metav1.ObjectMeta{ Name: "test-jwt-provider", @@ -385,6 +449,80 @@ func TestJWTProvider_Validate(t *testing.T) { MaxInterval: 20 * time.Second, }, }, + JWKSCluster: &JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &JWKSTLSCertificate{ + TrustedCA: &JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + }, + }, + ConnectTimeout: 890, + }, + }, + }, + Issuer: "test-issuer", + Audiences: []string{"aud1", "aud2"}, + Locations: []*JWTLocation{ + { + Header: &JWTLocationHeader{ + Name: "Authorization", + ValuePrefix: "Bearer", + Forward: true, + }, + }, + { + QueryParam: &JWTLocationQueryParam{ + Name: "access-token", + }, + }, + { + Cookie: &JWTLocationCookie{ + Name: "session-id", + }, + }, + }, + Forwarding: &JWTForwardingConfig{ + HeaderName: "jwt-forward-header", + PadForwardPayloadHeader: true, + }, + ClockSkewSeconds: 20, + CacheConfig: &JWTCacheConfig{ + Size: 30, + }, + }, + }, + expectedErrMsgs: nil, + }, + + "valid - remote jwks with all fields with CaCertificateProviderInstance": { + input: &JWTProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-jwt-provider", + }, + Spec: JWTProviderSpec{ + JSONWebKeySet: &JSONWebKeySet{ + Remote: &RemoteJWKS{ + URI: "https://jwks.example.com", + RequestTimeoutMs: 5000, + CacheDuration: 10 * time.Second, + FetchAsynchronously: true, + RetryPolicy: &JWKSRetryPolicy{ + NumRetries: 3, + RetryPolicyBackOff: &RetryPolicyBackOff{ + BaseInterval: 5 * time.Second, + MaxInterval: 20 * time.Second, + }, + }, + JWKSCluster: &JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &JWKSTLSCertificate{ + CaCertificateProviderInstance: &JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + CertificateName: "ROOTCA", + }, + }, + ConnectTimeout: 890, + }, }, }, Issuer: "test-issuer", @@ -522,6 +660,119 @@ func TestJWTProvider_Validate(t *testing.T) { }, }, + "invalid - remote jwks invalid jwkcluster - all TLSCertificates fields set": { + input: &JWTProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-jwks-invalid-uri", + }, + Spec: JWTProviderSpec{ + JSONWebKeySet: &JSONWebKeySet{ + Remote: &RemoteJWKS{ + URI: "https://jwks.example.com", + JWKSCluster: &JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &JWKSTLSCertificate{ + CaCertificateProviderInstance: &JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + }, + TrustedCA: &JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + }, + }, + ConnectTimeout: 890, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `jwtprovider.consul.hashicorp.com "test-jwks-invalid-uri" is invalid: spec.jsonWebKeySet.remote.jwksCluster.tlsCertificates: Invalid value:`, + `exactly one of 'trustedCa' or 'caCertificateProviderInstance' is required`, + }, + }, + + "invalid - remote jwks invalid jwkcluster - invalid discovery type": { + input: &JWTProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-jwks-invalid-uri", + }, + Spec: JWTProviderSpec{ + JSONWebKeySet: &JSONWebKeySet{ + Remote: &RemoteJWKS{ + URI: "https://jwks.example.com", + JWKSCluster: &JWKSCluster{ + DiscoveryType: "FAKE_DNS", + ConnectTimeout: 890, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `jwtprovider.consul.hashicorp.com "test-jwks-invalid-uri" is invalid: spec.jsonWebKeySet.remote.jwksCluster.discoveryType: Invalid value: "FAKE_DNS": unsupported jwks cluster discovery type.`, + }, + }, + + "invalid - remote jwks invalid jwkcluster - all trustedCa fields set": { + input: &JWTProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-jwks-invalid-uri", + }, + Spec: JWTProviderSpec{ + JSONWebKeySet: &JSONWebKeySet{ + Remote: &RemoteJWKS{ + URI: "https://jwks.example.com", + JWKSCluster: &JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &JWKSTLSCertificate{ + TrustedCA: &JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + EnvironmentVariable: "env-variable", + InlineString: "inline-string", + InlineBytes: []byte("inline-bytes"), + }, + }, + ConnectTimeout: 890, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `jwtprovider.consul.hashicorp.com "test-jwks-invalid-uri" is invalid: spec.jsonWebKeySet.remote.jwksCluster.tlsCertificates.trustedCa: Invalid value:`, + `exactly one of 'filename', 'environmentVariable', 'inlineString' or 'inlineBytes' is required`, + }, + }, + + "invalid - remote jwks invalid jwkcluster - set 2 trustedCa fields": { + input: &JWTProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-jwks-invalid-uri", + }, + Spec: JWTProviderSpec{ + JSONWebKeySet: &JSONWebKeySet{ + Remote: &RemoteJWKS{ + URI: "https://jwks.example.com", + JWKSCluster: &JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &JWKSTLSCertificate{ + TrustedCA: &JWKSTLSCertTrustedCA{ + Filename: "cert.crt", + EnvironmentVariable: "env-variable", + }, + }, + ConnectTimeout: 890, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `jwtprovider.consul.hashicorp.com "test-jwks-invalid-uri" is invalid: spec.jsonWebKeySet.remote.jwksCluster.tlsCertificates.trustedCa: Invalid value:`, + `exactly one of 'filename', 'environmentVariable', 'inlineString' or 'inlineBytes' is required`, + }, + }, + "invalid - JWT location with all fields": { input: &JWTProvider{ ObjectMeta: metav1.ObjectMeta{ diff --git a/control-plane/api/v1alpha1/zz_generated.deepcopy.go b/control-plane/api/v1alpha1/zz_generated.deepcopy.go index 5b54f4a5c5..23269fd8f0 100644 --- a/control-plane/api/v1alpha1/zz_generated.deepcopy.go +++ b/control-plane/api/v1alpha1/zz_generated.deepcopy.go @@ -848,6 +848,11 @@ func (in *IngressServiceConfig) DeepCopyInto(out *IngressServiceConfig) { *out = new(uint32) **out = **in } + if in.PassiveHealthCheck != nil { + in, out := &in.PassiveHealthCheck, &out.PassiveHealthCheck + *out = new(PassiveHealthCheck) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressServiceConfig. @@ -1116,6 +1121,26 @@ func (in *JSONWebKeySet) DeepCopy() *JSONWebKeySet { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWKSCluster) DeepCopyInto(out *JWKSCluster) { + *out = *in + if in.TLSCertificates != nil { + in, out := &in.TLSCertificates, &out.TLSCertificates + *out = new(JWKSTLSCertificate) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWKSCluster. +func (in *JWKSCluster) DeepCopy() *JWKSCluster { + if in == nil { + return nil + } + out := new(JWKSCluster) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWKSRetryPolicy) DeepCopyInto(out *JWKSRetryPolicy) { *out = *in @@ -1136,6 +1161,66 @@ func (in *JWKSRetryPolicy) DeepCopy() *JWKSRetryPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWKSTLSCertProviderInstance) DeepCopyInto(out *JWKSTLSCertProviderInstance) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWKSTLSCertProviderInstance. +func (in *JWKSTLSCertProviderInstance) DeepCopy() *JWKSTLSCertProviderInstance { + if in == nil { + return nil + } + out := new(JWKSTLSCertProviderInstance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWKSTLSCertTrustedCA) DeepCopyInto(out *JWKSTLSCertTrustedCA) { + *out = *in + if in.InlineBytes != nil { + in, out := &in.InlineBytes, &out.InlineBytes + *out = make([]byte, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWKSTLSCertTrustedCA. +func (in *JWKSTLSCertTrustedCA) DeepCopy() *JWKSTLSCertTrustedCA { + if in == nil { + return nil + } + out := new(JWKSTLSCertTrustedCA) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWKSTLSCertificate) DeepCopyInto(out *JWKSTLSCertificate) { + *out = *in + if in.CaCertificateProviderInstance != nil { + in, out := &in.CaCertificateProviderInstance, &out.CaCertificateProviderInstance + *out = new(JWKSTLSCertProviderInstance) + **out = **in + } + if in.TrustedCA != nil { + in, out := &in.TrustedCA, &out.TrustedCA + *out = new(JWKSTLSCertTrustedCA) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWKSTLSCertificate. +func (in *JWKSTLSCertificate) DeepCopy() *JWKSTLSCertificate { + if in == nil { + return nil + } + out := new(JWKSTLSCertificate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JWTCacheConfig) DeepCopyInto(out *JWTCacheConfig) { *out = *in @@ -2154,6 +2239,11 @@ func (in *RemoteJWKS) DeepCopyInto(out *RemoteJWKS) { *out = new(JWKSRetryPolicy) (*in).DeepCopyInto(*out) } + if in.JWKSCluster != nil { + in, out := &in.JWKSCluster, &out.JWKSCluster + *out = new(JWKSCluster) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteJWKS. diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_jwtproviders.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_jwtproviders.yaml index 2e8ac24330..8584404c56 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_jwtproviders.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_jwtproviders.yaml @@ -116,6 +116,66 @@ spec: the proxy listener will wait for the JWKS to be fetched before being activated. \n Default value is false." type: boolean + jwksCluster: + description: "JWKSCluster defines how the specified Remote JWKS + URI is to be fetched." + properties: + connectTimeout: + description: "The timeout for new network connections to hosts + in the cluster. \n If not set, a default value of 5s will be + used." + format: int64 + type: integer + discoveryType: + description: "DiscoveryType refers to the service discovery type + to use for resolving the cluster. \n Defaults to STRICT_DNS." + type: string + tlsCertificates: + description: "TLSCertificates refers to the data containing + certificate authority certificates to use in verifying a presented + peer certificate." + properties: + caCertificateProviderInstance: + description: "CaCertificateProviderInstance Certificate provider + instance for fetching TLS certificates." + properties: + instanceName: + description: "InstanceName refers to the certificate provider + instance name. \n The default value is 'default'." + type: string + certificateName: + description: "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. \n + The default value is the empty string." + type: string + type: object + trustedCA: + description: "TrustedCA defines TLS certificate data containing + certificate authority certificates to use in verifying a presented + peer certificate. \n Exactly one of Filename, EnvironmentVariable, + InlineString or InlineBytes must be specified." + properties: + filename: + description: "The name of the file on the local system to use a + data source for trusted CA certificates." + type: string + environmentVariable: + description: "The environment variable on the local system to use + a data source for trusted CA certificates." + type: string + inlineString: + description: "A string to inline in the configuration for use as + a data source for trusted CA certificates." + type: string + inlineBytes: + description: "A sequence of bytes to inline in the configuration + for use as a data source for trusted CA certificates." + type: string + type: object + type: object + type: object requestTimeoutMs: description: RequestTimeoutMs is the number of milliseconds to time out when making a request for the JWKS. diff --git a/control-plane/controllers/configentry_controller_test.go b/control-plane/controllers/configentry_controller_test.go index 071d67ca6f..07a3ea3730 100644 --- a/control-plane/controllers/configentry_controller_test.go +++ b/control-plane/controllers/configentry_controller_test.go @@ -476,6 +476,68 @@ func TestConfigEntryControllers_createsConfigEntry(t *testing.T) { require.Equal(t, "test-issuer", jwt.Issuer) }, }, + { + kubeKind: "JWTProvider", + consulKind: capi.JWTProvider, + configEntryResource: &v1alpha1.JWTProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-jwt-provider", + Namespace: kubeNS, + }, + Spec: v1alpha1.JWTProviderSpec{ + JSONWebKeySet: &v1alpha1.JSONWebKeySet{ + Remote: &v1alpha1.RemoteJWKS{ + URI: "https://jwks.example.com", + JWKSCluster: &v1alpha1.JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &v1alpha1.JWKSTLSCertificate{ + CaCertificateProviderInstance: &v1alpha1.JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + CertificateName: "ROOTCA", + }, + }, + }, + }, + }, + Issuer: "test-issuer", + }, + }, + reconciler: func(client client.Client, cfg *consul.Config, watcher consul.ServerConnectionManager, logger logr.Logger) testReconciler { + return &JWTProviderController{ + Client: client, + Log: logger, + ConfigEntryController: &ConfigEntryController{ + ConsulClientConfig: cfg, + ConsulServerConnMgr: watcher, + DatacenterName: datacenterName, + }, + } + }, + compare: func(t *testing.T, consulEntry capi.ConfigEntry) { + jwt, ok := consulEntry.(*capi.JWTProviderConfigEntry) + require.True(t, ok, "cast error") + require.Equal(t, capi.JWTProvider, jwt.Kind) + require.Equal(t, "test-jwt-provider", jwt.Name) + require.Equal(t, + &capi.JSONWebKeySet{ + Remote: &capi.RemoteJWKS{ + URI: "https://jwks.example.com", + JWKSCluster: &capi.JWKSCluster{ + DiscoveryType: "STRICT_DNS", + TLSCertificates: &capi.JWKSTLSCertificate{ + CaCertificateProviderInstance: &capi.JWKSTLSCertProviderInstance{ + InstanceName: "InstanceName", + CertificateName: "ROOTCA", + }, + }, + }, + }, + }, + jwt.JSONWebKeySet, + ) + require.Equal(t, "test-issuer", jwt.Issuer) + }, + }, } for _, c := range cases {