Browse Source

feat: add Cloudsmith generator for container registry authentication (#5267)

* feat: add Cloudsmith generator for container registry authentication

Implements CloudsmithAccessToken generator that uses OIDC token exchange
to authenticate with Cloudsmith's container registry. The generator creates
dockerconfigjson secrets for pulling/pushing container images.

Key features:
- OIDC authentication using Kubernetes service account tokens
- Generates dockerconfigjson secrets for Docker registry access
- Configurable API host with default to api.cloudsmith.io
- Support for both namespaced and cluster-wide generators
- Comprehensive documentation and examples

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>

* fix: code duplication feedback

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>

* fix: add documentation link for cloudsmith

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>

* fix:  param rename

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>

* Fix: review feedback

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>

* fix: update headers

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>

* fix:  add generated files

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>

---------

Signed-off-by: Ian Duffy <iduffy@cloudsmith.io>
Co-authored-by: Gergely Brautigam <skarlso777@gmail.com>
Ian Duffy 8 months ago
parent
commit
5f1680c32e

+ 1 - 1
apis/externalsecrets/v1/externalsecret_types.go

@@ -489,7 +489,7 @@ type GeneratorRef struct {
 	APIVersion string `json:"apiVersion,omitempty"`
 
 	// Specify the Kind of the generator resource
-	// +kubebuilder:validation:Enum=ACRAccessToken;ClusterGenerator;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana;MFA
+	// +kubebuilder:validation:Enum=ACRAccessToken;ClusterGenerator;CloudsmithAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana;MFA
 	Kind string `json:"kind"`
 
 	// Specify the name of the generator resource

+ 2 - 0
apis/generators/v1alpha1/register.go

@@ -50,6 +50,7 @@ var (
 	VaultDynamicSecretKind    = reflect.TypeOf(VaultDynamicSecret{}).Name()
 	GithubAccessTokenKind     = reflect.TypeOf(GithubAccessToken{}).Name()
 	QuayAccessTokenKind       = reflect.TypeOf(QuayAccessToken{}).Name()
+	CloudsmithAccessTokenKind = reflect.TypeOf(CloudsmithAccessToken{}).Name()
 	UUIDKind                  = reflect.TypeOf(UUID{}).Name()
 	GrafanaKind               = reflect.TypeOf(Grafana{}).Name()
 	MFAKind                   = reflect.TypeOf(MFA{}).Name()
@@ -76,6 +77,7 @@ func init() {
 
 	SchemeBuilder.Register(&ACRAccessToken{}, &ACRAccessTokenList{})
 	SchemeBuilder.Register(&ClusterGenerator{}, &ClusterGeneratorList{})
+	SchemeBuilder.Register(&CloudsmithAccessToken{}, &CloudsmithAccessTokenList{})
 	SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationTokenList{})
 	SchemeBuilder.Register(&Fake{}, &FakeList{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})

+ 60 - 0
apis/generators/v1alpha1/types_cloudsmith.go

@@ -0,0 +1,60 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type CloudsmithAccessTokenSpec struct {
+	// APIURL configures the Cloudsmith API URL. Defaults to https://api.cloudsmith.io.
+	// +kubebuilder:validation:Optional
+	APIURL string `json:"apiUrl,omitempty"`
+	// OrgSlug is the organization slug in Cloudsmith
+	// +kubebuilder:validation:Required
+	OrgSlug string `json:"orgSlug"`
+	// ServiceSlug is the service slug in Cloudsmith for OIDC authentication
+	// +kubebuilder:validation:Required
+	ServiceSlug string `json:"serviceSlug"`
+	// Name of the service account you are federating with
+	// +kubebuilder:validation:Required
+	ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
+}
+
+// CloudsmithAccessToken generates Cloudsmith access token using OIDC authentication
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// +kubebuilder:subresource:status
+// +kubebuilder:metadata:labels="external-secrets.io/component=controller"
+// +kubebuilder:resource:scope=Namespaced,categories={external-secrets, external-secrets-generators}
+type CloudsmithAccessToken struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec CloudsmithAccessTokenSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// CloudsmithAccessTokenList contains a list of CloudsmithAccessToken resources.
+type CloudsmithAccessTokenList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []CloudsmithAccessToken `json:"items"`
+}

+ 3 - 1
apis/generators/v1alpha1/types_cluster.go

@@ -29,11 +29,12 @@ type ClusterGeneratorSpec struct {
 }
 
 // GeneratorKind represents a kind of generator.
-// +kubebuilder:validation:Enum=ACRAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana
+// +kubebuilder:validation:Enum=ACRAccessToken;CloudsmithAccessToken;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;SSHKey;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana
 type GeneratorKind string
 
 const (
 	GeneratorKindACRAccessToken        GeneratorKind = "ACRAccessToken"
+	GeneratorKindCloudsmithAccessToken GeneratorKind = "CloudsmithAccessToken"
 	GeneratorKindECRAuthorizationToken GeneratorKind = "ECRAuthorizationToken"
 	GeneratorKindFake                  GeneratorKind = "Fake"
 	GeneratorKindGCRAccessToken        GeneratorKind = "GCRAccessToken"
@@ -53,6 +54,7 @@ const (
 // +kubebuilder:validation:MinProperties=1
 type GeneratorSpec struct {
 	ACRAccessTokenSpec        *ACRAccessTokenSpec        `json:"acrAccessTokenSpec,omitempty"`
+	CloudsmithAccessTokenSpec *CloudsmithAccessTokenSpec `json:"cloudsmithAccessTokenSpec,omitempty"`
 	ECRAuthorizationTokenSpec *ECRAuthorizationTokenSpec `json:"ecrAuthorizationTokenSpec,omitempty"`
 	FakeSpec                  *FakeSpec                  `json:"fakeSpec,omitempty"`
 	GCRAccessTokenSpec        *GCRAccessTokenSpec        `json:"gcrAccessTokenSpec,omitempty"`

+ 79 - 0
apis/generators/v1alpha1/zz_generated.deepcopy.go

@@ -288,6 +288,80 @@ func (in *AzureACRWorkloadIdentityAuth) DeepCopy() *AzureACRWorkloadIdentityAuth
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CloudsmithAccessToken) DeepCopyInto(out *CloudsmithAccessToken) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudsmithAccessToken.
+func (in *CloudsmithAccessToken) DeepCopy() *CloudsmithAccessToken {
+	if in == nil {
+		return nil
+	}
+	out := new(CloudsmithAccessToken)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CloudsmithAccessToken) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CloudsmithAccessTokenList) DeepCopyInto(out *CloudsmithAccessTokenList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]CloudsmithAccessToken, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudsmithAccessTokenList.
+func (in *CloudsmithAccessTokenList) DeepCopy() *CloudsmithAccessTokenList {
+	if in == nil {
+		return nil
+	}
+	out := new(CloudsmithAccessTokenList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CloudsmithAccessTokenList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CloudsmithAccessTokenSpec) DeepCopyInto(out *CloudsmithAccessTokenSpec) {
+	*out = *in
+	in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudsmithAccessTokenSpec.
+func (in *CloudsmithAccessTokenSpec) DeepCopy() *CloudsmithAccessTokenSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(CloudsmithAccessTokenSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterGenerator) DeepCopyInto(out *ClusterGenerator) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -675,6 +749,11 @@ func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
 		*out = new(ACRAccessTokenSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.CloudsmithAccessTokenSpec != nil {
+		in, out := &in.CloudsmithAccessTokenSpec, &out.CloudsmithAccessTokenSpec
+		*out = new(CloudsmithAccessTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.ECRAuthorizationTokenSpec != nil {
 		in, out := &in.ECRAuthorizationTokenSpec, &out.ECRAuthorizationTokenSpec
 		*out = new(ECRAuthorizationTokenSpec)

+ 2 - 0
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -161,6 +161,7 @@ spec:
                                   enum:
                                   - ACRAccessToken
                                   - ClusterGenerator
+                                  - CloudsmithAccessToken
                                   - ECRAuthorizationToken
                                   - Fake
                                   - GCRAccessToken
@@ -390,6 +391,7 @@ spec:
                                   enum:
                                   - ACRAccessToken
                                   - ClusterGenerator
+                                  - CloudsmithAccessToken
                                   - ECRAuthorizationToken
                                   - Fake
                                   - GCRAccessToken

+ 1 - 0
config/crds/bases/external-secrets.io_clusterpushsecrets.yaml

@@ -263,6 +263,7 @@ spec:
                             enum:
                             - ACRAccessToken
                             - ClusterGenerator
+                            - CloudsmithAccessToken
                             - ECRAuthorizationToken
                             - Fake
                             - GCRAccessToken

+ 2 - 0
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -141,6 +141,7 @@ spec:
                               enum:
                               - ACRAccessToken
                               - ClusterGenerator
+                              - CloudsmithAccessToken
                               - ECRAuthorizationToken
                               - Fake
                               - GCRAccessToken
@@ -369,6 +370,7 @@ spec:
                               enum:
                               - ACRAccessToken
                               - ClusterGenerator
+                              - CloudsmithAccessToken
                               - ECRAuthorizationToken
                               - Fake
                               - GCRAccessToken

+ 1 - 0
config/crds/bases/external-secrets.io_pushsecrets.yaml

@@ -187,6 +187,7 @@ spec:
                         enum:
                         - ACRAccessToken
                         - ClusterGenerator
+                        - CloudsmithAccessToken
                         - ECRAuthorizationToken
                         - Fake
                         - GCRAccessToken

+ 95 - 0
config/crds/bases/generators.external-secrets.io_cloudsmithaccesstokens.yaml

@@ -0,0 +1,95 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.19.0
+  labels:
+    external-secrets.io/component: controller
+  name: cloudsmithaccesstokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+    - external-secrets
+    - external-secrets-generators
+    kind: CloudsmithAccessToken
+    listKind: CloudsmithAccessTokenList
+    plural: cloudsmithaccesstokens
+    singular: cloudsmithaccesstoken
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: CloudsmithAccessToken generates Cloudsmith access token using
+          OIDC authentication
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            properties:
+              apiUrl:
+                description: APIURL configures the Cloudsmith API URL. Defaults to
+                  https://api.cloudsmith.io.
+                type: string
+              orgSlug:
+                description: OrgSlug is the organization slug in Cloudsmith
+                type: string
+              serviceAccountRef:
+                description: Name of the service account you are federating with
+                properties:
+                  audiences:
+                    description: |-
+                      Audience specifies the `aud` claim for the service account token
+                      If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                      then this audiences will be appended to the list
+                    items:
+                      type: string
+                    type: array
+                  name:
+                    description: The name of the ServiceAccount resource being referred
+                      to.
+                    maxLength: 253
+                    minLength: 1
+                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                    type: string
+                  namespace:
+                    description: |-
+                      Namespace of the resource being referred to.
+                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                    maxLength: 63
+                    minLength: 1
+                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                    type: string
+                required:
+                - name
+                type: object
+              serviceSlug:
+                description: ServiceSlug is the service slug in Cloudsmith for OIDC
+                  authentication
+                type: string
+            required:
+            - orgSlug
+            - serviceAccountRef
+            - serviceSlug
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

+ 49 - 0
config/crds/bases/generators.external-secrets.io_clustergenerators.yaml

@@ -211,6 +211,54 @@ spec:
                     - auth
                     - registry
                     type: object
+                  cloudsmithAccessTokenSpec:
+                    properties:
+                      apiUrl:
+                        description: APIURL configures the Cloudsmith API URL. Defaults
+                          to https://api.cloudsmith.io.
+                        type: string
+                      orgSlug:
+                        description: OrgSlug is the organization slug in Cloudsmith
+                        type: string
+                      serviceAccountRef:
+                        description: Name of the service account you are federating
+                          with
+                        properties:
+                          audiences:
+                            description: |-
+                              Audience specifies the `aud` claim for the service account token
+                              If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                              then this audiences will be appended to the list
+                            items:
+                              type: string
+                            type: array
+                          name:
+                            description: The name of the ServiceAccount resource being
+                              referred to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              Namespace of the resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        required:
+                        - name
+                        type: object
+                      serviceSlug:
+                        description: ServiceSlug is the service slug in Cloudsmith
+                          for OIDC authentication
+                        type: string
+                    required:
+                    - orgSlug
+                    - serviceAccountRef
+                    - serviceSlug
+                    type: object
                   ecrAuthorizationTokenSpec:
                     properties:
                       auth:
@@ -2104,6 +2152,7 @@ spec:
                 description: Kind the kind of this generator.
                 enum:
                 - ACRAccessToken
+                - CloudsmithAccessToken
                 - ECRAuthorizationToken
                 - Fake
                 - GCRAccessToken

+ 1 - 0
config/crds/bases/kustomization.yaml

@@ -9,6 +9,7 @@ resources:
   - external-secrets.io_pushsecrets.yaml
   - external-secrets.io_secretstores.yaml
   - generators.external-secrets.io_acraccesstokens.yaml
+  - generators.external-secrets.io_cloudsmithaccesstokens.yaml
   - generators.external-secrets.io_clustergenerators.yaml
   - generators.external-secrets.io_ecrauthorizationtokens.yaml
   - generators.external-secrets.io_fakes.yaml

+ 3 - 0
deploy/charts/external-secrets/templates/rbac.yaml

@@ -96,6 +96,7 @@ rules:
     - "generators.external-secrets.io"
     resources:
     - "acraccesstokens"
+    - "cloudsmithaccesstokens"
     {{- if .Values.processClusterGenerator }}
     - "clustergenerators"
     {{- end }}
@@ -229,6 +230,7 @@ rules:
     - "generators.external-secrets.io"
     resources:
     - "acraccesstokens"
+    - "cloudsmithaccesstokens"
     {{- if .Values.processClusterGenerator }}
     - "clustergenerators"
     {{- end }}
@@ -292,6 +294,7 @@ rules:
     - "generators.external-secrets.io"
     resources:
     - "acraccesstokens"
+    - "cloudsmithaccesstokens"
     {{- if .Values.processClusterGenerator }}
     - "clustergenerators"
     {{- end }}

+ 143 - 0
deploy/crds/bundle.yaml

@@ -151,6 +151,7 @@ spec:
                                     enum:
                                       - ACRAccessToken
                                       - ClusterGenerator
+                                      - CloudsmithAccessToken
                                       - ECRAuthorizationToken
                                       - Fake
                                       - GCRAccessToken
@@ -368,6 +369,7 @@ spec:
                                     enum:
                                       - ACRAccessToken
                                       - ClusterGenerator
+                                      - CloudsmithAccessToken
                                       - ECRAuthorizationToken
                                       - Fake
                                       - GCRAccessToken
@@ -1745,6 +1747,7 @@ spec:
                               enum:
                                 - ACRAccessToken
                                 - ClusterGenerator
+                                - CloudsmithAccessToken
                                 - ECRAuthorizationToken
                                 - Fake
                                 - GCRAccessToken
@@ -11429,6 +11432,7 @@ spec:
                                 enum:
                                   - ACRAccessToken
                                   - ClusterGenerator
+                                  - CloudsmithAccessToken
                                   - ECRAuthorizationToken
                                   - Fake
                                   - GCRAccessToken
@@ -11646,6 +11650,7 @@ spec:
                                 enum:
                                   - ACRAccessToken
                                   - ClusterGenerator
+                                  - CloudsmithAccessToken
                                   - ECRAuthorizationToken
                                   - Fake
                                   - GCRAccessToken
@@ -12729,6 +12734,7 @@ spec:
                           enum:
                             - ACRAccessToken
                             - ClusterGenerator
+                            - CloudsmithAccessToken
                             - ECRAuthorizationToken
                             - Fake
                             - GCRAccessToken
@@ -22520,6 +22526,98 @@ metadata:
     controller-gen.kubebuilder.io/version: v0.19.0
   labels:
     external-secrets.io/component: controller
+  name: cloudsmithaccesstokens.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+      - external-secrets
+      - external-secrets-generators
+    kind: CloudsmithAccessToken
+    listKind: CloudsmithAccessTokenList
+    plural: cloudsmithaccesstokens
+    singular: cloudsmithaccesstoken
+  scope: Namespaced
+  versions:
+    - name: v1alpha1
+      schema:
+        openAPIV3Schema:
+          description: CloudsmithAccessToken generates Cloudsmith access token using OIDC authentication
+          properties:
+            apiVersion:
+              description: |-
+                APIVersion defines the versioned schema of this representation of an object.
+                Servers should convert recognized schemas to the latest internal value, and
+                may reject unrecognized values.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+              type: string
+            kind:
+              description: |-
+                Kind is a string value representing the REST resource this object represents.
+                Servers may infer this from the endpoint the client submits requests to.
+                Cannot be updated.
+                In CamelCase.
+                More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+              type: string
+            metadata:
+              type: object
+            spec:
+              properties:
+                apiUrl:
+                  description: APIURL configures the Cloudsmith API URL. Defaults to https://api.cloudsmith.io.
+                  type: string
+                orgSlug:
+                  description: OrgSlug is the organization slug in Cloudsmith
+                  type: string
+                serviceAccountRef:
+                  description: Name of the service account you are federating with
+                  properties:
+                    audiences:
+                      description: |-
+                        Audience specifies the `aud` claim for the service account token
+                        If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                        then this audiences will be appended to the list
+                      items:
+                        type: string
+                      type: array
+                    name:
+                      description: The name of the ServiceAccount resource being referred to.
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    namespace:
+                      description: |-
+                        Namespace of the resource being referred to.
+                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                      type: string
+                  required:
+                    - name
+                  type: object
+                serviceSlug:
+                  description: ServiceSlug is the service slug in Cloudsmith for OIDC authentication
+                  type: string
+              required:
+                - orgSlug
+                - serviceAccountRef
+                - serviceSlug
+              type: object
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.19.0
+  labels:
+    external-secrets.io/component: controller
   name: clustergenerators.generators.external-secrets.io
 spec:
   group: generators.external-secrets.io
@@ -22714,6 +22812,50 @@ spec:
                         - auth
                         - registry
                       type: object
+                    cloudsmithAccessTokenSpec:
+                      properties:
+                        apiUrl:
+                          description: APIURL configures the Cloudsmith API URL. Defaults to https://api.cloudsmith.io.
+                          type: string
+                        orgSlug:
+                          description: OrgSlug is the organization slug in Cloudsmith
+                          type: string
+                        serviceAccountRef:
+                          description: Name of the service account you are federating with
+                          properties:
+                            audiences:
+                              description: |-
+                                Audience specifies the `aud` claim for the service account token
+                                If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                then this audiences will be appended to the list
+                              items:
+                                type: string
+                              type: array
+                            name:
+                              description: The name of the ServiceAccount resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                Namespace of the resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          required:
+                            - name
+                          type: object
+                        serviceSlug:
+                          description: ServiceSlug is the service slug in Cloudsmith for OIDC authentication
+                          type: string
+                      required:
+                        - orgSlug
+                        - serviceAccountRef
+                        - serviceSlug
+                      type: object
                     ecrAuthorizationTokenSpec:
                       properties:
                         auth:
@@ -24510,6 +24652,7 @@ spec:
                   description: Kind the kind of this generator.
                   enum:
                     - ACRAccessToken
+                    - CloudsmithAccessToken
                     - ECRAuthorizationToken
                     - Fake
                     - GCRAccessToken

+ 110 - 0
docs/api/generator/cloudsmith.md

@@ -0,0 +1,110 @@
+`CloudsmithAccessToken` creates a short-lived Cloudsmith access token that can be used to authenticate against Cloudsmith's container registry for pushing or pulling container images. This generator uses OIDC token exchange to authenticate with Cloudsmith using a Kubernetes service account token and generates Docker registry credentials in dockerconfigjson format.
+
+## Output Keys and Values
+
+| Key        | Description                                                                    |
+| ---------- | ------------------------------------------------------------------------------ |
+| auth       | Base64 encoded authentication string for Docker registry access.              |
+| expiry     | Time when token expires in UNIX time (seconds since January 1, 1970 UTC).    |
+
+## Authentication
+
+To use the Cloudsmith generator, you must configure OIDC authentication between your Kubernetes cluster and Cloudsmith. Your cluster must have a publicly available [OIDC service account issuer](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-issuer-discovery) endpoint for Cloudsmith to validate tokens against.
+
+### Prerequisites
+
+1. **Cloudsmith OIDC Service**: Configure an OIDC service in your Cloudsmith organization that trusts your Kubernetes cluster's OIDC issuer.
+2. **Service Account**: Create a Kubernetes service account that will be used for token exchange.
+3. **Proper Audiences**: The service account token must include the appropriate audience for Cloudsmith (typically `https://api.cloudsmith.io`).
+
+### Service Account Configuration
+
+You can determine the issuer and subject fields by creating and decoding a service account token for the service account you wish to use (this is the service account you will specify in `spec.serviceAccountRef`). For example, if using the `default` service account in the `default` namespace:
+
+Obtain issuer:
+
+```bash
+kubectl create token default -n default | cut -d '.' -f 2 | sed 's/[^=]$/&==/' | base64 -d | jq -r '.iss'
+```
+
+Use these values when configuring the OIDC service in your Cloudsmith Workspace settings.
+
+## Configuration Parameters
+
+| Parameter           | Description                                                              | Required |
+| ------------------- | ------------------------------------------------------------------------ | -------- |
+| `apiHost`          | The Cloudsmith API host. Defaults to `api.cloudsmith.io`.               | No       |
+| `orgSlug`          | The organization slug in Cloudsmith.                                    | Yes      |
+| `serviceSlug`      | The OIDC service slug configured in Cloudsmith.                         | Yes      |
+| `serviceAccountRef` | Reference to the Kubernetes service account for OIDC token exchange.    | Yes      |
+
+## Example Manifest
+
+```yaml
+{% include 'generator-cloudsmith.yaml' %}
+```
+
+Example `ExternalSecret` that references the Cloudsmith generator:
+
+```yaml
+{% include 'generator-cloudsmith-example.yaml' %}
+```
+
+## Using the Generated Docker Registry Secret
+
+Once the dockerconfigjson secret is created, you can use it to authenticate with Cloudsmith's container registry in several ways:
+
+### In Pod Specifications
+
+Reference the secret in your pod's `imagePullSecrets`:
+
+```yaml
+apiVersion: v1
+kind: Pod
+metadata:
+  name: my-app
+spec:
+  imagePullSecrets:
+    - name: cloudsmith-credentials
+  containers:
+    - name: app
+      image: docker.cloudsmith.io/my-org/my-repo/my-image:latest
+```
+
+### In ServiceAccount
+
+Add the secret to a ServiceAccount for automatic usage:
+
+```yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: my-service-account
+imagePullSecrets:
+  - name: cloudsmith-credentials
+```
+
+### For Docker CLI Authentication
+
+Extract the dockerconfigjson and use it with Docker:
+
+```bash
+kubectl get secret cloudsmith-credentials -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d > ~/.docker/config.json
+docker pull docker.cloudsmith.io/my-org/my-repo/my-image:latest
+```
+
+## Usage Notes
+
+- **Container Registry Access**: The generated dockerconfigjson secret is specifically designed for authenticating with Cloudsmith's container registry to push or pull Docker images.
+- **Token Lifetime**: Cloudsmith access tokens have a limited lifetime. The `expiry` field in the generated secret indicates when the token will expire.
+- **Refresh Interval**: Set an appropriate `refreshInterval` in your `ExternalSecret` to ensure tokens are refreshed before expiration.
+- **Permissions**: The generated token will have the same permissions as the OIDC service configured in Cloudsmith for container registry access.
+
+## Troubleshooting
+
+- **Token Exchange Fails**: Verify that your OIDC service in Cloudsmith is correctly configured with your cluster's issuer.
+- **Invalid Audience**: Ensure the service account token includes the correct audience for Cloudsmith API.
+- **Network Issues**: Check that your cluster can reach the Cloudsmith API endpoint specified in `apiHost`.
+- **Container Image Pull Fails**: Verify that the generated dockerconfigjson secret is properly referenced in your pod's `imagePullSecrets` and that the image exists in your Cloudsmith container registry.
+- **Registry Domain Issues**: Ensure you're using the correct registry domain format (e.g., `docker.cloudsmith.io/org/repo/image:tag`) in your image references.
+- **Permissions**: Confirm that your OIDC service in Cloudsmith has the necessary permissions to pull/push container images from the specific repositories.

+ 29 - 0
docs/snippets/generator-cloudsmith-example.yaml

@@ -0,0 +1,29 @@
+{% raw %}
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: cloudsmith-credentials
+  namespace: default
+spec:
+  dataFrom:
+    - sourceRef:
+        generatorRef:
+          apiVersion: generators.external-secrets.io/v1alpha1
+          kind: CloudsmithAccessToken
+          name: my-cloudsmith-token
+  refreshInterval: 50m # Refresh before token expires
+  target:
+    name: cloudsmith-credentials
+    template:
+      type: kubernetes.io/dockerconfigjson
+      data:
+        .dockerconfigjson: |
+          {
+            "auths": {
+              "docker.cloudsmith.io": {
+                "auth": "{{ .auth }}"
+              }
+            }
+          }
+
+{% endraw %}

+ 14 - 0
docs/snippets/generator-cloudsmith.yaml

@@ -0,0 +1,14 @@
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: CloudsmithAccessToken
+metadata:
+  name: my-cloudsmith-token
+  namespace: default
+spec:
+  apiHost: "api.cloudsmith.io"  # Optional, defaults to api.cloudsmith.io
+  orgSlug: "my-organization"
+  serviceSlug: "my-oidc-service"
+  serviceAccountRef:
+    name: "default"
+    namespace: "default"
+    audiences:
+      - "https://api.cloudsmith.io"

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -72,6 +72,7 @@ nav:
           - Azure Container Registry: api/generator/acr.md
           - AWS Elastic Container Registry: api/generator/ecr.md
           - AWS STS Session Token: api/generator/sts.md
+          - Cloudsmith: api/generator/cloudsmith.md
           - Cluster Generator: api/generator/cluster.md
           - Google Container Registry: api/generator/gcr.md
           - Quay: api/generator/quay.md

+ 195 - 0
pkg/generator/cloudsmith/cloudsmith.go

@@ -0,0 +1,195 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cloudsmith
+
+import (
+	"bytes"
+	"context"
+	b64 "encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/go-logr/logr"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/yaml"
+
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+type Generator struct {
+	httpClient *http.Client
+}
+
+type OIDCRequest struct {
+	OIDCToken   string `json:"oidc_token"`
+	ServiceSlug string `json:"service_slug"`
+}
+
+type OIDCResponse struct {
+	Token string `json:"token"`
+}
+
+const (
+	defaultCloudsmithAPIURL = "https://api.cloudsmith.io"
+
+	errNoSpec            = "no config spec provided"
+	errParseSpec         = "unable to parse spec: %w"
+	errExchangeToken     = "unable to exchange OIDC token: %w"
+	errMarshalRequest    = "failed to marshal request payload: %w"
+	errCreateRequest     = "failed to create HTTP request: %w"
+	errUnexpectedStatus  = "request failed due to unexpected status: %s"
+	errReadResponse      = "failed to read response body: %w"
+	errUnmarshalResponse = "failed to unmarshal response: %w"
+	errTokenNotFound     = "token not found in response"
+
+	httpClientTimeout = 30 * time.Second
+)
+
+func (g *Generator) Generate(ctx context.Context, cloudsmithSpec *apiextensions.JSON, kubeClient client.Client, targetNamespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	return g.generate(
+		ctx,
+		cloudsmithSpec,
+		kubeClient,
+		targetNamespace,
+	)
+}
+
+func (g *Generator) Cleanup(_ context.Context, cloudsmithSpec *apiextensions.JSON, providerState genv1alpha1.GeneratorProviderState, _ client.Client, _ string) error {
+	return nil
+}
+
+func (g *Generator) generate(
+	ctx context.Context,
+	cloudsmithSpec *apiextensions.JSON,
+	_ client.Client,
+	targetNamespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	if cloudsmithSpec == nil {
+		return nil, nil, errors.New(errNoSpec)
+	}
+	res, err := parseSpec(cloudsmithSpec.Raw)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errParseSpec, err)
+	}
+
+	// Fetch the service account token
+	oidcToken, err := utils.FetchServiceAccountToken(ctx, res.Spec.ServiceAccountRef, targetNamespace)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to fetch service account token: %w", err)
+	}
+
+	apiURL := res.Spec.APIURL
+	if apiURL == "" {
+		apiURL = defaultCloudsmithAPIURL
+	}
+
+	accessToken, err := g.exchangeTokenWithCloudsmith(ctx, oidcToken, res.Spec.OrgSlug, res.Spec.ServiceSlug, apiURL)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errExchangeToken, err)
+	}
+
+	exp, err := utils.ExtractJWTExpiration(accessToken)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return map[string][]byte{
+		"auth":   []byte(b64.StdEncoding.EncodeToString([]byte("token:" + accessToken))),
+		"expiry": []byte(exp),
+	}, nil, nil
+}
+
+func (g *Generator) exchangeTokenWithCloudsmith(ctx context.Context, oidcToken, orgSlug, serviceSlug, apiURL string) (string, error) {
+	log := logr.FromContextOrDiscard(ctx)
+	log.V(4).Info("Starting OIDC token exchange with Cloudsmith")
+
+	requestPayload := OIDCRequest{
+		OIDCToken:   oidcToken,
+		ServiceSlug: serviceSlug,
+	}
+
+	jsonPayload, err := json.Marshal(requestPayload)
+	if err != nil {
+		return "", fmt.Errorf(errMarshalRequest, err)
+	}
+
+	url := fmt.Sprintf("%s/openid/%s/", strings.TrimSuffix(apiURL, "/"), orgSlug)
+	log.Info("Exchanging OIDC token with Cloudsmith",
+		"url", url,
+		"serviceSlug", serviceSlug,
+		"orgSlug", orgSlug)
+
+	httpClient := g.httpClient
+	if httpClient == nil {
+		httpClient = &http.Client{
+			Timeout: httpClientTimeout,
+		}
+	}
+
+	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonPayload))
+	if err != nil {
+		return "", fmt.Errorf(errCreateRequest, err)
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := httpClient.Do(req)
+	if err != nil {
+		return "", fmt.Errorf("failed to execute HTTP request: %w", err)
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	if resp.StatusCode != http.StatusCreated {
+		return "", fmt.Errorf(errUnexpectedStatus, resp.Status)
+	}
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", fmt.Errorf(errReadResponse, err)
+	}
+
+	var result OIDCResponse
+	err = json.Unmarshal(body, &result)
+	if err != nil {
+		return "", fmt.Errorf(errUnmarshalResponse, err)
+	}
+
+	if result.Token == "" {
+		return "", errors.New(errTokenNotFound)
+	}
+
+	log.V(4).Info("Successfully exchanged OIDC token for Cloudsmith access token")
+	return result.Token, nil
+}
+
+func parseSpec(specData []byte) (*genv1alpha1.CloudsmithAccessToken, error) {
+	var spec genv1alpha1.CloudsmithAccessToken
+	err := yaml.Unmarshal(specData, &spec)
+	return &spec, err
+}
+
+func init() {
+	genv1alpha1.Register(genv1alpha1.CloudsmithAccessTokenKind, &Generator{})
+}

+ 165 - 0
pkg/generator/cloudsmith/cloudsmith_test.go

@@ -0,0 +1,165 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package cloudsmith
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"sigs.k8s.io/yaml"
+
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const mockJWTToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNzAwMDAwMDAwfQ.signature"
+
+func TestCloudsmithGenerator_Generate(t *testing.T) {
+	// Test server that mimics Cloudsmith OIDC endpoint
+	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != "POST" {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+
+		var req OIDCRequest
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			http.Error(w, "Bad request", http.StatusBadRequest)
+			return
+		}
+
+		// Mock response with a JWT-like token (simplified for testing)
+		mockToken := mockJWTToken
+		response := OIDCResponse{
+			Token: mockToken,
+		}
+
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+		json.NewEncoder(w).Encode(response)
+	}))
+	defer server.Close()
+
+	// Create test spec
+	spec := &genv1alpha1.CloudsmithAccessToken{
+		Spec: genv1alpha1.CloudsmithAccessTokenSpec{
+			APIURL:      server.URL,
+			OrgSlug:     "test-org",
+			ServiceSlug: "test-service",
+			ServiceAccountRef: esmeta.ServiceAccountSelector{
+				Name:      "test-sa",
+				Audiences: []string{"https://api.cloudsmith.io"},
+			},
+		},
+	}
+
+	specBytes, err := yaml.Marshal(spec)
+	if err != nil {
+		t.Fatalf("Failed to marshal spec: %v", err)
+	}
+
+	_ = &apiextensions.JSON{
+		Raw: specBytes,
+	}
+
+	// Create generator with custom HTTP client that accepts self-signed certificates
+	httpClient := &http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				InsecureSkipVerify: true,
+			},
+		},
+	}
+	generator := &Generator{
+		httpClient: httpClient,
+	}
+
+	// Note: This test will fail because we don't have a real service account token
+	// In a real test environment, you would mock the fetchServiceAccountToken function
+	// or set up proper test fixtures
+	t.Run("parseSpec", func(t *testing.T) {
+		parsed, err := parseSpec(specBytes)
+		if err != nil {
+			t.Fatalf("Failed to parse spec: %v", err)
+		}
+
+		if parsed.Spec.OrgSlug != "test-org" {
+			t.Errorf("Expected OrgSlug to be 'test-org', got %s", parsed.Spec.OrgSlug)
+		}
+		if parsed.Spec.ServiceSlug != "test-service" {
+			t.Errorf("Expected ServiceSlug to be 'test-service', got %s", parsed.Spec.ServiceSlug)
+		}
+	})
+
+	t.Run("exchangeTokenWithCloudsmith", func(t *testing.T) {
+		ctx := context.Background()
+		oidcToken := "mock-oidc-token"
+
+		token, err := generator.exchangeTokenWithCloudsmith(
+			ctx,
+			oidcToken,
+			"test-org",
+			"test-service",
+			server.URL,
+		)
+
+		if err != nil {
+			t.Fatalf("Failed to exchange token: %v", err)
+		}
+
+		if token == "" {
+			t.Error("Expected non-empty token")
+		}
+	})
+
+	t.Run("ParseJWTClaims", func(t *testing.T) {
+		// Mock JWT token with known payload
+		mockToken := mockJWTToken
+
+		claims, err := utils.ParseJWTClaims(mockToken)
+		if err != nil {
+			t.Fatalf("Failed to get claims: %v", err)
+		}
+
+		if claims["sub"] != "1234567890" {
+			t.Errorf("Expected sub claim to be '1234567890', got %v", claims["sub"])
+		}
+		if claims["name"] != "John Doe" {
+			t.Errorf("Expected name claim to be 'John Doe', got %v", claims["name"])
+		}
+	})
+
+	t.Run("ExtractJWTExpiration", func(t *testing.T) {
+		// Mock JWT token with known exp claim
+		mockToken := mockJWTToken
+
+		exp, err := utils.ExtractJWTExpiration(mockToken)
+		if err != nil {
+			t.Fatalf("Failed to get token expiration: %v", err)
+		}
+
+		if exp != "1700000000" {
+			t.Errorf("Expected expiration to be '1700000000', got %s", exp)
+		}
+	})
+}

