Skip to content
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
34 changes: 31 additions & 3 deletions api/v1alpha1/jwt_types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package v1alpha1

import corev1 "k8s.io/api/core/v1"
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// JWTAuthentication defines the providers used to configure JWT authentication
type JWTAuthentication struct {
Expand Down Expand Up @@ -106,13 +110,17 @@ type JWTClaimToHeader struct {
}

// JWKS (JSON Web Key Set) configures the source for the JWKS
// Exactly one of LocalJWKS or RemoteJWKS must be specified.
// +kubebuilder:validation:ExactlyOneOf=local;remote
type JWKS struct {
// LocalJWKS configures getting the public keys to validate the JWT from a Kubernetes configmap,
// or inline (raw string) JWKS.
// +required
// +optional
LocalJWKS *LocalJWKS `json:"local,omitempty"`

// TODO: Add support for remote JWKS
// RemoteJWKS configures getting the public keys to validate the JWT from a remote JWKS server.
// +optional
RemoteJWKS *RemoteJWKS `json:"remote,omitempty"`
}

// LocalJWKS configures getting the public keys to validate the JWT from a Kubernetes ConfigMap,
Expand All @@ -131,3 +139,23 @@ type LocalJWKS struct {
// +optional
ConfigMapRef *corev1.LocalObjectReference `json:"configMapRef,omitempty"`
}

type RemoteJWKS struct {
// URL is the URL of the remote JWKS server, it must be a full FQDN with protocol, host and path.
// For example, https://example.com/keys
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=2048
// +required
URL string `json:"url"`

// BackendRef is reference to the backend of the JWKS server.
// +required
BackendRef *gwv1.BackendObjectReference `json:"backendRef"`

// Duration after which the cached JWKS expires.
// If unspecified, the default cache duration is 5 minutes.
// +optional
// +kubebuilder:validation:XValidation:rule="matches(self, '^([0-9]{1,5}(h|m|s|ms)){1,4}$')",message="invalid duration value"
// +kubebuilder:validation:XValidation:rule="duration(self) >= duration('1ms')",message="cacheDuration must be at least 1ms."
CacheDuration *metav1.Duration `json:"cacheDuration,omitempty"`
}
30 changes: 30 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -651,9 +651,108 @@ spec:
must be set
rule: '[has(self.inline),has(self.configMapRef)].filter(x,x==true).size()
== 1'
required:
- local
remote:
description: RemoteJWKS configures getting the public keys
to validate the JWT from a remote JWKS server.
properties:
backendRef:
description: BackendRef is reference to the backend
of the JWKS server.
properties:
group:
default: ""
description: |-
Group is the group of the referent. For example, "gateway.networking.k8s.io".
When unspecified or empty string, core API group is inferred.
maxLength: 253
pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
type: string
kind:
default: Service
description: |-
Kind is the Kubernetes resource kind of the referent. For example
"Service".

Defaults to "Service" when not specified.

ExternalName services can refer to CNAME DNS records that may live
outside of the cluster and as such are difficult to reason about in
terms of conformance. They also may not be safe to forward to (see
CVE-2021-25740 for more information). Implementations SHOULD NOT
support ExternalName Services.

Support: Core (Services with a type other than ExternalName)

Support: Implementation-specific (Services with type ExternalName)
maxLength: 63
minLength: 1
pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
type: string
name:
description: Name is the name of the referent.
maxLength: 253
minLength: 1
type: string
namespace:
description: |-
Namespace is the namespace of the backend. When unspecified, the local
namespace is inferred.

Note that when a namespace different than the local namespace is specified,
a ReferenceGrant object is required in the referent namespace to allow that
namespace's owner to accept the reference. See the ReferenceGrant
documentation for details.

Support: Core
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
port:
description: |-
Port specifies the destination port number to use for this resource.
Port is required when the referent is a Kubernetes Service. In this
case, the port number is the service port number, not the target port.
For other resources, destination port might be derived from the referent
resource or this field.
format: int32
maximum: 65535
minimum: 1
type: integer
required:
- name
type: object
x-kubernetes-validations:
- message: Must have port for Service reference
rule: '(size(self.group) == 0 && self.kind == ''Service'')
? has(self.port) : true'
cacheDuration:
description: |-
Duration after which the cached JWKS expires.
If unspecified, the default cache duration is 5 minutes.
type: string
x-kubernetes-validations:
- message: invalid duration value
rule: matches(self, '^([0-9]{1,5}(h|m|s|ms)){1,4}$')
- message: cacheDuration must be at least 1ms.
rule: duration(self) >= duration('1ms')
url:
description: |-
URL is the URL of the remote JWKS server, it must be a full FQDN with protocol, host and path.
For example, https://example.com/keys
maxLength: 2048
minLength: 1
type: string
required:
- backendRef
- url
type: object
type: object
x-kubernetes-validations:
- message: exactly one of the fields in [local remote] must
be set
rule: '[has(self.local),has(self.remote)].filter(x,x==true).size()
== 1'
keepToken:
default: Remove
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,15 @@ func TranslateGatewayExtensionBuilder(commoncol *collections.CommonCollections)

p.RateLimit = rateLimitConfig
case gExt.JwtProviders != nil:
jwtConfig, err := resolveJwtProviders(krtctx, commoncol.ConfigMaps, gExt.Name, gExt.Namespace, gExt.JwtProviders)
jwtConfig, err := resolveJwtProviders(
krtctx,
commoncol.ConfigMaps,
commoncol.BackendIndex,
gExt.ObjectSource,
gExt.Name,
gExt.Namespace,
gExt.JwtProviders,
)
if err != nil {
p.Err = fmt.Errorf("jwt: %w", err)
return p
Expand All @@ -177,6 +185,8 @@ func TranslateGatewayExtensionBuilder(commoncol *collections.CommonCollections)
func resolveJwtProviders(
krtctx krt.HandlerContext,
configMaps krt.Collection[*corev1.ConfigMap],
backendResolver backendResolver,
gwExtObj ir.ObjectSource,
policyName, policyNamespace string,
jwtProviders []v1alpha1.NamedJWTProvider,
) (*envoyjwtauthnv3.JwtAuthentication, error) {
Expand All @@ -185,7 +195,14 @@ func resolveJwtProviders(

for _, provider := range jwtProviders {
providerNameForPolicy := ProviderName(policyNameNamespace, provider.Name)
jwtProvider, err := translateProvider(krtctx, provider.JWTProvider, policyNamespace, configMaps)
jwtProvider, err := translateProvider(
krtctx,
provider.JWTProvider,
policyNamespace,
configMaps,
backendResolver,
gwExtObj,
)
if err != nil {
return nil, err
}
Expand Down
84 changes: 67 additions & 17 deletions internal/kgateway/extensions2/plugins/trafficpolicy/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import (
jwtauthnv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3"
"github.com/go-jose/go-jose/v4"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"istio.io/istio/pkg/kube/krt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"

"github.com/kgateway-dev/kgateway/v2/api/v1alpha1"
"github.com/kgateway-dev/kgateway/v2/internal/kgateway/extensions2/pluginutils"
Expand All @@ -31,6 +33,8 @@ const (
PayloadInMetadata = "payload"
jwtFilterNamePrefix = "jwt"
jwtConfigMapKey = "jwks"

remoteJWKSTimeoutSecs = 5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see an API value for this, is this just hardcoded?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup for now

)

type jwtIr struct {
Expand Down Expand Up @@ -158,7 +162,14 @@ func ProviderName(resourceName, providerName string) string {
return fmt.Sprintf("%s_%s", resourceName, providerName)
}

func translateProvider(krtctx krt.HandlerContext, provider v1alpha1.JWTProvider, policyNs string, configMaps krt.Collection[*corev1.ConfigMap]) (*jwtauthnv3.JwtProvider, error) {
func translateProvider(
krtctx krt.HandlerContext,
provider v1alpha1.JWTProvider,
policyNs string,
configMaps krt.Collection[*corev1.ConfigMap],
resolver backendResolver,
gwExtObj ir.ObjectSource,
) (*jwtauthnv3.JwtProvider, error) {
var claimToHeaders []*jwtauthnv3.JwtClaimToHeader
for _, claim := range provider.ClaimsToHeaders {
claimToHeaders = append(claimToHeaders, &jwtauthnv3.JwtClaimToHeader{
Expand All @@ -182,8 +193,7 @@ func translateProvider(krtctx krt.HandlerContext, provider v1alpha1.JWTProvider,
jwtProvider.ClearRouteCache = true
}
translateTokenSource(provider, jwtProvider)
err := translateJwks(krtctx, provider.JWKS, configMaps, policyNs, jwtProvider)

err := translateJwks(krtctx, provider.JWKS, policyNs, jwtProvider, configMaps, resolver, gwExtObj)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -211,25 +221,65 @@ func translateTokenSource(provider v1alpha1.JWTProvider, out *jwtauthnv3.JwtProv
}
}

func translateJwks(krtctx krt.HandlerContext, jwkConfig v1alpha1.JWKS, configMaps krt.Collection[*corev1.ConfigMap], policyNs string, out *jwtauthnv3.JwtProvider) error {
var jwkSource *jwtauthnv3.JwtProvider_LocalJwks
if jwkConfig.LocalJWKS.Inline != nil {
var err error
jwkSource, err = translateJwksInline(*jwkConfig.LocalJWKS.Inline)
if err != nil {
return err
type backendResolver interface {
GetBackendFromRef(krt.HandlerContext, ir.ObjectSource, gwv1.BackendObjectReference) (*ir.BackendObjectIR, error)
}

func translateJwks(
krtctx krt.HandlerContext,
jwkConfig v1alpha1.JWKS,
policyNs string,
out *jwtauthnv3.JwtProvider,
configMaps krt.Collection[*corev1.ConfigMap],
resolver backendResolver,
gwExtObj ir.ObjectSource,
) error {
switch {
case jwkConfig.LocalJWKS != nil:
switch {
case jwkConfig.LocalJWKS.Inline != nil:
jwkSource, err := translateJwksInline(*jwkConfig.LocalJWKS.Inline)
if err != nil {
return err
}
out.JwksSourceSpecifier = jwkSource
case jwkConfig.LocalJWKS.ConfigMapRef != nil:
cm, err := GetConfigMap(krtctx, configMaps, jwkConfig.LocalJWKS.ConfigMapRef.Name, policyNs)
if err != nil {
return fmt.Errorf("failed to find configmap %s: %v", jwkConfig.LocalJWKS.ConfigMapRef.Name, err)
}
jwkSource, err := translateJwksConfigMap(cm)
if err != nil {
return err
}
out.JwksSourceSpecifier = jwkSource
}
} else if jwkConfig.LocalJWKS.ConfigMapRef != nil {
cm, err := GetConfigMap(krtctx, configMaps, jwkConfig.LocalJWKS.ConfigMapRef.Name, policyNs)
if err != nil {
return fmt.Errorf("failed to find configmap %s: %v", jwkConfig.LocalJWKS.ConfigMapRef.Name, err)
case jwkConfig.RemoteJWKS != nil:
remote := jwkConfig.RemoteJWKS
if remote.BackendRef == nil {
// shouldn't happen due to CEL validation
return fmt.Errorf("remote jwks: nil backend ref")
}
jwkSource, err = translateJwksConfigMap(cm)
backend, err := resolver.GetBackendFromRef(krtctx, gwExtObj, *remote.BackendRef)
if err != nil {
return err
return fmt.Errorf("remote jwks: unresolved backend ref: %w", err)
}
jwksOut := &jwtauthnv3.JwtProvider_RemoteJwks{
RemoteJwks: &jwtauthnv3.RemoteJwks{
HttpUri: &envoycorev3.HttpUri{
Timeout: &durationpb.Duration{Seconds: remoteJWKSTimeoutSecs},
Uri: remote.URL,
HttpUpstreamType: &envoycorev3.HttpUri_Cluster{
Cluster: backend.ClusterName(),
},
},
},
}
if remote.CacheDuration != nil {
jwksOut.RemoteJwks.CacheDuration = durationpb.New(remote.CacheDuration.Duration)
}
out.JwksSourceSpecifier = jwksOut
}
out.JwksSourceSpecifier = jwkSource
return nil
}

Expand Down
Loading