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) + } +}