Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose JWKS cluster config through JWTProviderConfigEntry #17978

Merged
merged 4 commits into from
Jul 4, 2023
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
3 changes: 3 additions & 0 deletions .changelog/17978.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
mesh: Expose remote jwks cluster configuration through jwt-provider config entry
```
20 changes: 20 additions & 0 deletions agent/proxycfg/proxycfg.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ func (o *ConfigSnapshot) DeepCopy() *ConfigSnapshot {
*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 {
Expand Down
137 changes: 132 additions & 5 deletions agent/structs/config_entry_jwt_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import (

const (
DefaultClockSkewSeconds = 30

DiscoveryTypeStrictDNS ClusterDiscoveryType = "STRICT_DNS"
DiscoveryTypeStatic ClusterDiscoveryType = "STATIC"
DiscoveryTypeLogicalDNS ClusterDiscoveryType = "LOGICAL_DNS"
DiscoveryTypeEDS ClusterDiscoveryType = "EDS"
DiscoveryTypeOriginalDST ClusterDiscoveryType = "ORIGINAL_DST"
)

type JWTProviderConfigEntry struct {
Expand Down Expand Up @@ -97,7 +103,7 @@ func (location *JWTLocation) Validate() error {
hasCookie := location.Cookie != nil

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 {
Expand Down Expand Up @@ -205,7 +211,7 @@ func (ks *LocalJWKS) Validate() error {
hasJWKS := ks.JWKS != ""

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 {
Expand Down Expand Up @@ -245,6 +251,9 @@ type RemoteJWKS struct {
//
// There is no retry by default.
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 {
Expand All @@ -257,9 +266,127 @@ func (ks *RemoteJWKS) Validate() error {
}

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.
roncodingenthusiast marked this conversation as resolved.
Show resolved Hide resolved
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
}

Expand Down Expand Up @@ -293,7 +420,7 @@ type RetryPolicyBackOff struct {
func (r *RetryPolicyBackOff) Validate() error {

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
Expand Down Expand Up @@ -339,7 +466,7 @@ func (jwks *JSONWebKeySet) Validate() error {
hasRemoteKeySet := jwks.Remote != nil

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 {
Expand Down
87 changes: 81 additions & 6 deletions agent/structs/config_entry_jwt_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func newTestAuthz(t *testing.T, src string) acl.Authorizer {

var tenSeconds time.Duration = 10 * time.Second
var hundredSeconds time.Duration = 100 * time.Second
var connectTimeout = time.Duration(5) * time.Second

func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
defaultMeta := DefaultEnterpriseMetaInDefaultPartition()
Expand Down Expand Up @@ -113,7 +114,7 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
Name: "okta",
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": {
entry: &JWTProviderConfigEntry{
Expand All @@ -138,7 +139,7 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
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": {
entry: &JWTProviderConfigEntry{
Expand All @@ -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": {
entry: &JWTProviderConfigEntry{
Expand Down Expand Up @@ -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": {
entry: &JWTProviderConfigEntry{
Expand All @@ -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": {
entry: &JWTProviderConfigEntry{
Expand All @@ -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": {
entry: &JWTProviderConfigEntry{
Expand All @@ -265,6 +322,15 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
MaxInterval: hundredSeconds,
},
},
JWKSCluster: &JWKSCluster{
DiscoveryType: "STATIC",
ConnectTimeout: connectTimeout,
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
},
},
},
},
},
Forwarding: &JWTForwardingConfig{
Expand Down Expand Up @@ -297,6 +363,15 @@ func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
MaxInterval: hundredSeconds,
},
},
JWKSCluster: &JWKSCluster{
DiscoveryType: "STATIC",
ConnectTimeout: connectTimeout,
TLSCertificates: &JWKSTLSCertificate{
TrustedCA: &JWKSTLSCertTrustedCA{
Filename: "myfile.cert",
},
},
},
},
},
Forwarding: &JWTForwardingConfig{
Expand Down
Loading