+ 3 - 63
pkg/generator/quay/quay.go

@@ -24,20 +24,15 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"strconv"
 	"strings"
 	"time"
 
-	authv1 "k8s.io/api/authentication/v1"
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 	"sigs.k8s.io/yaml"
 
 	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
-	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
 )
 
 type Generator struct {
@@ -81,7 +76,7 @@ func (g *Generator) generate(
 	}
 
 	// Fetch the service account token
-	token, err := fetchServiceAccountToken(ctx, res.Spec.ServiceAccountRef, namespace)
+	token, err := utils.FetchServiceAccountToken(ctx, res.Spec.ServiceAccountRef, namespace)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to fetch service account token: %w", err)
 	}
@@ -95,7 +90,7 @@ func (g *Generator) generate(
 	if err != nil {
 		return nil, nil, err
 	}
-	exp, err := tokenExpiration(accessToken)
+	exp, err := utils.ExtractJWTExpiration(accessToken)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -106,39 +101,6 @@ func (g *Generator) generate(
 	}, nil, nil
 }
 
-func getClaims(tokenString string) (map[string]interface{}, error) {
-	// Split the token into its three parts
-	parts := strings.Split(tokenString, ".")
-	if len(parts) != 3 {
-		return nil, fmt.Errorf("invalid token format")
-	}
-
-	// Decode the payload (the second part of the token)
-	payload, err := b64.RawURLEncoding.DecodeString(parts[1])
-	if err != nil {
-		return nil, fmt.Errorf("error decoding payload: %w", err)
-	}
-
-	var claims map[string]interface{}
-	if err := json.Unmarshal(payload, &claims); err != nil {
-		return nil, fmt.Errorf("error un-marshaling claims: %w", err)
-	}
-	return claims, nil
-}
-
-func tokenExpiration(tokenString string) (string, error) {
-	claims, err := getClaims(tokenString)
-	if err != nil {
-		return "", fmt.Errorf("error getting claims: %w", err)
-	}
-	exp, ok := claims["exp"].(float64)
-	if ok {
-		return strconv.FormatFloat(exp, 'f', -1, 64), nil
-	}
-
-	return "", fmt.Errorf("exp claim not found or wrong type")
-}
-
 // https://docs.projectquay.io/manage_quay.html#exchanging-oauth2-robot-account-token
 func getQuayRobotToken(ctx context.Context, fedToken, robotAccount, url string, hc *http.Client) (string, error) {
 	if hc == nil {
@@ -186,28 +148,6 @@ func getQuayRobotToken(ctx context.Context, fedToken, robotAccount, url string,
 	return tokenString, nil
 }
 
-func fetchServiceAccountToken(ctx context.Context, saRef esmeta.ServiceAccountSelector, namespace string) (string, error) {
-	cfg, err := ctrlcfg.GetConfig()
-	if err != nil {
-		return "", err
-	}
-	kubeClient, err := kubernetes.NewForConfig(cfg)
-	if err != nil {
-		return "", fmt.Errorf("failed to create kubernetes client: %w", err)
-	}
-
-	tokenRequest := &authv1.TokenRequest{
-		Spec: authv1.TokenRequestSpec{
-			Audiences: saRef.Audiences,
-		},
-	}
-	tokenResponse, err := kubeClient.CoreV1().ServiceAccounts(namespace).CreateToken(ctx, saRef.Name, tokenRequest, metav1.CreateOptions{})
-	if err != nil {
-		return "", fmt.Errorf("failed to create token: %w", err)
-	}
-	return tokenResponse.Status.Token, nil
-}
-
 func parseSpec(data []byte) (*genv1alpha1.QuayAccessToken, error) {
 	var spec genv1alpha1.QuayAccessToken
 	err := yaml.Unmarshal(data, &spec)

+ 1 - 0
pkg/generator/register/register.go

@@ -20,6 +20,7 @@ package register
 
 import (
 	_ "github.com/external-secrets/external-secrets/pkg/generator/acr"
+	_ "github.com/external-secrets/external-secrets/pkg/generator/cloudsmith"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/ecr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/fake"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/gcr"

+ 11 - 0
pkg/utils/resolvers/generator.go

@@ -152,6 +152,17 @@ func clusterGeneratorToVirtual(gen *genv1alpha1.ClusterGenerator) (client.Object
 			},
 			Spec: *gen.Spec.Generator.ACRAccessTokenSpec,
 		}, nil
+	case genv1alpha1.GeneratorKindCloudsmithAccessToken:
+		if gen.Spec.Generator.CloudsmithAccessTokenSpec == nil {
+			return nil, fmt.Errorf("when kind is %s, CloudsmithAccessTokenSpec must be set", gen.Spec.Kind)
+		}
+		return &genv1alpha1.CloudsmithAccessToken{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: genv1alpha1.SchemeGroupVersion.String(),
+				Kind:       genv1alpha1.CloudsmithAccessTokenKind,
+			},
+			Spec: *gen.Spec.Generator.CloudsmithAccessTokenSpec,
+		}, nil
 	case genv1alpha1.GeneratorKindECRAuthorizationToken:
 		if gen.Spec.Generator.ECRAuthorizationTokenSpec == nil {
 			return nil, fmt.Errorf("when kind is %s, ECRAuthorizationTokenSpec must be set", gen.Spec.Kind)

+ 61 - 0
pkg/utils/utils.go

@@ -40,11 +40,14 @@ import (
 	"unicode"
 
 	"github.com/go-logr/logr"
+	authv1 "k8s.io/api/authentication/v1"
 	corev1 "k8s.io/api/core/v1"
 	discoveryv1 "k8s.io/api/discovery/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 
@@ -859,3 +862,61 @@ func CheckEndpointSlicesReady(ctx context.Context, c client.Client, svcName, svc
 	}
 	return nil
 }
+
+// ParseJWTClaims extracts claims from a JWT token string.
+func ParseJWTClaims(tokenString string) (map[string]interface{}, error) {
+	// Split the token into its three parts
+	parts := strings.Split(tokenString, ".")
+	if len(parts) != 3 {
+		return nil, fmt.Errorf("invalid token format")
+	}
+
+	// Decode the payload (the second part of the token)
+	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
+	if err != nil {
+		return nil, fmt.Errorf("error decoding payload: %w", err)
+	}
+
+	var claims map[string]interface{}
+	if err := json.Unmarshal(payload, &claims); err != nil {
+		return nil, fmt.Errorf("error un-marshaling claims: %w", err)
+	}
+	return claims, nil
+}
+
+// ExtractJWTExpiration extracts the expiration time from a JWT token string.
+func ExtractJWTExpiration(tokenString string) (string, error) {
+	claims, err := ParseJWTClaims(tokenString)
+	if err != nil {
+		return "", fmt.Errorf("error getting claims: %w", err)
+	}
+	exp, ok := claims["exp"].(float64)
+	if ok {
+		return strconv.FormatFloat(exp, 'f', -1, 64), nil
+	}
+
+	return "", fmt.Errorf("exp claim not found or wrong type")
+}
+
+// FetchServiceAccountToken creates a service account token for the specified service account.
+func FetchServiceAccountToken(ctx context.Context, saRef esmeta.ServiceAccountSelector, namespace string) (string, error) {
+	cfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return "", err
+	}
+	kubeClient, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return "", fmt.Errorf("failed to create kubernetes client: %w", err)
+	}
+
+	tokenRequest := &authv1.TokenRequest{
+		Spec: authv1.TokenRequestSpec{
+			Audiences: saRef.Audiences,
+		},
+	}
+	tokenResponse, err := kubeClient.CoreV1().ServiceAccounts(namespace).CreateToken(ctx, saRef.Name, tokenRequest, metav1.CreateOptions{})
+	if err != nil {
+		return "", fmt.Errorf("failed to create token: %w", err)
+	}
+	return tokenResponse.Status.Token, nil
+}

+ 33 - 0
pkg/utils/utils_test.go

@@ -1415,3 +1415,36 @@ func TestValidateReferentServiceAccountSelector(t *testing.T) {
 		})
 	}
 }
+
+const mockJWTToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNzAwMDAwMDAwfQ.signature"
+
+func TestParseJWTClaims(t *testing.T) {
+	// Mock JWT token with known payload
+	mockToken := mockJWTToken
+
+	claims, err := ParseJWTClaims(mockToken)
+	if err != nil {
+		t.Fatalf("Failed to get claims: %v", err)
+	}
+
+	if claims["sub"] != "1234567890" {
+		t.Errorf("Expected sub claim to be '1234567890', got %v", claims["sub"])
+	}
+	if claims["name"] != "John Doe" {
+		t.Errorf("Expected name claim to be 'John Doe', got %v", claims["name"])
+	}
+}
+
+func TestExtractJWTExpiration(t *testing.T) {
+	// Mock JWT token with known exp claim
+	mockToken := mockJWTToken
+
+	exp, err := ExtractJWTExpiration(mockToken)
+	if err != nil {
+		t.Fatalf("Failed to get token expiration: %v", err)
+	}
+
+	if exp != "1700000000" {
+		t.Errorf("Expected expiration to be '1700000000', got %s", exp)
+	}
+}