Browse Source

feat: add MFA token generator Generator (#4790)

* feat: add MFA token generator Generator

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* updated the annotation on allowed values

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* also updated rbac

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* import the generator

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Gergely Brautigam 10 months ago
parent
commit
e5fac2433f

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

@@ -440,7 +440,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;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana
+	// +kubebuilder:validation:Enum=ACRAccessToken;ClusterGenerator;ECRAuthorizationToken;Fake;GCRAccessToken;GithubAccessToken;QuayAccessToken;Password;STSSessionToken;UUID;VaultDynamicSecret;Webhook;Grafana;MFA
 	Kind string `json:"kind"`
 
 	// Specify the name of the generator resource

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

@@ -49,6 +49,7 @@ var (
 	QuayAccessTokenKind       = reflect.TypeOf(QuayAccessToken{}).Name()
 	UUIDKind                  = reflect.TypeOf(UUID{}).Name()
 	GrafanaKind               = reflect.TypeOf(Grafana{}).Name()
+	MFAKind                   = reflect.TypeOf(MFA{}).Name()
 	ClusterGeneratorKind      = reflect.TypeOf(ClusterGenerator{}).Name()
 )
 
@@ -83,4 +84,5 @@ func init() {
 	SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
 	SchemeBuilder.Register(&Webhook{}, &WebhookList{})
 	SchemeBuilder.Register(&Grafana{}, &GrafanaList{})
+	SchemeBuilder.Register(&MFA{}, &MFAList{})
 }

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

@@ -43,6 +43,7 @@ const (
 	GeneratorKindVaultDynamicSecret    GeneratorKind = "VaultDynamicSecret"
 	GeneratorKindWebhook               GeneratorKind = "Webhook"
 	GeneratorKindGrafana               GeneratorKind = "Grafana"
+	GeneratorKindMFA                   GeneratorKind = "MFA"
 )
 
 // +kubebuilder:validation:MaxProperties=1
@@ -60,6 +61,7 @@ type GeneratorSpec struct {
 	VaultDynamicSecretSpec    *VaultDynamicSecretSpec    `json:"vaultDynamicSecretSpec,omitempty"`
 	WebhookSpec               *WebhookSpec               `json:"webhookSpec,omitempty"`
 	GrafanaSpec               *GrafanaSpec               `json:"grafanaSpec,omitempty"`
+	MFASpec                   *MFASpec                   `json:"mfaSpec,omitempty"`
 }
 
 // ClusterGenerator represents a cluster-wide generator which can be referenced as part of `generatorRef` fields.

+ 56 - 0
apis/generators/v1alpha1/types_mfa.go

@@ -0,0 +1,56 @@
+/*
+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 (
+	smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// MFASpec controls the behavior of the mfa generator.
+type MFASpec struct {
+	// Secret is a secret selector to a secret containing the seed secret to generate the TOTP value from.
+	Secret smmeta.SecretKeySelector `json:"secret"`
+	// Length defines the token length. Defaults to 6 characters.
+	Length int `json:"length,omitempty"`
+	// TimePeriod defines how long the token can be active. Defaults to 30 seconds.
+	TimePeriod int `json:"timePeriod,omitempty"`
+	// Algorithm to use for encoding. Defaults to SHA1 as per the RFC.
+	Algorithm string `json:"algorithm,omitempty"`
+	// When defines a time parameter that can be used to pin the origin time of the generated token.
+	When *metav1.Time `json:"when,omitempty"`
+}
+
+// MFA generates a new TOTP token that is compliant with RFC 6238.
+// +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 MFA struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec MFASpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// MFAList contains a list of MFA resources.
+type MFAList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []MFA `json:"items"`
+}

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

@@ -723,6 +723,11 @@ func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
 		*out = new(GrafanaSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.MFASpec != nil {
+		in, out := &in.MFASpec, &out.MFASpec
+		*out = new(MFASpec)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorSpec.
@@ -1157,6 +1162,84 @@ func (in *GrafanaStateServiceAccount) DeepCopy() *GrafanaStateServiceAccount {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MFA) DeepCopyInto(out *MFA) {
+	*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 MFA.
+func (in *MFA) DeepCopy() *MFA {
+	if in == nil {
+		return nil
+	}
+	out := new(MFA)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *MFA) 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 *MFAList) DeepCopyInto(out *MFAList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]MFA, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MFAList.
+func (in *MFAList) DeepCopy() *MFAList {
+	if in == nil {
+		return nil
+	}
+	out := new(MFAList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *MFAList) 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 *MFASpec) DeepCopyInto(out *MFASpec) {
+	*out = *in
+	in.Secret.DeepCopyInto(&out.Secret)
+	if in.When != nil {
+		in, out := &in.When, &out.When
+		*out = (*in).DeepCopy()
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MFASpec.
+func (in *MFASpec) DeepCopy() *MFASpec {
+	if in == nil {
+		return nil
+	}
+	out := new(MFASpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NTLMProtocol) DeepCopyInto(out *NTLMProtocol) {
 	*out = *in
 	in.UserName.DeepCopyInto(&out.UserName)

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

@@ -172,6 +172,7 @@ spec:
                                   - VaultDynamicSecret
                                   - Webhook
                                   - Grafana
+                                  - MFA
                                   type: string
                                 name:
                                   description: Specify the name of the generator resource
@@ -369,6 +370,7 @@ spec:
                                   - VaultDynamicSecret
                                   - Webhook
                                   - Grafana
+                                  - MFA
                                   type: string
                                 name:
                                   description: Specify the name of the generator resource

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

@@ -274,6 +274,7 @@ spec:
                             - VaultDynamicSecret
                             - Webhook
                             - Grafana
+                            - MFA
                             type: string
                           name:
                             description: Specify the name of the generator resource

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

@@ -152,6 +152,7 @@ spec:
                               - VaultDynamicSecret
                               - Webhook
                               - Grafana
+                              - MFA
                               type: string
                             name:
                               description: Specify the name of the generator resource
@@ -348,6 +349,7 @@ spec:
                               - VaultDynamicSecret
                               - Webhook
                               - Grafana
+                              - MFA
                               type: string
                             name:
                               description: Specify the name of the generator resource

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

@@ -198,6 +198,7 @@ spec:
                         - VaultDynamicSecret
                         - Webhook
                         - Grafana
+                        - MFA
                         type: string
                       name:
                         description: Specify the name of the generator resource

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

@@ -619,6 +619,57 @@ spec:
                     - serviceAccount
                     - url
                     type: object
+                  mfaSpec:
+                    description: MFASpec controls the behavior of the mfa generator.
+                    properties:
+                      algorithm:
+                        description: Algorithm to use for encoding. Defaults to SHA1
+                          as per the RFC.
+                        type: string
+                      length:
+                        description: Length defines the token length. Defaults to
+                          6 characters.
+                        type: integer
+                      secret:
+                        description: Secret is a secret selector to a secret containing
+                          the seed secret to generate the TOTP value from.
+                        properties:
+                          key:
+                            description: |-
+                              A key in the referenced Secret.
+                              Some instances of this field may be defaulted, in others it may be required.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the Secret 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: |-
+                              The namespace of the Secret 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
+                        type: object
+                      timePeriod:
+                        description: TimePeriod defines how long the token can be
+                          active. Defaults to 30 seconds.
+                        type: integer
+                      when:
+                        description: When defines a time parameter that can be used
+                          to pin the origin time of the generated token.
+                        format: date-time
+                        type: string
+                    required:
+                    - secret
+                    type: object
                   passwordSpec:
                     description: PasswordSpec controls the behavior of the password
                       generator.

+ 96 - 0
config/crds/bases/generators.external-secrets.io_mfas.yaml

@@ -0,0 +1,96 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.18.0
+  labels:
+    external-secrets.io/component: controller
+  name: mfas.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+    - external-secrets
+    - external-secrets-generators
+    kind: MFA
+    listKind: MFAList
+    plural: mfas
+    singular: mfa
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: MFA generates a new TOTP token that is compliant with RFC 6238.
+        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:
+            description: MFASpec controls the behavior of the mfa generator.
+            properties:
+              algorithm:
+                description: Algorithm to use for encoding. Defaults to SHA1 as per
+                  the RFC.
+                type: string
+              length:
+                description: Length defines the token length. Defaults to 6 characters.
+                type: integer
+              secret:
+                description: Secret is a secret selector to a secret containing the
+                  seed secret to generate the TOTP value from.
+                properties:
+                  key:
+                    description: |-
+                      A key in the referenced Secret.
+                      Some instances of this field may be defaulted, in others it may be required.
+                    maxLength: 253
+                    minLength: 1
+                    pattern: ^[-._a-zA-Z0-9]+$
+                    type: string
+                  name:
+                    description: The name of the Secret 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: |-
+                      The namespace of the Secret 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
+                type: object
+              timePeriod:
+                description: TimePeriod defines how long the token can be active.
+                  Defaults to 30 seconds.
+                type: integer
+              when:
+                description: When defines a time parameter that can be used to pin
+                  the origin time of the generated token.
+                format: date-time
+                type: string
+            required:
+            - secret
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

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

@@ -16,6 +16,7 @@ resources:
   - generators.external-secrets.io_generatorstates.yaml
   - generators.external-secrets.io_githubaccesstokens.yaml
   - generators.external-secrets.io_grafanas.yaml
+  - generators.external-secrets.io_mfas.yaml
   - generators.external-secrets.io_passwords.yaml
   - generators.external-secrets.io_quayaccesstokens.yaml
   - generators.external-secrets.io_stssessiontokens.yaml

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

@@ -108,6 +108,7 @@ rules:
     - "vaultdynamicsecrets"
     - "webhooks"
     - "grafanas"
+    - "mfas"
     verbs:
     - "get"
     - "list"
@@ -227,6 +228,7 @@ rules:
     - "webhooks"
     - "grafanas"
     - "generatorstates"
+    - "mfas"
     verbs:
       - "get"
       - "watch"
@@ -285,6 +287,7 @@ rules:
     - "webhooks"
     - "grafanas"
     - "generatorstates"
+    - "mfas"
     verbs:
       - "create"
       - "delete"

+ 144 - 0
deploy/crds/bundle.yaml

@@ -162,6 +162,7 @@ spec:
                                       - VaultDynamicSecret
                                       - Webhook
                                       - Grafana
+                                      - MFA
                                     type: string
                                   name:
                                     description: Specify the name of the generator resource
@@ -350,6 +351,7 @@ spec:
                                       - VaultDynamicSecret
                                       - Webhook
                                       - Grafana
+                                      - MFA
                                     type: string
                                   name:
                                     description: Specify the name of the generator resource
@@ -1719,6 +1721,7 @@ spec:
                                 - VaultDynamicSecret
                                 - Webhook
                                 - Grafana
+                                - MFA
                               type: string
                             name:
                               description: Specify the name of the generator resource
@@ -10479,6 +10482,7 @@ spec:
                                   - VaultDynamicSecret
                                   - Webhook
                                   - Grafana
+                                  - MFA
                                 type: string
                               name:
                                 description: Specify the name of the generator resource
@@ -10667,6 +10671,7 @@ spec:
                                   - VaultDynamicSecret
                                   - Webhook
                                   - Grafana
+                                  - MFA
                                 type: string
                               name:
                                 description: Specify the name of the generator resource
@@ -11735,6 +11740,7 @@ spec:
                             - VaultDynamicSecret
                             - Webhook
                             - Grafana
+                            - MFA
                           type: string
                         name:
                           description: Specify the name of the generator resource
@@ -21173,6 +21179,51 @@ spec:
                         - serviceAccount
                         - url
                       type: object
+                    mfaSpec:
+                      description: MFASpec controls the behavior of the mfa generator.
+                      properties:
+                        algorithm:
+                          description: Algorithm to use for encoding. Defaults to SHA1 as per the RFC.
+                          type: string
+                        length:
+                          description: Length defines the token length. Defaults to 6 characters.
+                          type: integer
+                        secret:
+                          description: Secret is a secret selector to a secret containing the seed secret to generate the TOTP value from.
+                          properties:
+                            key:
+                              description: |-
+                                A key in the referenced Secret.
+                                Some instances of this field may be defaulted, in others it may be required.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the Secret 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: |-
+                                The namespace of the Secret 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
+                          type: object
+                        timePeriod:
+                          description: TimePeriod defines how long the token can be active. Defaults to 30 seconds.
+                          type: integer
+                        when:
+                          description: When defines a time parameter that can be used to pin the origin time of the generated token.
+                          format: date-time
+                          type: string
+                      required:
+                        - secret
+                      type: object
                     passwordSpec:
                       description: PasswordSpec controls the behavior of the password generator.
                       properties:
@@ -23162,6 +23213,99 @@ metadata:
     controller-gen.kubebuilder.io/version: v0.18.0
   labels:
     external-secrets.io/component: controller
+  name: mfas.generators.external-secrets.io
+spec:
+  group: generators.external-secrets.io
+  names:
+    categories:
+      - external-secrets
+      - external-secrets-generators
+    kind: MFA
+    listKind: MFAList
+    plural: mfas
+    singular: mfa
+  scope: Namespaced
+  versions:
+    - name: v1alpha1
+      schema:
+        openAPIV3Schema:
+          description: MFA generates a new TOTP token that is compliant with RFC 6238.
+          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:
+              description: MFASpec controls the behavior of the mfa generator.
+              properties:
+                algorithm:
+                  description: Algorithm to use for encoding. Defaults to SHA1 as per the RFC.
+                  type: string
+                length:
+                  description: Length defines the token length. Defaults to 6 characters.
+                  type: integer
+                secret:
+                  description: Secret is a secret selector to a secret containing the seed secret to generate the TOTP value from.
+                  properties:
+                    key:
+                      description: |-
+                        A key in the referenced Secret.
+                        Some instances of this field may be defaulted, in others it may be required.
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[-._a-zA-Z0-9]+$
+                      type: string
+                    name:
+                      description: The name of the Secret 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: |-
+                        The namespace of the Secret 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
+                  type: object
+                timePeriod:
+                  description: TimePeriod defines how long the token can be active. Defaults to 30 seconds.
+                  type: integer
+                when:
+                  description: When defines a time parameter that can be used to pin the origin time of the generated token.
+                  format: date-time
+                  type: string
+              required:
+                - secret
+              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.18.0
+  labels:
+    external-secrets.io/component: controller
   name: passwords.generators.external-secrets.io
 spec:
   group: generators.external-secrets.io

+ 41 - 0
docs/api/generator/mfa.md

@@ -0,0 +1,41 @@
+# MFA Generator
+
+This generator can create [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226) compliant TOTP tokens given a seed
+secret. The seed secret is usually provided through a QR code. However, the provider will always also provide a text
+based format of that QR code. That's the secret that this generator will use to create tokens.
+
+## Output Keys and Values
+
+| Key      | Description                                      |
+|----------|--------------------------------------------------|
+| token    | the generated N letter token                     |
+| timeLeft | the time left until the token expires in seconds |
+
+## Parameters
+
+The following configuration options are available when generating a token:
+
+| Key        | Default  | Description                                                                                                    |
+|------------|----------|----------------------------------------------------------------------------------------------------------------|
+| length     | 6        | Digit length of the generated code. Some providers allow larger tokens.                                        |
+| timePeriod | 30       | Number of seconds the code can be valid. This is provider specific, usually it's 30 seconds                    |
+| secret     | empty    | This is a secret ref pointing to the seed secret                                                               |
+| algorithm  | sha1     | Algorithm for encoding. The RFC defines SHA1, though a provider will set it to SHA256 or SHA512 sometimes      |
+| when       | time.Now | This allows for pinning the creation date of the token makes for reproducible tokens. Mostly used for testing. |
+
+## Example Manifest
+
+```yaml
+{% include 'generator-mfa.yaml' %}
+```
+
+This will generate an output like this:
+
+```
+token: 123456
+timeLeft: 25
+```
+
+!!! warning "Usage of the token might fail on first try if it JUST expired"
+It is possible that from requesting the token to actually using it, the token might be already out of date if timeLeft was
+very low to begin with. Therefor, the code that uses this token should allow for retries with new tokens.

+ 9 - 0
docs/snippets/generator-mfa.yaml

@@ -0,0 +1,9 @@
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: MFA
+metadata:
+  name: my-mfa-generator
+spec:
+  length: 42
+  secret:
+    key: token
+    name: secret

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

@@ -81,6 +81,7 @@ nav:
           - Webhook: api/generator/webhook.md
           - Github: api/generator/github.md
           - UUID: api/generator/uuid.md
+          - MFA: api/generator/mfa.md
       - Reference Docs:
           - API specification: api/spec.md
           - Controller Options: api/controller-options.md

+ 92 - 0
pkg/generator/mfa/mfa.go

@@ -0,0 +1,92 @@
+/*
+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 mfa
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	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"
+)
+
+type Generator struct{}
+
+const (
+	errNoSpec    = "no config spec provided"
+	errParseSpec = "unable to parse spec: %w"
+)
+
+func (g *Generator) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, c client.Client, namespace string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
+	if jsonSpec == nil {
+		return nil, nil, errors.New(errNoSpec)
+	}
+	res, err := parseSpec(jsonSpec.Raw)
+	if err != nil {
+		return nil, nil, fmt.Errorf(errParseSpec, err)
+	}
+
+	var opts []GeneratorOptionsFunc
+	if res.Spec.Length > 0 {
+		opts = append(opts, WithLength(res.Spec.Length))
+	}
+	if res.Spec.TimePeriod > 0 {
+		opts = append(opts, WithTimePeriod(int64(res.Spec.TimePeriod)))
+	}
+	if res.Spec.When != nil {
+		opts = append(opts, WithWhen(res.Spec.When.Time))
+	}
+
+	secret := &corev1.Secret{}
+	if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: res.Spec.Secret.Name}, secret); err != nil {
+		return nil, nil, fmt.Errorf("failed to find secret for token key: %w", err)
+	}
+
+	seed, ok := secret.Data[res.Spec.Secret.Key]
+	if !ok {
+		return nil, nil, fmt.Errorf("secret key %s does not exist in secret data map", res.Spec.Secret.Key)
+	}
+
+	opts = append(opts, WithToken(string(seed)))
+
+	token, timeLeft, err := generateCode(opts...)
+	if err != nil {
+		return nil, nil, fmt.Errorf("failed to generate code for token key: %w", err)
+	}
+
+	return map[string][]byte{
+		"token":    []byte(token),
+		"timeLeft": []byte(timeLeft),
+	}, nil, nil
+}
+
+func (g *Generator) Cleanup(_ context.Context, jsonSpec *apiextensions.JSON, state genv1alpha1.GeneratorProviderState, _ client.Client, _ string) error {
+	return nil
+}
+
+func parseSpec(data []byte) (*genv1alpha1.MFA, error) {
+	var spec genv1alpha1.MFA
+	err := yaml.Unmarshal(data, &spec)
+	return &spec, err
+}
+
+func init() {
+	genv1alpha1.Register(genv1alpha1.MFAKind, &Generator{})
+}

+ 92 - 0
pkg/generator/mfa/mfa_test.go

@@ -0,0 +1,92 @@
+/*
+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 mfa
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestGenerate(t *testing.T) {
+	type args struct {
+		jsonSpec *apiextensions.JSON
+		client   client.Client
+	}
+	tests := []struct {
+		name    string
+		g       *Generator
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "no json spec should result in error",
+			args: args{
+				jsonSpec: nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid json spec should result in error",
+			args: args{
+				jsonSpec: &apiextensions.JSON{
+					Raw: []byte(`no json`),
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "spec with secret should result in valid token",
+			args: args{
+				jsonSpec: &apiextensions.JSON{
+					// time is used to pin the numbers, otherwise, they would keep changing.
+					Raw: []byte(`{"spec": {"secret": {"name": "secret", "key": "secret"}, "when": "1998-05-05T05:05:05Z"}}`),
+				},
+				client: clientfake.NewClientBuilder().WithObjects(&v1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "secret",
+						Namespace: "namespace",
+					},
+					Data: map[string][]byte{
+						"secret": []byte("foo"),
+					},
+				}).Build(),
+			},
+			want: map[string][]byte{
+				"token":    []byte(`674024`),
+				"timeLeft": []byte(`25`),
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := &Generator{}
+			got, _, err := g.Generate(context.Background(), tt.args.jsonSpec, tt.args.client, "namespace")
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Generator.Generate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}

+ 164 - 0
pkg/generator/mfa/otp.go

@@ -0,0 +1,164 @@
+/*
+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 mfa
+
+import (
+	"crypto/hmac"
+	"crypto/sha1" //nolint:gosec // not used for encryption purposes
+	"crypto/sha256"
+	"crypto/sha512"
+	"encoding/base32"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"hash"
+	"math"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const (
+	// defaultLength for a token is 6 characters.
+	defaultLength = 6
+	// defaultTimePeriod for a token is 30 seconds.
+	defaultTimePeriod = 30
+	// defaultAlgorithm according to the RFC should be sha1.
+	defaultAlgorithm = "sha1"
+)
+
+// options define configurable values for a TOTP token.
+type options struct {
+	algorithm  string
+	when       time.Time
+	token      string
+	timePeriod int64
+	length     int
+}
+
+// GeneratorOptionsFunc provides a nice way of configuring the generator while allowing defaults.
+type GeneratorOptionsFunc func(*options)
+
+// WithToken can be used to set the token value.
+func WithToken(token string) GeneratorOptionsFunc {
+	return func(o *options) {
+		o.token = token
+	}
+}
+
+// WithTimePeriod sets the time-period for the generated token. Default is 30s.
+func WithTimePeriod(timePeriod int64) GeneratorOptionsFunc {
+	return func(o *options) {
+		o.timePeriod = timePeriod
+	}
+}
+
+// WithLength sets the length of the token. Defaults to 6 digits where the token can start with 0.
+func WithLength(length int) GeneratorOptionsFunc {
+	return func(o *options) {
+		o.length = length
+	}
+}
+
+// WithAlgorithm configures the algorithm. Defaults to sha1.
+func WithAlgorithm(algorithm string) GeneratorOptionsFunc {
+	return func(o *options) {
+		o.algorithm = algorithm
+	}
+}
+
+// WithWhen allows configuring the time when the token is generated from. Defaults to time.Now().
+func WithWhen(when time.Time) GeneratorOptionsFunc {
+	return func(o *options) {
+		o.when = when
+	}
+}
+
+// generateCode generates an N digit TOTP code from the secret token.
+func generateCode(opts ...GeneratorOptionsFunc) (string, string, error) {
+	defaults := &options{
+		algorithm:  defaultAlgorithm,
+		length:     defaultLength,
+		timePeriod: defaultTimePeriod,
+		when:       time.Now(),
+	}
+
+	for _, opt := range opts {
+		opt(defaults)
+	}
+
+	cleanUpToken(defaults)
+
+	if defaults.length > math.MaxInt {
+		return "", "", errors.New("length too big")
+	}
+
+	timer := uint64(math.Floor(float64(defaults.when.Unix()) / float64(defaults.timePeriod)))
+	remainingTime := defaults.timePeriod - defaults.when.Unix()%defaults.timePeriod
+
+	secretBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(defaults.token)
+	if err != nil {
+		return "", "", fmt.Errorf("failed to generate OTP code: %w", err)
+	}
+
+	buf := make([]byte, 8)
+	shaFunc, err := getAlgorithmFunction(defaults.algorithm)
+	if err != nil {
+		return "", "", err
+	}
+	mac := hmac.New(shaFunc, secretBytes)
+
+	binary.BigEndian.PutUint64(buf, timer)
+	_, _ = mac.Write(buf)
+	sum := mac.Sum(nil)
+
+	// http://tools.ietf.org/html/rfc4226#section-5.4
+	offset := sum[len(sum)-1] & 0xf
+	value := ((int(sum[offset]) & 0x7f) << 24) |
+		((int(sum[offset+1] & 0xff)) << 16) |
+		((int(sum[offset+2] & 0xff)) << 8) |
+		(int(sum[offset+3]) & 0xff)
+
+	modulo := value % int(math.Pow10(defaults.length))
+
+	format := fmt.Sprintf("%%0%dd", defaults.length)
+
+	return fmt.Sprintf(format, modulo), strconv.Itoa(int(remainingTime)), nil
+}
+
+func cleanUpToken(defaults *options) {
+	// Remove all spaces. Providers sometimes make it more readable by fragmentation.
+	defaults.token = strings.ReplaceAll(defaults.token, " ", "")
+
+	// The token is always uppercase.
+	defaults.token = strings.ToUpper(defaults.token)
+}
+
+func getAlgorithmFunction(algo string) (func() hash.Hash, error) {
+	switch algo {
+	case "sha512":
+		return sha512.New, nil
+	case "sha384":
+		return sha512.New384, nil
+	case "sha512_256":
+		return sha512.New512_256, nil
+	case "sha256":
+		return sha256.New, nil
+	case "sha1":
+		return sha1.New, nil
+	default:
+		return nil, fmt.Errorf("%s for hash function is invalid", algo)
+	}
+}

+ 118 - 0
pkg/generator/mfa/otp_test.go

@@ -0,0 +1,118 @@
+/*
+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 mfa
+
+import (
+	"encoding/base32"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestDefault(t *testing.T) {
+	input := base32.StdEncoding.EncodeToString([]byte("12345678912345678912"))
+	table := map[time.Time]string{
+		time.Date(1970, 1, 1, 0, 0, 59, 0, time.UTC):     "016480",
+		time.Date(2005, 3, 18, 1, 58, 29, 0, time.UTC):   "785218",
+		time.Date(2005, 3, 18, 1, 58, 31, 0, time.UTC):   "081980",
+		time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC):  "369925",
+		time.Date(2016, 9, 16, 12, 40, 12, 0, time.UTC):  "437634",
+		time.Date(2033, 5, 18, 3, 33, 20, 0, time.UTC):   "413665",
+		time.Date(2603, 10, 11, 11, 33, 20, 0, time.UTC): "151178",
+	}
+
+	for when, expected := range table {
+		code, _, err := generateCode(WithToken(input), WithWhen(when))
+
+		require.NoError(t, err)
+		require.Equal(t, expected, code, when.String())
+	}
+}
+
+func TestDifferentLength(t *testing.T) {
+	input := base32.StdEncoding.EncodeToString([]byte("12345678912345678912"))
+	table := map[time.Time]string{
+		time.Date(1970, 1, 1, 0, 0, 59, 0, time.UTC):     "71016480",
+		time.Date(2005, 3, 18, 1, 58, 29, 0, time.UTC):   "24785218",
+		time.Date(2005, 3, 18, 1, 58, 31, 0, time.UTC):   "89081980",
+		time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC):  "20369925",
+		time.Date(2016, 9, 16, 12, 40, 12, 0, time.UTC):  "92437634",
+		time.Date(2033, 5, 18, 3, 33, 20, 0, time.UTC):   "94413665",
+		time.Date(2603, 10, 11, 11, 33, 20, 0, time.UTC): "91151178",
+	}
+
+	for when, expected := range table {
+		code, _, err := generateCode(WithToken(input), WithWhen(when), WithLength(8))
+
+		require.NoError(t, err)
+		require.Equal(t, expected, code, when.String())
+	}
+}
+
+func TestSpaceSeparatedToken(t *testing.T) {
+	input := "asdf qwer zxcv fghj rtyu ghjk lk"
+	table := map[time.Time]string{
+		time.Date(1970, 1, 1, 0, 0, 59, 0, time.UTC):     "338356",
+		time.Date(2005, 3, 18, 1, 58, 29, 0, time.UTC):   "474671",
+		time.Date(2005, 3, 18, 1, 58, 31, 0, time.UTC):   "985005",
+		time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC):  "453314",
+		time.Date(2016, 9, 16, 12, 40, 12, 0, time.UTC):  "492092",
+		time.Date(2033, 5, 18, 3, 33, 20, 0, time.UTC):   "797055",
+		time.Date(2603, 10, 11, 11, 33, 20, 0, time.UTC): "385618",
+	}
+
+	for when, expected := range table {
+		code, _, err := generateCode(WithToken(input), WithWhen(when))
+
+		require.NoError(t, err)
+		require.Equal(t, expected, code, when.String())
+	}
+}
+
+func TestNonPaddedHashes(t *testing.T) {
+	input := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+	table := map[time.Time]string{
+		time.Date(1970, 1, 1, 0, 0, 59, 0, time.UTC):     "983918",
+		time.Date(2005, 3, 18, 1, 58, 29, 0, time.UTC):   "349978",
+		time.Date(2005, 3, 18, 1, 58, 31, 0, time.UTC):   "074850",
+		time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC):  "181361",
+		time.Date(2016, 9, 16, 12, 40, 12, 0, time.UTC):  "296434",
+		time.Date(2033, 5, 18, 3, 33, 20, 0, time.UTC):   "845675",
+		time.Date(2603, 10, 11, 11, 33, 20, 0, time.UTC): "055244",
+	}
+
+	for when, expected := range table {
+		code, _, err := generateCode(WithToken(input), WithWhen(when))
+
+		require.NoError(t, err)
+		require.Equal(t, expected, code, when.String())
+	}
+}
+
+func TestInvalidPadding(t *testing.T) {
+	input := "a6mr*&^&*%*&ylj|'[lbufszudtjdt42nh5by"
+	table := map[time.Time]string{
+		time.Date(1970, 1, 1, 0, 0, 59, 0, time.UTC):   "",
+		time.Date(2005, 3, 18, 1, 58, 29, 0, time.UTC): "",
+	}
+
+	for when, expected := range table {
+		code, _, err := generateCode(WithToken(input), WithWhen(when))
+
+		require.Error(t, err)
+		require.Equal(t, expected, code, when.String())
+	}
+}

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

@@ -23,6 +23,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/generator/gcr"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/github"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/grafana"
+	_ "github.com/external-secrets/external-secrets/pkg/generator/mfa"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/password"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/quay"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/sts"

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

@@ -267,6 +267,17 @@ func clusterGeneratorToVirtual(gen *genv1alpha1.ClusterGenerator) (client.Object
 			},
 			Spec: *gen.Spec.Generator.GrafanaSpec,
 		}, nil
+	case genv1alpha1.GeneratorKindMFA:
+		if gen.Spec.Generator.MFASpec == nil {
+			return nil, fmt.Errorf("when kind is %s, MFASpec must be set", gen.Spec.Kind)
+		}
+		return &genv1alpha1.MFA{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: genv1alpha1.SchemeGroupVersion.String(),
+				Kind:       genv1alpha1.MFAKind,
+			},
+			Spec: *gen.Spec.Generator.MFASpec,
+		}, nil
 	default:
 		return nil, fmt.Errorf("unknown kind %s", gen.Spec.Kind)
 	}