From 41da7c195e2bea24a58391182536d21680517a24 Mon Sep 17 00:00:00 2001
From: Birdie K <5210502+moo-im-a-cow@users.noreply.github.com>
Date: Sun, 4 May 2025 14:16:50 +1000
Subject: [PATCH 1/2] add VaultTokenSecret CRD
---
PROJECT | 9 +
api/v1beta1/vaulttokensecret_types.go | 94 +++++
api/v1beta1/zz_generated.deepcopy.go | 108 ++++++
...crets.hashicorp.com_vaulttokensecrets.yaml | 321 ++++++++++++++++++
chart/templates/role.yaml | 3 +
.../vaulttokensecret_editor_role.yaml | 36 ++
.../vaulttokensecret_viewer_role.yaml | 32 ++
common/common.go | 8 +
...crets.hashicorp.com_vaulttokensecrets.yaml | 321 ++++++++++++++++++
config/crd/kustomization.yaml | 1 +
...ecrets-operator.clusterserviceversion.yaml | 5 +
config/rbac/kustomization.yaml | 7 +
config/rbac/role.yaml | 3 +
config/rbac/vaulttokensecret_admin_role.yaml | 30 ++
config/rbac/vaulttokensecret_editor_role.yaml | 36 ++
config/rbac/vaulttokensecret_viewer_role.yaml | 32 ++
config/samples/kustomization.yaml | 1 +
.../secrets_v1beta1_vaulttokensecret.yaml | 20 ++
controllers/registry.go | 3 +
controllers/vaulttokensecret_controller.go | 306 +++++++++++++++++
docs/api/api-reference.md | 73 ++++
main.go | 12 +
22 files changed, 1461 insertions(+)
create mode 100644 api/v1beta1/vaulttokensecret_types.go
create mode 100644 chart/crds/secrets.hashicorp.com_vaulttokensecrets.yaml
create mode 100644 chart/templates/vaulttokensecret_editor_role.yaml
create mode 100644 chart/templates/vaulttokensecret_viewer_role.yaml
create mode 100644 config/crd/bases/secrets.hashicorp.com_vaulttokensecrets.yaml
create mode 100644 config/rbac/vaulttokensecret_admin_role.yaml
create mode 100644 config/rbac/vaulttokensecret_editor_role.yaml
create mode 100644 config/rbac/vaulttokensecret_viewer_role.yaml
create mode 100644 config/samples/secrets_v1beta1_vaulttokensecret.yaml
create mode 100644 controllers/vaulttokensecret_controller.go
diff --git a/PROJECT b/PROJECT
index 297d589b3..9b1ac7922 100644
--- a/PROJECT
+++ b/PROJECT
@@ -110,4 +110,13 @@ resources:
kind: VaultAuthGlobal
path: github.com/hashicorp/vault-secrets-operator/api/v1beta1
version: v1beta1
+- api:
+ crdVersion: v1
+ namespaced: true
+ controller: true
+ domain: hashicorp.com
+ group: secrets
+ kind: VaultTokenSecret
+ path: github.com/hashicorp/vault-secrets-operator/api/v1beta1
+ version: v1beta1
version: "3"
diff --git a/api/v1beta1/vaulttokensecret_types.go b/api/v1beta1/vaulttokensecret_types.go
new file mode 100644
index 000000000..720e0df9c
--- /dev/null
+++ b/api/v1beta1/vaulttokensecret_types.go
@@ -0,0 +1,94 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: BUSL-1.1
+
+package v1beta1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
+// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
+
+// VaultTokenSecretSpec defines the desired state of VaultTokenSecret.
+type VaultTokenSecretSpec struct {
+ // VaultAuthRef to the VaultAuth resource, can be prefixed with a namespace,
+ // eg: `namespaceA/vaultAuthRefB`. If no namespace prefix is provided it will default to the
+ // namespace of the VaultAuth CR. If no value is specified for VaultAuthRef the Operator will
+ // default to the `default` VaultAuth, configured in the operator's namespace.
+ VaultAuthRef string `json:"vaultAuthRef,omitempty"`
+ // Namespace of the secrets engine mount in Vault. If not set, the namespace that's
+ // part of VaultAuth resource will be inferred.
+ Namespace string `json:"namespace,omitempty"`
+ // Mount for the secret in Vault
+ // RefreshAfter a period of time, in duration notation e.g. 30s, 1m, 24h
+ // +kubebuilder:validation:Type=string
+ // +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(s|m|h))$`
+ RefreshAfter string `json:"refreshAfter,omitempty"`
+ // RolloutRestartTargets should be configured whenever the application(s) consuming the Vault secret does
+ // not support dynamically reloading a rotated secret.
+ // In that case one, or more RolloutRestartTarget(s) can be configured here. The Operator will
+ // trigger a "rollout-restart" for each target whenever the Vault secret changes between reconciliation events.
+ // All configured targets will be ignored if HMACSecretData is set to false.
+ // See RolloutRestartTarget for more details.
+ RolloutRestartTargets []RolloutRestartTarget `json:"rolloutRestartTargets,omitempty"`
+ // Destination provides configuration necessary for syncing the Vault secret to Kubernetes.
+ Destination Destination `json:"destination"`
+
+ // TokenRole is the name of the token role to use when creating the token.
+ TokenRole string `json:"tokenRole,omitempty"`
+
+ TTL string `json:"ttl,omitempty"`
+ Policies []string `json:"policies,omitempty"`
+ No_default_policy bool `json:"noDefaultPolicy,omitempty"`
+ DisplayName string `json:"displayName,omitempty"`
+ EntityAlias string `json:"entityAlias,omitempty"`
+ Meta map[string]string `json:"meta,omitempty"`
+
+ // RenewalPercent is the percent out of 100 of the lease duration when the
+ // lease is renewed. Defaults to 67 percent plus jitter.
+ // +kubebuilder:default=67
+ // +kubebuilder:validation:Minimum=0
+ // +kubebuilder:validation:Maximum=90
+ RenewalPercent int `json:"renewalPercent,omitempty"`
+ // Revoke the existing lease on VDS resource deletion.
+ Revoke bool `json:"revoke,omitempty"`
+}
+
+// VaultTokenSecretStatus defines the observed state of VaultTokenSecret.
+type VaultTokenSecretStatus struct {
+ // LastGeneration is the Generation of the last reconciled resource.
+ LastGeneration int64 `json:"lastGeneration"`
+ // TokenAccessor is the accessor of the token created by the operator.
+ // This is used to revoke the token when the resource is deleted.
+ TokenAccessor string `json:"tokenAccessor,omitempty"`
+ EntityID string `json:"entity_id,omitempty"`
+ LeaseDuration int `json:"lease_duration,omitempty"`
+ LastRenewalTime int64 `json:"lastRenewalTime"`
+ VaultClientMeta VaultClientMeta `json:"vaultClientMeta,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+
+// VaultTokenSecret is the Schema for the vaulttokensecrets API.
+type VaultTokenSecret struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec VaultTokenSecretSpec `json:"spec,omitempty"`
+ Status VaultTokenSecretStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// VaultTokenSecretList contains a list of VaultTokenSecret.
+type VaultTokenSecretList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+ Items []VaultTokenSecret `json:"items"`
+}
+
+func init() {
+ SchemeBuilder.Register(&VaultTokenSecret{}, &VaultTokenSecretList{})
+}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index dd288c0ed..0dd1a555b 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -1624,3 +1624,111 @@ func (in *VaultStaticSecretStatus) DeepCopy() *VaultStaticSecretStatus {
in.DeepCopyInto(out)
return out
}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VaultTokenSecret) DeepCopyInto(out *VaultTokenSecret) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultTokenSecret.
+func (in *VaultTokenSecret) DeepCopy() *VaultTokenSecret {
+ if in == nil {
+ return nil
+ }
+ out := new(VaultTokenSecret)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VaultTokenSecret) 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 *VaultTokenSecretList) DeepCopyInto(out *VaultTokenSecretList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]VaultTokenSecret, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultTokenSecretList.
+func (in *VaultTokenSecretList) DeepCopy() *VaultTokenSecretList {
+ if in == nil {
+ return nil
+ }
+ out := new(VaultTokenSecretList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *VaultTokenSecretList) 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 *VaultTokenSecretSpec) DeepCopyInto(out *VaultTokenSecretSpec) {
+ *out = *in
+ if in.RolloutRestartTargets != nil {
+ in, out := &in.RolloutRestartTargets, &out.RolloutRestartTargets
+ *out = make([]RolloutRestartTarget, len(*in))
+ copy(*out, *in)
+ }
+ in.Destination.DeepCopyInto(&out.Destination)
+ if in.Policies != nil {
+ in, out := &in.Policies, &out.Policies
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Meta != nil {
+ in, out := &in.Meta, &out.Meta
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultTokenSecretSpec.
+func (in *VaultTokenSecretSpec) DeepCopy() *VaultTokenSecretSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(VaultTokenSecretSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VaultTokenSecretStatus) DeepCopyInto(out *VaultTokenSecretStatus) {
+ *out = *in
+ out.VaultClientMeta = in.VaultClientMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultTokenSecretStatus.
+func (in *VaultTokenSecretStatus) DeepCopy() *VaultTokenSecretStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(VaultTokenSecretStatus)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/chart/crds/secrets.hashicorp.com_vaulttokensecrets.yaml b/chart/crds/secrets.hashicorp.com_vaulttokensecrets.yaml
new file mode 100644
index 000000000..e3bb2242c
--- /dev/null
+++ b/chart/crds/secrets.hashicorp.com_vaulttokensecrets.yaml
@@ -0,0 +1,321 @@
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.3
+ name: vaulttokensecrets.secrets.hashicorp.com
+spec:
+ group: secrets.hashicorp.com
+ names:
+ kind: VaultTokenSecret
+ listKind: VaultTokenSecretList
+ plural: vaulttokensecrets
+ singular: vaulttokensecret
+ scope: Namespaced
+ versions:
+ - name: v1beta1
+ schema:
+ openAPIV3Schema:
+ description: VaultTokenSecret is the Schema for the vaulttokensecrets API.
+ 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: VaultTokenSecretSpec defines the desired state of VaultTokenSecret.
+ properties:
+ destination:
+ description: Destination provides configuration necessary for syncing
+ the Vault secret to Kubernetes.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ description: Annotations to apply to the Secret. Requires Create
+ to be set to true.
+ type: object
+ create:
+ default: false
+ description: |-
+ Create the destination Secret.
+ If the Secret already exists this should be set to false.
+ type: boolean
+ labels:
+ additionalProperties:
+ type: string
+ description: Labels to apply to the Secret. Requires Create to
+ be set to true.
+ type: object
+ name:
+ description: Name of the Secret
+ type: string
+ overwrite:
+ default: false
+ description: |-
+ Overwrite the destination Secret if it exists and Create is true. This is
+ useful when migrating to VSO from a previous secret deployment strategy.
+ type: boolean
+ transformation:
+ description: |-
+ Transformation provides configuration for transforming the secret data before
+ it is stored in the Destination.
+ properties:
+ excludeRaw:
+ description: |-
+ ExcludeRaw data from the destination Secret. Exclusion policy can be set
+ globally by including 'exclude-raw` in the '--global-transformation-options'
+ command line flag. If set, the command line flag always takes precedence over
+ this configuration.
+ type: boolean
+ excludes:
+ description: |-
+ Excludes contains regex patterns used to filter top-level source secret data
+ fields for exclusion from the final K8s Secret data. These pattern filters are
+ never applied to templated fields as defined in Templates. They are always
+ applied before any inclusion patterns. To exclude all source secret data
+ fields, you can configure the single pattern ".*".
+ items:
+ type: string
+ type: array
+ includes:
+ description: |-
+ Includes contains regex patterns used to filter top-level source secret data
+ fields for inclusion in the final K8s Secret data. These pattern filters are
+ never applied to templated fields as defined in Templates. They are always
+ applied last.
+ items:
+ type: string
+ type: array
+ templates:
+ additionalProperties:
+ description: Template provides templating configuration.
+ properties:
+ name:
+ description: Name of the Template
+ type: string
+ text:
+ description: |-
+ Text contains the Go text template format. The template
+ references attributes from the data structure of the source secret.
+ Refer to https://pkg.go.dev/text/template for more information.
+ type: string
+ required:
+ - text
+ type: object
+ description: |-
+ Templates maps a template name to its Template. Templates are always included
+ in the rendered K8s Secret, and take precedence over templates defined in a
+ SecretTransformation.
+ type: object
+ transformationRefs:
+ description: |-
+ TransformationRefs contain references to template configuration from
+ SecretTransformation.
+ items:
+ description: |-
+ TransformationRef contains the configuration for accessing templates from an
+ SecretTransformation resource. TransformationRefs can be shared across all
+ syncable secret custom resources.
+ properties:
+ ignoreExcludes:
+ description: |-
+ IgnoreExcludes controls whether to use the SecretTransformation's Excludes
+ data key filters.
+ type: boolean
+ ignoreIncludes:
+ description: |-
+ IgnoreIncludes controls whether to use the SecretTransformation's Includes
+ data key filters.
+ type: boolean
+ name:
+ description: Name of the SecretTransformation resource.
+ type: string
+ namespace:
+ description: Namespace of the SecretTransformation resource.
+ type: string
+ templateRefs:
+ description: |-
+ TemplateRefs map to a Template found in this TransformationRef. If empty, then
+ all templates from the SecretTransformation will be rendered to the K8s Secret.
+ items:
+ description: |-
+ TemplateRef points to templating text that is stored in a
+ SecretTransformation custom resource.
+ properties:
+ keyOverride:
+ description: |-
+ KeyOverride to the rendered template in the Destination secret. If Key is
+ empty, then the Key from reference spec will be used. Set this to override the
+ Key set from the reference spec.
+ type: string
+ name:
+ description: |-
+ Name of the Template in SecretTransformationSpec.Templates.
+ the rendered secret data.
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ required:
+ - name
+ type: object
+ type: array
+ type: object
+ type:
+ description: |-
+ Type of Kubernetes Secret. Requires Create to be set to true.
+ Defaults to Opaque.
+ type: string
+ required:
+ - name
+ type: object
+ displayName:
+ type: string
+ entityAlias:
+ type: string
+ meta:
+ additionalProperties:
+ type: string
+ type: object
+ namespace:
+ description: |-
+ Namespace of the secrets engine mount in Vault. If not set, the namespace that's
+ part of VaultAuth resource will be inferred.
+ type: string
+ noDefaultPolicy:
+ type: boolean
+ policies:
+ items:
+ type: string
+ type: array
+ refreshAfter:
+ description: |-
+ Mount for the secret in Vault
+ RefreshAfter a period of time, in duration notation e.g. 30s, 1m, 24h
+ pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))$
+ type: string
+ renewalPercent:
+ default: 67
+ description: |-
+ RenewalPercent is the percent out of 100 of the lease duration when the
+ lease is renewed. Defaults to 67 percent plus jitter.
+ maximum: 90
+ minimum: 0
+ type: integer
+ revoke:
+ description: Revoke the existing lease on VDS resource deletion.
+ type: boolean
+ rolloutRestartTargets:
+ description: |-
+ RolloutRestartTargets should be configured whenever the application(s) consuming the Vault secret does
+ not support dynamically reloading a rotated secret.
+ In that case one, or more RolloutRestartTarget(s) can be configured here. The Operator will
+ trigger a "rollout-restart" for each target whenever the Vault secret changes between reconciliation events.
+ All configured targets will be ignored if HMACSecretData is set to false.
+ See RolloutRestartTarget for more details.
+ items:
+ description: |-
+ RolloutRestartTarget provides the configuration required to perform a
+ rollout-restart of the supported resources upon Vault Secret rotation.
+ The rollout-restart is triggered by patching the target resource's
+ 'spec.template.metadata.annotations' to include 'vso.secrets.hashicorp.com/restartedAt'
+ with a timestamp value of when the trigger was executed.
+ E.g. vso.secrets.hashicorp.com/restartedAt: "2023-03-23T13:39:31Z"
+
+ Supported resources: Deployment, DaemonSet, StatefulSet, argo.Rollout
+ properties:
+ kind:
+ description: Kind of the resource
+ enum:
+ - Deployment
+ - DaemonSet
+ - StatefulSet
+ - argo.Rollout
+ type: string
+ name:
+ description: Name of the resource
+ type: string
+ required:
+ - kind
+ - name
+ type: object
+ type: array
+ tokenRole:
+ description: TokenRole is the name of the token role to use when creating
+ the token.
+ type: string
+ ttl:
+ type: string
+ vaultAuthRef:
+ description: |-
+ VaultAuthRef to the VaultAuth resource, can be prefixed with a namespace,
+ eg: `namespaceA/vaultAuthRefB`. If no namespace prefix is provided it will default to the
+ namespace of the VaultAuth CR. If no value is specified for VaultAuthRef the Operator will
+ default to the `default` VaultAuth, configured in the operator's namespace.
+ type: string
+ required:
+ - destination
+ type: object
+ status:
+ description: VaultTokenSecretStatus defines the observed state of VaultTokenSecret.
+ properties:
+ entity_id:
+ type: string
+ lastGeneration:
+ description: LastGeneration is the Generation of the last reconciled
+ resource.
+ format: int64
+ type: integer
+ lastRenewalTime:
+ format: int64
+ type: integer
+ lease_duration:
+ type: integer
+ tokenAccessor:
+ description: |-
+ TokenAccessor is the accessor of the token created by the operator.
+ This is used to revoke the token when the resource is deleted.
+ type: string
+ vaultClientMeta:
+ description: |-
+ VaultClientMeta defines the observed state of the last Vault Client used to
+ sync the secret. This status is used during resource reconciliation.
+ properties:
+ cacheKey:
+ description: CacheKey is the unique key used to identify the client
+ cache.
+ type: string
+ id:
+ description: |-
+ ID is the Vault ID of the authenticated client. The ID should never contain
+ any sensitive information.
+ type: string
+ type: object
+ required:
+ - lastGeneration
+ - lastRenewalTime
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/chart/templates/role.yaml b/chart/templates/role.yaml
index fdff19680..0404d57de 100644
--- a/chart/templates/role.yaml
+++ b/chart/templates/role.yaml
@@ -86,6 +86,7 @@ rules:
- vaultdynamicsecrets
- vaultpkisecrets
- vaultstaticsecrets
+ - vaulttokensecrets
verbs:
- create
- delete
@@ -106,6 +107,7 @@ rules:
- vaultdynamicsecrets/finalizers
- vaultpkisecrets/finalizers
- vaultstaticsecrets/finalizers
+ - vaulttokensecrets/finalizers
verbs:
- update
- apiGroups:
@@ -120,6 +122,7 @@ rules:
- vaultdynamicsecrets/status
- vaultpkisecrets/status
- vaultstaticsecrets/status
+ - vaulttokensecrets/status
verbs:
- get
- patch
diff --git a/chart/templates/vaulttokensecret_editor_role.yaml b/chart/templates/vaulttokensecret_editor_role.yaml
new file mode 100644
index 000000000..1488fa4e1
--- /dev/null
+++ b/chart/templates/vaulttokensecret_editor_role.yaml
@@ -0,0 +1,36 @@
+{{- /*
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+# auto generated by sync-rbac.sh from ./config/rbac/vaulttokensecret_editor_role.yaml -- do not edit
+*/ -}}
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "vaulttokensecret-editor-role" }}
+ labels:
+ app.kubernetes.io/component: rbac
+ # allow for selecting on the canonical name
+ vso.hashicorp.com/role-instance: vaulttokensecret-editor-role
+ vso.hashicorp.com/aggregate-to-editor: "true"
+ {{- include "vso.chart.labels" . | nindent 4 }}
+rules:
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets/status
+ verbs:
+ - get
diff --git a/chart/templates/vaulttokensecret_viewer_role.yaml b/chart/templates/vaulttokensecret_viewer_role.yaml
new file mode 100644
index 000000000..6f480e3f6
--- /dev/null
+++ b/chart/templates/vaulttokensecret_viewer_role.yaml
@@ -0,0 +1,32 @@
+{{- /*
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+# auto generated by sync-rbac.sh from ./config/rbac/vaulttokensecret_viewer_role.yaml -- do not edit
+*/ -}}
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ printf "%s-%s" (include "vso.chart.fullname" .) "vaulttokensecret-viewer-role" }}
+ labels:
+ app.kubernetes.io/component: rbac
+ # allow for selecting on the canonical name
+ vso.hashicorp.com/role-instance: vaulttokensecret-viewer-role
+ vso.hashicorp.com/aggregate-to-viewer: "true"
+ {{- include "vso.chart.labels" . | nindent 4 }}
+rules:
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets/status
+ verbs:
+ - get
diff --git a/common/common.go b/common/common.go
index d422e7c5d..1798c59b1 100644
--- a/common/common.go
+++ b/common/common.go
@@ -742,6 +742,8 @@ func GetVaultNamespace(obj client.Object) (string, error) {
ns = o.Spec.Namespace
case *secretsv1beta1.VaultDynamicSecret:
ns = o.Spec.Namespace
+ case *secretsv1beta1.VaultTokenSecret:
+ ns = o.Spec.Namespace
default:
return "", fmt.Errorf("unsupported type %T", o)
}
@@ -792,6 +794,12 @@ func NewSyncableSecretMetaData(obj ctrlclient.Object) (*SyncableSecretMetaData,
meta.APIVersion = t.APIVersion
meta.Kind = t.Kind
meta.AuthRef = t.Spec.VaultAuthRef
+ case *secretsv1beta1.VaultTokenSecret:
+ meta.Destination = t.Spec.Destination.DeepCopy()
+ meta.APIVersion = t.APIVersion
+ meta.Kind = t.Kind
+ meta.AuthRef = t.Spec.VaultAuthRef
+
case *secretsv1beta1.VaultStaticSecret:
meta.Destination = t.Spec.Destination.DeepCopy()
meta.APIVersion = t.APIVersion
diff --git a/config/crd/bases/secrets.hashicorp.com_vaulttokensecrets.yaml b/config/crd/bases/secrets.hashicorp.com_vaulttokensecrets.yaml
new file mode 100644
index 000000000..e3bb2242c
--- /dev/null
+++ b/config/crd/bases/secrets.hashicorp.com_vaulttokensecrets.yaml
@@ -0,0 +1,321 @@
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.3
+ name: vaulttokensecrets.secrets.hashicorp.com
+spec:
+ group: secrets.hashicorp.com
+ names:
+ kind: VaultTokenSecret
+ listKind: VaultTokenSecretList
+ plural: vaulttokensecrets
+ singular: vaulttokensecret
+ scope: Namespaced
+ versions:
+ - name: v1beta1
+ schema:
+ openAPIV3Schema:
+ description: VaultTokenSecret is the Schema for the vaulttokensecrets API.
+ 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: VaultTokenSecretSpec defines the desired state of VaultTokenSecret.
+ properties:
+ destination:
+ description: Destination provides configuration necessary for syncing
+ the Vault secret to Kubernetes.
+ properties:
+ annotations:
+ additionalProperties:
+ type: string
+ description: Annotations to apply to the Secret. Requires Create
+ to be set to true.
+ type: object
+ create:
+ default: false
+ description: |-
+ Create the destination Secret.
+ If the Secret already exists this should be set to false.
+ type: boolean
+ labels:
+ additionalProperties:
+ type: string
+ description: Labels to apply to the Secret. Requires Create to
+ be set to true.
+ type: object
+ name:
+ description: Name of the Secret
+ type: string
+ overwrite:
+ default: false
+ description: |-
+ Overwrite the destination Secret if it exists and Create is true. This is
+ useful when migrating to VSO from a previous secret deployment strategy.
+ type: boolean
+ transformation:
+ description: |-
+ Transformation provides configuration for transforming the secret data before
+ it is stored in the Destination.
+ properties:
+ excludeRaw:
+ description: |-
+ ExcludeRaw data from the destination Secret. Exclusion policy can be set
+ globally by including 'exclude-raw` in the '--global-transformation-options'
+ command line flag. If set, the command line flag always takes precedence over
+ this configuration.
+ type: boolean
+ excludes:
+ description: |-
+ Excludes contains regex patterns used to filter top-level source secret data
+ fields for exclusion from the final K8s Secret data. These pattern filters are
+ never applied to templated fields as defined in Templates. They are always
+ applied before any inclusion patterns. To exclude all source secret data
+ fields, you can configure the single pattern ".*".
+ items:
+ type: string
+ type: array
+ includes:
+ description: |-
+ Includes contains regex patterns used to filter top-level source secret data
+ fields for inclusion in the final K8s Secret data. These pattern filters are
+ never applied to templated fields as defined in Templates. They are always
+ applied last.
+ items:
+ type: string
+ type: array
+ templates:
+ additionalProperties:
+ description: Template provides templating configuration.
+ properties:
+ name:
+ description: Name of the Template
+ type: string
+ text:
+ description: |-
+ Text contains the Go text template format. The template
+ references attributes from the data structure of the source secret.
+ Refer to https://pkg.go.dev/text/template for more information.
+ type: string
+ required:
+ - text
+ type: object
+ description: |-
+ Templates maps a template name to its Template. Templates are always included
+ in the rendered K8s Secret, and take precedence over templates defined in a
+ SecretTransformation.
+ type: object
+ transformationRefs:
+ description: |-
+ TransformationRefs contain references to template configuration from
+ SecretTransformation.
+ items:
+ description: |-
+ TransformationRef contains the configuration for accessing templates from an
+ SecretTransformation resource. TransformationRefs can be shared across all
+ syncable secret custom resources.
+ properties:
+ ignoreExcludes:
+ description: |-
+ IgnoreExcludes controls whether to use the SecretTransformation's Excludes
+ data key filters.
+ type: boolean
+ ignoreIncludes:
+ description: |-
+ IgnoreIncludes controls whether to use the SecretTransformation's Includes
+ data key filters.
+ type: boolean
+ name:
+ description: Name of the SecretTransformation resource.
+ type: string
+ namespace:
+ description: Namespace of the SecretTransformation resource.
+ type: string
+ templateRefs:
+ description: |-
+ TemplateRefs map to a Template found in this TransformationRef. If empty, then
+ all templates from the SecretTransformation will be rendered to the K8s Secret.
+ items:
+ description: |-
+ TemplateRef points to templating text that is stored in a
+ SecretTransformation custom resource.
+ properties:
+ keyOverride:
+ description: |-
+ KeyOverride to the rendered template in the Destination secret. If Key is
+ empty, then the Key from reference spec will be used. Set this to override the
+ Key set from the reference spec.
+ type: string
+ name:
+ description: |-
+ Name of the Template in SecretTransformationSpec.Templates.
+ the rendered secret data.
+ type: string
+ required:
+ - name
+ type: object
+ type: array
+ required:
+ - name
+ type: object
+ type: array
+ type: object
+ type:
+ description: |-
+ Type of Kubernetes Secret. Requires Create to be set to true.
+ Defaults to Opaque.
+ type: string
+ required:
+ - name
+ type: object
+ displayName:
+ type: string
+ entityAlias:
+ type: string
+ meta:
+ additionalProperties:
+ type: string
+ type: object
+ namespace:
+ description: |-
+ Namespace of the secrets engine mount in Vault. If not set, the namespace that's
+ part of VaultAuth resource will be inferred.
+ type: string
+ noDefaultPolicy:
+ type: boolean
+ policies:
+ items:
+ type: string
+ type: array
+ refreshAfter:
+ description: |-
+ Mount for the secret in Vault
+ RefreshAfter a period of time, in duration notation e.g. 30s, 1m, 24h
+ pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))$
+ type: string
+ renewalPercent:
+ default: 67
+ description: |-
+ RenewalPercent is the percent out of 100 of the lease duration when the
+ lease is renewed. Defaults to 67 percent plus jitter.
+ maximum: 90
+ minimum: 0
+ type: integer
+ revoke:
+ description: Revoke the existing lease on VDS resource deletion.
+ type: boolean
+ rolloutRestartTargets:
+ description: |-
+ RolloutRestartTargets should be configured whenever the application(s) consuming the Vault secret does
+ not support dynamically reloading a rotated secret.
+ In that case one, or more RolloutRestartTarget(s) can be configured here. The Operator will
+ trigger a "rollout-restart" for each target whenever the Vault secret changes between reconciliation events.
+ All configured targets will be ignored if HMACSecretData is set to false.
+ See RolloutRestartTarget for more details.
+ items:
+ description: |-
+ RolloutRestartTarget provides the configuration required to perform a
+ rollout-restart of the supported resources upon Vault Secret rotation.
+ The rollout-restart is triggered by patching the target resource's
+ 'spec.template.metadata.annotations' to include 'vso.secrets.hashicorp.com/restartedAt'
+ with a timestamp value of when the trigger was executed.
+ E.g. vso.secrets.hashicorp.com/restartedAt: "2023-03-23T13:39:31Z"
+
+ Supported resources: Deployment, DaemonSet, StatefulSet, argo.Rollout
+ properties:
+ kind:
+ description: Kind of the resource
+ enum:
+ - Deployment
+ - DaemonSet
+ - StatefulSet
+ - argo.Rollout
+ type: string
+ name:
+ description: Name of the resource
+ type: string
+ required:
+ - kind
+ - name
+ type: object
+ type: array
+ tokenRole:
+ description: TokenRole is the name of the token role to use when creating
+ the token.
+ type: string
+ ttl:
+ type: string
+ vaultAuthRef:
+ description: |-
+ VaultAuthRef to the VaultAuth resource, can be prefixed with a namespace,
+ eg: `namespaceA/vaultAuthRefB`. If no namespace prefix is provided it will default to the
+ namespace of the VaultAuth CR. If no value is specified for VaultAuthRef the Operator will
+ default to the `default` VaultAuth, configured in the operator's namespace.
+ type: string
+ required:
+ - destination
+ type: object
+ status:
+ description: VaultTokenSecretStatus defines the observed state of VaultTokenSecret.
+ properties:
+ entity_id:
+ type: string
+ lastGeneration:
+ description: LastGeneration is the Generation of the last reconciled
+ resource.
+ format: int64
+ type: integer
+ lastRenewalTime:
+ format: int64
+ type: integer
+ lease_duration:
+ type: integer
+ tokenAccessor:
+ description: |-
+ TokenAccessor is the accessor of the token created by the operator.
+ This is used to revoke the token when the resource is deleted.
+ type: string
+ vaultClientMeta:
+ description: |-
+ VaultClientMeta defines the observed state of the last Vault Client used to
+ sync the secret. This status is used during resource reconciliation.
+ properties:
+ cacheKey:
+ description: CacheKey is the unique key used to identify the client
+ cache.
+ type: string
+ id:
+ description: |-
+ ID is the Vault ID of the authenticated client. The ID should never contain
+ any sensitive information.
+ type: string
+ type: object
+ required:
+ - lastGeneration
+ - lastRenewalTime
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 009cb25dd..88db655de 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -14,6 +14,7 @@ resources:
- bases/secrets.hashicorp.com_hcpauths.yaml
- bases/secrets.hashicorp.com_secrettransformations.yaml
- bases/secrets.hashicorp.com_vaultauthglobals.yaml
+- bases/secrets.hashicorp.com_vaulttokensecrets.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:
diff --git a/config/manifests/bases/vault-secrets-operator.clusterserviceversion.yaml b/config/manifests/bases/vault-secrets-operator.clusterserviceversion.yaml
index fab9584d7..28935d781 100644
--- a/config/manifests/bases/vault-secrets-operator.clusterserviceversion.yaml
+++ b/config/manifests/bases/vault-secrets-operator.clusterserviceversion.yaml
@@ -124,6 +124,11 @@ spec:
kind: VaultStaticSecret
name: vaultstaticsecrets.secrets.hashicorp.com
version: v1beta1
+ - description: VaultTokenSecret is the Schema for the vaulttokensecrets API.
+ displayName: Vault Token Secret
+ kind: VaultTokenSecret
+ name: vaulttokensecrets.secrets.hashicorp.com
+ version: v1beta1
description: |-
The Vault Secrets Operator (VSO) allows Pods to consume Vault secrets
natively from Kubernetes Secrets.
diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml
index e8e4aa95a..ff62b1776 100644
--- a/config/rbac/kustomization.yaml
+++ b/config/rbac/kustomization.yaml
@@ -19,3 +19,10 @@ resources:
- auth_proxy_role.yaml
- auth_proxy_role_binding.yaml
- auth_proxy_client_clusterrole.yaml
+# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
+# default, aiding admins in cluster management. Those roles are
+# not used by the {{ .ProjectName }} itself. You can comment the following lines
+# if you do not want those helpers be installed with your Project.
+- vaulttokensecret_admin_role.yaml
+- vaulttokensecret_editor_role.yaml
+- vaulttokensecret_viewer_role.yaml
\ No newline at end of file
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 0ae6b4415..7bcbbe8b8 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -77,6 +77,7 @@ rules:
- vaultdynamicsecrets
- vaultpkisecrets
- vaultstaticsecrets
+ - vaulttokensecrets
verbs:
- create
- delete
@@ -97,6 +98,7 @@ rules:
- vaultdynamicsecrets/finalizers
- vaultpkisecrets/finalizers
- vaultstaticsecrets/finalizers
+ - vaulttokensecrets/finalizers
verbs:
- update
- apiGroups:
@@ -111,6 +113,7 @@ rules:
- vaultdynamicsecrets/status
- vaultpkisecrets/status
- vaultstaticsecrets/status
+ - vaulttokensecrets/status
verbs:
- get
- patch
diff --git a/config/rbac/vaulttokensecret_admin_role.yaml b/config/rbac/vaulttokensecret_admin_role.yaml
new file mode 100644
index 000000000..2c38ce11b
--- /dev/null
+++ b/config/rbac/vaulttokensecret_admin_role.yaml
@@ -0,0 +1,30 @@
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+# This rule is not used by the project vault-secrets-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants full permissions ('*') over secrets.hashicorp.com.
+# This role is intended for users authorized to modify roles and bindings within the cluster,
+# enabling them to delegate specific permissions to other users or groups as needed.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: vaulttokensecret-admin-role
+rules:
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets
+ verbs:
+ - '*'
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets/status
+ verbs:
+ - get
diff --git a/config/rbac/vaulttokensecret_editor_role.yaml b/config/rbac/vaulttokensecret_editor_role.yaml
new file mode 100644
index 000000000..3d66dd2b7
--- /dev/null
+++ b/config/rbac/vaulttokensecret_editor_role.yaml
@@ -0,0 +1,36 @@
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+# This rule is not used by the project vault-secrets-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants permissions to create, update, and delete resources within the secrets.hashicorp.com.
+# This role is intended for users who need to manage these resources
+# but should not control RBAC or manage permissions for others.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: vaulttokensecret-editor-role
+rules:
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets/status
+ verbs:
+ - get
diff --git a/config/rbac/vaulttokensecret_viewer_role.yaml b/config/rbac/vaulttokensecret_viewer_role.yaml
new file mode 100644
index 000000000..dd52ddecd
--- /dev/null
+++ b/config/rbac/vaulttokensecret_viewer_role.yaml
@@ -0,0 +1,32 @@
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+# This rule is not used by the project vault-secrets-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants read-only access to secrets.hashicorp.com resources.
+# This role is intended for users who need visibility into these resources
+# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ app.kubernetes.io/name: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: vaulttokensecret-viewer-role
+rules:
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - secrets.hashicorp.com
+ resources:
+ - vaulttokensecrets/status
+ verbs:
+ - get
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
index 13f76038f..bc47a9968 100644
--- a/config/samples/kustomization.yaml
+++ b/config/samples/kustomization.yaml
@@ -13,4 +13,5 @@ resources:
- secrets_v1beta1_hcpauth.yaml
- secrets_v1beta1_secrettransformation.yaml
- secrets_v1beta1_vaultauthglobal.yaml
+- secrets_v1beta1_vaulttokensecret.yaml
#+kubebuilder:scaffold:manifestskustomizesamples
diff --git a/config/samples/secrets_v1beta1_vaulttokensecret.yaml b/config/samples/secrets_v1beta1_vaulttokensecret.yaml
new file mode 100644
index 000000000..6c67be070
--- /dev/null
+++ b/config/samples/secrets_v1beta1_vaulttokensecret.yaml
@@ -0,0 +1,20 @@
+# Copyright (c) HashiCorp, Inc.
+# SPDX-License-Identifier: BUSL-1.1
+
+apiVersion: secrets.hashicorp.com/v1beta1
+kind: VaultTokenSecret
+metadata:
+ labels:
+ app.kubernetes.io/name: vault-secrets-operator
+ app.kubernetes.io/managed-by: kustomize
+ name: vaulttokensecret-sample
+spec:
+ destination:
+ create: true
+ name: vaulttokensecret-sample
+ tokenRole: "vaulttokensecret-sample"
+ policies:
+ - "demopolicy"
+ meta:
+ user: test
+ ttl: 1h
diff --git a/controllers/registry.go b/controllers/registry.go
index 1f5fb819a..d0393eb9d 100644
--- a/controllers/registry.go
+++ b/controllers/registry.go
@@ -21,6 +21,7 @@ const (
HCPVaultSecretsApp
VaultAuth
VaultAuthGlobal
+ VaultTokenSecret
)
func (k ResourceKind) String() string {
@@ -39,6 +40,8 @@ func (k ResourceKind) String() string {
return "VaultAuth"
case VaultAuthGlobal:
return "VaultAuthGlobal"
+ case VaultTokenSecret:
+ return "VaultTokenSecret"
default:
return "unknown"
}
diff --git a/controllers/vaulttokensecret_controller.go b/controllers/vaulttokensecret_controller.go
new file mode 100644
index 000000000..85744b488
--- /dev/null
+++ b/controllers/vaulttokensecret_controller.go
@@ -0,0 +1,306 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: BUSL-1.1
+
+package controllers
+
+import (
+ "context"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/tools/record"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/source"
+
+ secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1"
+ "github.com/hashicorp/vault-secrets-operator/consts"
+ "github.com/hashicorp/vault-secrets-operator/helpers"
+ "github.com/hashicorp/vault-secrets-operator/vault"
+)
+
+const (
+ vaultTokenSecretFinalizer = "vaulttokensecret.secrets.hashicorp.com/finalizer"
+)
+
+// VaultTokenSecretReconciler reconciles a VaultTokenSecret object
+type VaultTokenSecretReconciler struct {
+ client.Client
+ Scheme *runtime.Scheme
+ Recorder record.EventRecorder
+ ClientFactory vault.ClientFactory
+ SecretDataBuilder *helpers.SecretDataBuilder
+ SecretsClient client.Client
+ HMACValidator helpers.HMACValidator
+ referenceCache ResourceReferenceCache
+ GlobalTransformationOptions *helpers.GlobalTransformationOptions
+ BackOffRegistry *BackOffRegistry
+ // SourceCh is used to trigger a requeue of resource instances from an
+ // external source. Should be set on a source.Channel in SetupWithManager.
+ // This channel should be closed when the controller is stopped.
+ SourceCh chan event.GenericEvent
+ eventWatcherRegistry *eventWatcherRegistry
+}
+
+// +kubebuilder:rbac:groups=secrets.hashicorp.com,resources=vaulttokensecrets,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=secrets.hashicorp.com,resources=vaulttokensecrets/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=secrets.hashicorp.com,resources=vaulttokensecrets/finalizers,verbs=update
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+// TODO(user): Modify the Reconcile function to compare the state specified by
+// the VaultTokenSecret object against the actual cluster state, and then
+// perform operations to make the cluster state reflect the state specified by
+// the user.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/reconcile
+func (r *VaultTokenSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ logger := log.FromContext(ctx)
+
+ o := &secretsv1beta1.VaultTokenSecret{}
+ if err := r.Client.Get(ctx, req.NamespacedName, o); err != nil {
+ if apierrors.IsNotFound(err) {
+ return ctrl.Result{}, nil
+ }
+
+ logger.Error(err, "error getting resource from k8s", "secret", o)
+ return ctrl.Result{}, err
+ }
+
+ if o.GetDeletionTimestamp() != nil {
+ logger.Info("Got deletion timestamp", "obj", o)
+ return ctrl.Result{}, r.handleDeletion(ctx, o)
+ }
+
+ c, err := r.ClientFactory.Get(ctx, r.Client, o)
+ if err != nil {
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonVaultClientConfigError,
+ "Failed to get Vault auth login: %s", err)
+ return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+ }
+
+ var requeueAfter time.Duration
+ if o.Spec.RefreshAfter != "" {
+ d, err := parseDurationString(o.Spec.RefreshAfter, ".spec.refreshAfter", 0)
+ if err != nil {
+ logger.Error(err, "Field validation failed")
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonVaultStaticSecret,
+ "Field validation failed, err=%s", err)
+ return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+ }
+ requeueAfter = computeHorizonWithJitter(d)
+ }
+
+ r.referenceCache.Set(SecretTransformation, req.NamespacedName,
+ helpers.GetTransformationRefObjKeys(
+ o.Spec.Destination.Transformation, o.Namespace)...)
+
+ transOption, err := helpers.NewSecretTransformationOption(ctx, r.Client, o, r.GlobalTransformationOptions)
+ if err != nil {
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonTransformationError,
+ "Failed setting up SecretTransformationOption: %s", err)
+ return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+ }
+
+ tokenReq, err := newTokenRequest(o.Spec)
+ if err != nil {
+ r.Recorder.Event(o, corev1.EventTypeWarning, consts.ReasonVaultStaticSecret, err.Error())
+ return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+ }
+
+ resp, err := c.Write(ctx, tokenReq)
+ if err != nil {
+ if vault.IsForbiddenError(err) {
+ c.Taint()
+ }
+
+ entry, _ := r.BackOffRegistry.Get(req.NamespacedName)
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonVaultClientError,
+ "Failed to create Vault token: %s", err)
+ return ctrl.Result{RequeueAfter: entry.NextBackOff()}, nil
+ } else {
+ r.BackOffRegistry.Delete(req.NamespacedName)
+ }
+
+ authdata := make(map[string]any)
+
+ if resp.Secret().Auth != nil {
+ o.Status.TokenAccessor = resp.Secret().Auth.Accessor
+ o.Status.EntityID = resp.Secret().Auth.EntityID
+ o.Status.LeaseDuration = resp.Secret().Auth.LeaseDuration
+ o.Status.LastRenewalTime = nowFunc().Unix()
+
+ authdata["accessor"] = resp.Secret().Auth.Accessor
+ authdata["token"] = resp.Secret().Auth.ClientToken
+ authdata["policies"] = resp.Secret().Auth.Policies
+ authdata["token_policies"] = resp.Secret().Auth.TokenPolicies
+ authdata["identity_policies"] = resp.Secret().Auth.IdentityPolicies
+ authdata["metadata"] = resp.Secret().Auth.Metadata
+ authdata["entity_id"] = resp.Secret().Auth.EntityID
+ authdata["orphan"] = resp.Secret().Auth.Orphan
+ authdata["lease_duration"] = resp.Secret().Auth.LeaseDuration
+ authdata["renewable"] = resp.Secret().Auth.Renewable
+ }
+ authinfo := make(map[string]any)
+ authinfo["data"] = authdata
+
+ data, err := r.SecretDataBuilder.WithVaultData(authdata, resp.Secret().Data, transOption)
+ if err != nil {
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretDataBuilderError,
+ "Failed to build K8s secret data: %s", err)
+ return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+ }
+ leaseDuration := time.Duration(o.Status.LeaseDuration) * time.Second
+ if leaseDuration < 1 {
+ // set an artificial leaseDuration in the case the lease duration is not
+ // compatible with computeHorizonWithJitter()
+ leaseDuration = time.Second * 5
+ }
+ // leaseDuration = time.Second * 5
+
+ horizon := computeDynamicHorizonWithJitter(leaseDuration, o.Spec.RenewalPercent)
+ // if horizon > requeueAfter {
+ requeueAfter = horizon
+ // }
+ r.Recorder.Eventf(o, corev1.EventTypeNormal, "requeueAfter", "requeueAfter: %d", requeueAfter)
+
+ var doRolloutRestart bool
+ doSync := true
+
+ if doSync {
+ if err := helpers.SyncSecret(ctx, r.Client, o, data); err != nil {
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretSyncError,
+ "Failed to update k8s secret: %s", err)
+ return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+ }
+ reason := consts.ReasonSecretSynced
+ if doRolloutRestart {
+ reason = consts.ReasonSecretRotated
+ // rollout-restart errors are not retryable
+ // all error reporting is handled by helpers.HandleRolloutRestarts
+ _ = helpers.HandleRolloutRestarts(ctx, r.Client, o, r.Recorder)
+ }
+ r.Recorder.Event(o, corev1.EventTypeNormal, reason, "Secret synced")
+ } else {
+ logger.V(consts.LogLevelDebug).Info("Secret sync not required")
+ }
+
+ if err := r.updateStatus(ctx, o); err != nil {
+ return ctrl.Result{}, err
+ }
+
+ return ctrl.Result{
+ RequeueAfter: requeueAfter,
+ }, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *VaultTokenSecretReconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
+ r.referenceCache = newResourceReferenceCache()
+ if r.BackOffRegistry == nil {
+ r.BackOffRegistry = NewBackOffRegistry()
+ }
+ r.SourceCh = make(chan event.GenericEvent)
+ r.eventWatcherRegistry = newEventWatcherRegistry()
+
+ return ctrl.NewControllerManagedBy(mgr).
+ For(&secretsv1beta1.VaultTokenSecret{}).
+ WithEventFilter(syncableSecretPredicate(nil)).
+ WithOptions(opts).
+ Watches(
+ &secretsv1beta1.SecretTransformation{},
+ NewEnqueueRefRequestsHandlerST(r.referenceCache, nil),
+ ).
+ // In order to reduce the operator's memory usage, we only watch for the
+ // Secret's metadata. That is sufficient for us to know when a Secret is
+ // deleted. If we ever need to access to the Secret's data, we can always fetch
+ // it from the API server in a RequestHandler, selectively based on the Secret's
+ // labels.
+ WatchesMetadata(
+ &corev1.Secret{},
+ &enqueueOnDeletionRequestHandler{
+ gvk: secretsv1beta1.GroupVersion.WithKind(VaultTokenSecret.String()),
+ },
+ builder.WithPredicates(&secretsPredicate{}),
+ ).
+ WatchesRawSource(
+ source.Channel(r.SourceCh,
+ &enqueueDelayingSyncEventHandler{
+ enqueueDurationForJitter: time.Second * 2,
+ },
+ ),
+ ).
+ Complete(r)
+}
+
+func (r *VaultTokenSecretReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultTokenSecret) error {
+ logger := log.FromContext(ctx)
+ logger.V(consts.LogLevelDebug).Info("Updating status")
+ o.Status.LastGeneration = o.GetGeneration()
+ if err := r.Status().Update(ctx, o); err != nil {
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonStatusUpdateError,
+ "Failed to update the resource's status, err=%s", err)
+ }
+
+ _, err := maybeAddFinalizer(ctx, r.Client, o, vaultTokenSecretFinalizer)
+ return err
+}
+
+func (r *VaultTokenSecretReconciler) handleDeletion(ctx context.Context, o client.Object) error {
+ logger := log.FromContext(ctx)
+ logger.Info("deleting")
+
+ objKey := client.ObjectKeyFromObject(o)
+ r.referenceCache.Remove(SecretTransformation, objKey)
+ r.BackOffRegistry.Delete(objKey)
+ if controllerutil.ContainsFinalizer(o, vaultTokenSecretFinalizer) {
+ logger.Info("Removing finalizer")
+ if controllerutil.RemoveFinalizer(o, vaultTokenSecretFinalizer) {
+ if err := r.Update(ctx, o); err != nil {
+ logger.Error(err, "Failed to remove the finalizer")
+ return err
+ }
+ logger.Info("Successfully removed the finalizer")
+ }
+ }
+ return nil
+}
+
+func newTokenRequest(s secretsv1beta1.VaultTokenSecretSpec) (vault.WriteRequest, error) {
+ var Req vault.WriteRequest
+ Path := "auth/token/create"
+ if s.TokenRole != "" {
+ Path = Path + "/" + s.TokenRole
+ }
+ params := make(map[string]any)
+ params["renewable"] = false
+ params["no_default_policy"] = s.No_default_policy
+ if s.TTL != "" {
+ params["ttl"] = s.TTL
+ params["explicit_max_ttl"] = s.TTL
+ }
+ if s.DisplayName != "" {
+ params["display_name"] = s.DisplayName
+ }
+ if s.EntityAlias != "" {
+ params["entity_alias"] = s.EntityAlias
+ }
+ if s.Policies != nil {
+ params["policies"] = s.Policies
+ }
+ if s.Meta != nil {
+ params["meta"] = s.Meta
+ }
+
+ Req = vault.NewWriteRequest(Path, params)
+
+ return Req, nil
+}
diff --git a/docs/api/api-reference.md b/docs/api/api-reference.md
index 43c734fe6..21d952d66 100644
--- a/docs/api/api-reference.md
+++ b/docs/api/api-reference.md
@@ -27,6 +27,8 @@ Package v1beta1 contains API Schema definitions for the secrets v1beta1 API grou
- [VaultPKISecretList](#vaultpkisecretlist)
- [VaultStaticSecret](#vaultstaticsecret)
- [VaultStaticSecretList](#vaultstaticsecretlist)
+- [VaultTokenSecret](#vaulttokensecret)
+- [VaultTokenSecretList](#vaulttokensecretlist)
@@ -44,6 +46,7 @@ _Appears in:_
- [VaultDynamicSecretSpec](#vaultdynamicsecretspec)
- [VaultPKISecretSpec](#vaultpkisecretspec)
- [VaultStaticSecretSpec](#vaultstaticsecretspec)
+- [VaultTokenSecretSpec](#vaulttokensecretspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
@@ -284,6 +287,7 @@ _Appears in:_
- [VaultDynamicSecretSpec](#vaultdynamicsecretspec)
- [VaultPKISecretSpec](#vaultpkisecretspec)
- [VaultStaticSecretSpec](#vaultstaticsecretspec)
+- [VaultTokenSecretSpec](#vaulttokensecretspec)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
@@ -874,6 +878,7 @@ sync the secret. This status is used during resource reconciliation.
_Appears in:_
- [VaultDynamicSecretStatus](#vaultdynamicsecretstatus)
+- [VaultTokenSecretStatus](#vaulttokensecretstatus)
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
@@ -1185,3 +1190,71 @@ _Appears in:_
+#### VaultTokenSecret
+
+
+
+VaultTokenSecret is the Schema for the vaulttokensecrets API.
+
+
+
+_Appears in:_
+- [VaultTokenSecretList](#vaulttokensecretlist)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `secrets.hashicorp.com/v1beta1` | | |
+| `kind` _string_ | `VaultTokenSecret` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[VaultTokenSecretSpec](#vaulttokensecretspec)_ | | | |
+
+
+#### VaultTokenSecretList
+
+
+
+VaultTokenSecretList contains a list of VaultTokenSecret.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `secrets.hashicorp.com/v1beta1` | | |
+| `kind` _string_ | `VaultTokenSecretList` | | |
+| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `items` _[VaultTokenSecret](#vaulttokensecret) array_ | | | |
+
+
+#### VaultTokenSecretSpec
+
+
+
+VaultTokenSecretSpec defines the desired state of VaultTokenSecret.
+
+
+
+_Appears in:_
+- [VaultTokenSecret](#vaulttokensecret)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `vaultAuthRef` _string_ | VaultAuthRef to the VaultAuth resource, can be prefixed with a namespace,
eg: `namespaceA/vaultAuthRefB`. If no namespace prefix is provided it will default to the
namespace of the VaultAuth CR. If no value is specified for VaultAuthRef the Operator will
default to the `default` VaultAuth, configured in the operator's namespace. | | |
+| `namespace` _string_ | Namespace of the secrets engine mount in Vault. If not set, the namespace that's
part of VaultAuth resource will be inferred. | | |
+| `refreshAfter` _string_ | Mount for the secret in Vault
RefreshAfter a period of time, in duration notation e.g. 30s, 1m, 24h | | Pattern: `^([0-9]+(\.[0-9]+)?(s|m|h))$`
Type: string
|
+| `rolloutRestartTargets` _[RolloutRestartTarget](#rolloutrestarttarget) array_ | RolloutRestartTargets should be configured whenever the application(s) consuming the Vault secret does
not support dynamically reloading a rotated secret.
In that case one, or more RolloutRestartTarget(s) can be configured here. The Operator will
trigger a "rollout-restart" for each target whenever the Vault secret changes between reconciliation events.
All configured targets will be ignored if HMACSecretData is set to false.
See RolloutRestartTarget for more details. | | |
+| `destination` _[Destination](#destination)_ | Destination provides configuration necessary for syncing the Vault secret to Kubernetes. | | |
+| `tokenRole` _string_ | TokenRole is the name of the token role to use when creating the token. | | |
+| `ttl` _string_ | | | |
+| `policies` _string array_ | | | |
+| `noDefaultPolicy` _boolean_ | | | |
+| `displayName` _string_ | | | |
+| `entityAlias` _string_ | | | |
+| `meta` _object (keys:string, values:string)_ | | | |
+| `renewalPercent` _integer_ | RenewalPercent is the percent out of 100 of the lease duration when the
lease is renewed. Defaults to 67 percent plus jitter. | 67 | Maximum: 90
Minimum: 0
|
+| `revoke` _boolean_ | Revoke the existing lease on VDS resource deletion. | | |
+
+
+
+
diff --git a/main.go b/main.go
index 58eb61948..5af788d0f 100644
--- a/main.go
+++ b/main.go
@@ -632,6 +632,18 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "VaultAuthGlobal")
os.Exit(1)
}
+ if err = (&controllers.VaultTokenSecretReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Recorder: mgr.GetEventRecorderFor("VaultTokenSecret"),
+ ClientFactory: clientFactory,
+ SecretsClient: secretsClient,
+ BackOffRegistry: controllers.NewBackOffRegistry(backoffOpts...),
+ GlobalTransformationOptions: globalTransOptions,
+ }).SetupWithManager(mgr, controllerOptions); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "VaultTokenSecret")
+ os.Exit(1)
+ }
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
From f8d325a64d804d5c1a45fe32d23d57fc6152d883 Mon Sep 17 00:00:00 2001
From: Birdie K <5210502+moo-im-a-cow@users.noreply.github.com>
Date: Sun, 4 May 2025 18:17:58 +1000
Subject: [PATCH 2/2] revoke token on deletion
---
controllers/vaulttokensecret_controller.go | 30 ++++++++++++++++++++--
1 file changed, 28 insertions(+), 2 deletions(-)
diff --git a/controllers/vaulttokensecret_controller.go b/controllers/vaulttokensecret_controller.go
index 85744b488..2ce25cda0 100644
--- a/controllers/vaulttokensecret_controller.go
+++ b/controllers/vaulttokensecret_controller.go
@@ -254,10 +254,10 @@ func (r *VaultTokenSecretReconciler) updateStatus(ctx context.Context, o *secret
return err
}
-func (r *VaultTokenSecretReconciler) handleDeletion(ctx context.Context, o client.Object) error {
+func (r *VaultTokenSecretReconciler) handleDeletion(ctx context.Context, o *secretsv1beta1.VaultTokenSecret) error {
logger := log.FromContext(ctx)
logger.Info("deleting")
-
+ r.revokeToken(ctx, o, "")
objKey := client.ObjectKeyFromObject(o)
r.referenceCache.Remove(SecretTransformation, objKey)
r.BackOffRegistry.Delete(objKey)
@@ -304,3 +304,29 @@ func newTokenRequest(s secretsv1beta1.VaultTokenSecretSpec) (vault.WriteRequest,
return Req, nil
}
+
+func (r *VaultTokenSecretReconciler) revokeToken(ctx context.Context, o *secretsv1beta1.VaultTokenSecret, id string) {
+ logger := log.FromContext(ctx)
+ TokenAccessor := id
+ if TokenAccessor == "" {
+ TokenAccessor = o.Status.TokenAccessor
+ }
+
+ logger.Info("Revoking token ", "accessor", TokenAccessor)
+ c, err := r.ClientFactory.Get(ctx, r.Client, o)
+ if err != nil {
+ logger.Error(err, "Failed to get client when revoking token for ", "accessor", TokenAccessor)
+ return
+ }
+ if _, err = c.Write(ctx, vault.NewWriteRequest("auth/token/revoke-accessor", map[string]any{
+ "accessor": TokenAccessor,
+ })); err != nil {
+ msg := "Failed to revoke token"
+ r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretLeaseRevoke, msg+": %s", err)
+ logger.Error(err, "Failed to revoke token ", "id", TokenAccessor)
+ } else {
+ msg := "Token revoked"
+ r.Recorder.Eventf(o, corev1.EventTypeNormal, consts.ReasonSecretLeaseRevoke, msg+": %s", TokenAccessor)
+ logger.Info("Token revoked ", "id", TokenAccessor)
+ }
+}