Repository: hashicorp/vault-secrets-operator

Issue:
v1.0.0 vso.secrets.hashicorp.com/restartedAt `rolloutRefreshTargets` causing infinite rollout loop on refreshAfter interval using ArgoCD
<!-- Please reserve GitHub issues for bug reports and feature requests.

For questions, the best place to get answers is on our [discussion forum](https://discuss.hashicorp.com/c/vault), as they will get more visibility from experienced users than the issue tracker.

Please note: We take Vault's security and our users' trust very seriously. If you believe you have found a security issue in Vault Secrets Operator, _please responsibly disclose_ by contacting us at [security@hashicorp.com](mailto:security@hashicorp.com).

-->

**Describe the bug**
This issue was previously encountered in #582. That issue became stale, but the suggested resolution was to include `spec.hmacSecretData: true`. Despite including that spec value I show similar behavior after release of `v1.0.0`.


**To Reproduce**
Steps to reproduce the behavior:
1. Deploy an app with this yaml:
```yaml
spec:
  destination:
    create: true
    name: app-secrets
    overwrite: true
  rolloutRestartTargets:
  - kind: Deployment
    name: app
  hmacSecretData: true
  mount: kvv2
  path: app/secrets
  refreshAfter: 120s
  type: kv-v2
  vaultAuthRef: vault-auth
```

2. Watch ArgoCD kick off a deployment at every `spec.refreshAfter` interval. 

**Expected behavior**

**Environment**
* Kubernetes version: 
  * Distribution or cloud vendor (OpenShift, EKS, GKE, AKS, etc.): OpenShift
  * Other configuration options or runtime services (istio, etc.): ArgoCD/ArgoRollouts
* vault-secrets-operator version: v1.0.0

**Additional context**

NOTES: 
- Omission of `spec.refereshAfter` interval causes a new rollout on sub-second intervals.
- Omission of `spec.rolloutRestartTargets` fails to create a rollout when new static secrets versions are synced because `vso.secrets.hashicorp.com/restartedAt` is not updated.
- Inclusion of `spec.hmacSecretData: true` does not force sync to occur as suggested in previous issue #582.



Discussion:
@joshgubler: Even without `spec.rolloutRestartTargets` set, you can watch `status.conditions` and see that the `lastTransitionTime` on both the `type: Healthy` and the `type: RolloutRestart` conditions update every `spec.refreshAfter` interval. This implies that the Operator believes that the Secret values are constantly needing to be synchronized.

With `spec.refreshAfter: 5s`:

`kubectl get vaultstaticsecrets.secrets.hashicorp.com app-secrets -w -o yaml`

<img width="373" height="256" alt="Image" src="https://github.com/user-attachments/assets/63e9555d-ea49-4b13-94b3-88aee7f4cefe" />

<img width="366" height="255" alt="Image" src="https://github.com/user-attachments/assets/90955e5c-8448-411b-8795-65b169c1bfe2" />

And it keeps changing every 5s.@geoffr: We had the constant restarts on deployments both managed by and not by ArgoCD after we upgraded to 1.0.0.  So that seems like that's a red herring.  We had a VSO with a `refreshAfter: 120s` which did the same and that deployment is a standard kubectl applied deployment (i.e. not an ArgoCD Application)@benashz: Hi @rhettjay  , would you mind providing us with the complete VaultStaticSecret's complete `status` output. That should help is to determine next steps. Thanks!@achmadhisyam: Hi All,

I have the same problem, the VSO is automatically updated within the OpenShift. The deployment is triggered to restart unnecessary. The secrets in the Vault is not changed, the OpenShift also nothing changed in it. Below is the VaultStaticSecret

`apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret 
metadata:
  namespace: xxx
  name: xxx
spec:
  vaultAuthRef: xxx
  mount: xxx
  type: kv-v2
  path: xxx
  refreshAfter: 5s
  rolloutRestartTargets:
    - kind: Deployment
      name: xxx
    - kind: Deployment
      name: xxx
  destination:
    create: true
    name: xxx`

Any idea how to downgrade or rollback the VSO to the previous version? Or any other way to mitigate the issue of restarting the deployment unnecessarily?@benashz: @achmadhisyam, sorry to hear that you are having issues with the latest VSO release. We are actively investigating this issue.

You should be able to install 0.10.0 explicitly.

In the meantime, you might want to adjust the VaultStaticSecret to omit the rolloutRestartTargets per: https://developer.hashicorp.com/vault/docs/deploy/kubernetes/vso/api-reference#vaultstaticsecretspec@benashz: @achmadhisyam - would you mind providing us with an example of the VaultStaticSecret resource that is exhibiting the issue?@vertisan: I have the same problem on my local K3s - after upgrade to `v1.0.0`, VSO triggering pods restart over and over without any other changes (also in secrets)... After downgrading to `v0.10.0`, everything is back to normal. 
There are a few examples of my `VaultStaticSecret`:

```yaml
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: admin-credentials
spec:
  vaultAuthRef: vso-auth
  mount: services
  type: kv-v2
  path: grafana/secrets/admin-credentials
  refreshAfter: 30s
  destination:
    name: grafana-admin-credentials
    create: true
  rolloutRestartTargets:
    - name: grafana
      kind: Deployment
```

```yaml
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: repository-k8s-crimson
spec:
  vaultAuthRef: vso-auth
  mount: services
  type: kv-v2
  path: argo-cd/secrets/repositories/k8s-crimson
  refreshAfter: 10s
  destination:
    name: repository-k8s-crimson
    create: true
    labels:
      argocd.argoproj.io/secret-type: repo-creds
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: argocd-secret
spec:
  vaultAuthRef: vso-auth
  mount: services
  type: kv-v2
  path: argo-cd/secrets/config
  refreshAfter: 10s
  destination:
    name: argocd-secret
    create: true
    overwrite: true
  rolloutRestartTargets:
    - kind: Deployment
      name: argo-cd-argocd-server
    - kind: Deployment
      name: argo-cd-argocd-dex-server
    - kind: Deployment
      name: argo-cd-argocd-applicationset-controller
```

```yaml
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: cloudflare-xxx-config
spec:
  vaultAuthRef: vso-auth
  mount: services
  type: kv-v2
  path: xxx
  refreshAfter: 30s
  destination:
    create: true
    name: cloudflare-xxx-config
    transformation:
      excludeRaw: true
      excludes:
        - .*
      templates:
        TOKEN:
          text: |
            {{- printf "%s" (get .Secrets "token") -}}
  rolloutRestartTargets:
    - name: cloudflare-xxx
      kind: Deployment
```@achmadhisyam: Hi @vertisan , could you please share the step to downgrade the VSO to previous version? Thanks@vertisan: > Hi [@vertisan](https://github.com/vertisan) , could you please share the step to downgrade the VSO to previous version? Thanks

I'm using an official Helm for VSO, so I just reverted the Chart's version to `0.10.0`, and re-deployed it. Nothing more was required (in my case ofc). After this, everything stopped restarting.@benashz: We have identified the issue and are working on a fix. We should be doing a patch release shortly. Stay tuned.@achmadhisyam: > > Hi [@vertisan](https://github.com/vertisan) , could you please share the step to downgrade the VSO to previous version? Thanks
> 
> I'm using an official Helm for VSO, so I just reverted the Chart's version to `0.10.0`, and re-deployed it. Nothing more was required (in my case ofc). After this, everything stopped restarting.

So, you are using helm rollback command isn't it?@vertisan: > > > Hi [@vertisan](https://github.com/vertisan) , could you please share the step to downgrade the VSO to previous version? Thanks
> > 
> > 
> > I'm using an official Helm for VSO, so I just reverted the Chart's version to `0.10.0`, and re-deployed it. Nothing more was required (in my case ofc). After this, everything stopped restarting.
> 
> So, you are using helm rollback command isn't it?

Nope, I'm using ArgoCD so he's doing all of these things for me :) I have just changed version as I mentioned, committed it into the repository, and manually refreshed the Application in Argo (to make a faster refresh and re-deploy).@rhettjay: @benashz it sounds like there's been significant progress. Thanks for the swift response. 
Here's the status output (with some admitted redactions, like last applied configuration) if it's at all helpful.

```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: >
      {"apiVersion":"secrets.hashicorp.com/v1beta1","kind":"VaultStaticSecret","metadata":{"annotations":{""},"labels":{"app.kubernetes.io/component":"app","app.kubernetes.io/instance":"app-env","app.kubernetes.io/managed-by":"argocd","app.kubernetes.io/name":"app","app.kubernetes.io/part-of":"app","argocd.argoproj.io/instance":"app-env"},"name":"app-env-vars","namespace":"app-env"},"spec":{"destination":{"create":true,"name":"app-env-vars","overwrite":true},"hmacSecretData":true,"mount":"kvv2","path":"app/env/env-vars","rolloutRestartTargets":[{"kind":"Deployment","name":"app"}],"type":"kv-v2","vaultAuthRef":"vault-auth"}}
  creationTimestamp: '2025-09-25T21:41:13Z'
  finalizers:
    - vaultstaticsecret.secrets.hashicorp.com/finalizer
  generation: 4
  labels:
    app.kubernetes.io/component: app
    app.kubernetes.io/instance: app-env
    app.kubernetes.io/managed-by: argocd
    app.kubernetes.io/name: app
    app.kubernetes.io/part-of: app
    argocd.argoproj.io/instance: app-env
  name: app-env-vars
  namespace: app-env
  resourceVersion: '830874587'
  uid: 1v787172-1b97-4d83-8721-d445f8d291
spec:
  destination:
    create: true
    name: app-env-vars
    overwrite: true
    transformation: {}
  hmacSecretData: true
  mount: kvv2
  path: app/env/env-vars
  rolloutRestartTargets:
    - kind: Deployment
      name: app
  type: kv-v2
  vaultAuthRef: vault-auth
status:
  conditions:
    - lastTransitionTime: '2025-09-25T22:17:18Z'
      message: Secret synced, horizon=49.639311009s
      observedGeneration: 4
      reason: Synced
      status: 'True'
      type: SecretSynced
    - lastTransitionTime: '2025-09-26T16:19:45Z'
      message: Rollout restart triggered
      observedGeneration: 4
      reason: RolloutRestartTriggered
      status: 'True'
      type: RolloutRestart
    - lastTransitionTime: '2025-09-26T16:19:45Z'
      message: VaultStaticSecretHealthy
      observedGeneration: 4
      reason: Healthy
      status: 'True'
      type: Healthy
  lastGeneration: 4
  secretMAC: MAC=
```@joshgubler: 
>         Rollout restart trigger failed
@rhettjay I think this is on an application that you have paused. Hence the failed restart. I just don't want the team to waste time following that red herring.

@rhettjay: > > ```
> >     Rollout restart trigger failed
> > ```
> 
> [@rhettjay](https://github.com/rhettjay) I think this is on an application that you have paused. Hence the failed restart. I just don't want the team to waste time following that red herring.

Correct and fair point. I've edited the original post to include an example of a deployment that is not paused.@benashz: Fix is released in [v1.0.1](https://github.com/hashicorp/vault-secrets-operator/releases/tag/v1.0.1)

OLM packaging is in progress for OpenShift.@joshgubler: I've confirmed that v1.0.1 fixes the issue for us. Thank you for jumping on this so fast!

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index d6fd3e9..f45322e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,6 +30,7 @@ COPY internal/ internal/
 COPY template/ template/
 COPY utils/ utils/
 COPY vault/ vault/
+COPY options/ options/
 
 # These flags gets redynamically computed on each `docker build` invocation, keep this under `go mod download` and friends
 # so it doesn't unnecessarily bust the Docker cache.
diff --git a/api/v1beta1/csisecrets_types.go b/api/v1beta1/csisecrets_types.go
index fc1c5d7..7225063 100644
--- a/api/v1beta1/csisecrets_types.go
+++ b/api/v1beta1/csisecrets_types.go
@@ -101,8 +101,9 @@ type AccessControl struct {
 
 // CSISecretsStatus defines the observed state of CSISecrets
 type CSISecretsStatus struct {
-	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
-	// Important: Run "make" to regenerate code after modifying this file
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/hcpauth_types.go b/api/v1beta1/hcpauth_types.go
index b2d671f..20fb7d7 100644
--- a/api/v1beta1/hcpauth_types.go
+++ b/api/v1beta1/hcpauth_types.go
@@ -51,6 +51,9 @@ type HCPAuthStatus struct {
 	// Valid auth mechanism.
 	Valid *bool  `json:"valid"`
 	Error string `json:"error"`
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/hcpvaultsecretsapp_types.go b/api/v1beta1/hcpvaultsecretsapp_types.go
index 294f11b..b9e185e 100644
--- a/api/v1beta1/hcpvaultsecretsapp_types.go
+++ b/api/v1beta1/hcpvaultsecretsapp_types.go
@@ -81,6 +81,9 @@ type HCPVaultSecretsAppStatus struct {
 	// DynamicSecrets lists the last observed state of any dynamic secrets
 	// within the HCP Vault Secrets App
 	DynamicSecrets []HVSDynamicStatus `json:"dynamicSecrets,omitempty"`
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/vaultauth_types.go b/api/v1beta1/vaultauth_types.go
index a6fcd0b..144d79a 100644
--- a/api/v1beta1/vaultauth_types.go
+++ b/api/v1beta1/vaultauth_types.go
@@ -432,10 +432,14 @@ type VaultAuthSpec struct {
 // VaultAuthStatus defines the observed state of VaultAuth
 type VaultAuthStatus struct {
 	// Valid auth mechanism.
-	Valid      *bool              `json:"valid,omitempty"`
-	Error      string             `json:"error,omitempty"`
+	Valid *bool `json:"valid,omitempty"`
+	// Error is a human-readable error message indicating why the VaultAuth is invalid.
+	Error string `json:"error,omitempty"`
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
 	Conditions []metav1.Condition `json:"conditions,omitempty"`
-	SpecHash   string             `json:"specHash,omitempty"`
+	// SpecHash is a SHA256 hash of the spec, used to determine if the spec has changed.
+	SpecHash string `json:"specHash,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/vaultconnection_types.go b/api/v1beta1/vaultconnection_types.go
index 45ab43b..db63b85 100644
--- a/api/v1beta1/vaultconnection_types.go
+++ b/api/v1beta1/vaultconnection_types.go
@@ -31,6 +31,9 @@ type VaultConnectionSpec struct {
 type VaultConnectionStatus struct {
 	// Valid auth mechanism.
 	Valid *bool `json:"valid"`
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/vaultdynamicsecret_types.go b/api/v1beta1/vaultdynamicsecret_types.go
index 085e17b..4264e32 100644
--- a/api/v1beta1/vaultdynamicsecret_types.go
+++ b/api/v1beta1/vaultdynamicsecret_types.go
@@ -97,6 +97,9 @@ type VaultDynamicSecretStatus struct {
 	// VaultClientMeta contains the status of the Vault client and is used during
 	// resource reconciliation.
 	VaultClientMeta VaultClientMeta `json:"vaultClientMeta,omitempty"`
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 type VaultSecretLease struct {
diff --git a/api/v1beta1/vaultpkisecret_types.go b/api/v1beta1/vaultpkisecret_types.go
index e0ae088..a83de9f 100644
--- a/api/v1beta1/vaultpkisecret_types.go
+++ b/api/v1beta1/vaultpkisecret_types.go
@@ -138,6 +138,9 @@ type VaultPKISecretStatus struct {
 	SecretMAC string `json:"secretMAC,omitempty"`
 	Valid     *bool  `json:"valid"`
 	Error     string `json:"error"`
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/vaultstaticsecret_types.go b/api/v1beta1/vaultstaticsecret_types.go
index 7083195..3c3f071 100644
--- a/api/v1beta1/vaultstaticsecret_types.go
+++ b/api/v1beta1/vaultstaticsecret_types.go
@@ -88,6 +88,9 @@ type VaultStaticSecretStatus struct {
 	// The SecretMac is also used to detect drift in the Destination Secret's Data.
 	// If drift is detected the data will be synced to the Destination.
 	SecretMAC string `json:"secretMAC,omitempty"`
+	// Conditions hold information that can be used by other apps to determine the
+	// health of the resource instance.
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index e9eaa77..03a1675 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -50,7 +50,7 @@ func (in *CSISecrets) DeepCopyInto(out *CSISecrets) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
-	out.Status = in.Status
+	in.Status.DeepCopyInto(&out.Status)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISecrets.
@@ -129,6 +129,13 @@ func (in *CSISecretsSpec) DeepCopy() *CSISecretsSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CSISecretsStatus) DeepCopyInto(out *CSISecretsStatus) {
 	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSISecretsStatus.
@@ -313,6 +320,13 @@ func (in *HCPAuthStatus) DeepCopyInto(out *HCPAuthStatus) {
 		*out = new(bool)
 		**out = **in
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPAuthStatus.
@@ -418,6 +432,13 @@ func (in *HCPVaultSecretsAppStatus) DeepCopyInto(out *HCPVaultSecretsAppStatus)
 		*out = make([]HVSDynamicStatus, len(*in))
 		copy(*out, *in)
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HCPVaultSecretsAppStatus.
@@ -1503,6 +1524,13 @@ func (in *VaultConnectionStatus) DeepCopyInto(out *VaultConnectionStatus) {
 		*out = new(bool)
 		**out = **in
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultConnectionStatus.
@@ -1521,7 +1549,7 @@ func (in *VaultDynamicSecret) DeepCopyInto(out *VaultDynamicSecret) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
-	out.Status = in.Status
+	in.Status.DeepCopyInto(&out.Status)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultDynamicSecret.
@@ -1608,6 +1636,13 @@ func (in *VaultDynamicSecretStatus) DeepCopyInto(out *VaultDynamicSecretStatus)
 	out.SecretLease = in.SecretLease
 	out.StaticCredsMetaData = in.StaticCredsMetaData
 	out.VaultClientMeta = in.VaultClientMeta
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultDynamicSecretStatus.
@@ -1733,6 +1768,13 @@ func (in *VaultPKISecretStatus) DeepCopyInto(out *VaultPKISecretStatus) {
 		*out = new(bool)
 		**out = **in
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultPKISecretStatus.
@@ -1781,7 +1823,7 @@ func (in *VaultStaticSecret) DeepCopyInto(out *VaultStaticSecret) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
-	out.Status = in.Status
+	in.Status.DeepCopyInto(&out.Status)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultStaticSecret.
@@ -1905,6 +1947,13 @@ func (in *VaultStaticSecretSpec) DeepCopy() *VaultStaticSecretSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VaultStaticSecretStatus) DeepCopyInto(out *VaultStaticSecretStatus) {
 	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultStaticSecretStatus.
diff --git a/chart/crds/secrets.hashicorp.com_csisecrets.yaml b/chart/crds/secrets.hashicorp.com_csisecrets.yaml
index 3d6b722..fde2450 100644
--- a/chart/crds/secrets.hashicorp.com_csisecrets.yaml
+++ b/chart/crds/secrets.hashicorp.com_csisecrets.yaml
@@ -553,6 +553,66 @@ spec:
             type: object
           status:
             description: CSISecretsStatus defines the observed state of CSISecrets
+            properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
             type: object
         type: object
     served: true
diff --git a/chart/crds/secrets.hashicorp.com_hcpauths.yaml b/chart/crds/secrets.hashicorp.com_hcpauths.yaml
index 8ce5039..2aa22bb 100644
--- a/chart/crds/secrets.hashicorp.com_hcpauths.yaml
+++ b/chart/crds/secrets.hashicorp.com_hcpauths.yaml
@@ -94,6 +94,65 @@ spec:
           status:
             description: HCPAuthStatus defines the observed state of HCPAuth
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               error:
                 type: string
               valid:
diff --git a/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml b/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml
index f896fd0..4640386 100644
--- a/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml
+++ b/chart/crds/secrets.hashicorp.com_hcpvaultsecretsapps.yaml
@@ -266,6 +266,65 @@ spec:
           status:
             description: HCPVaultSecretsAppStatus defines the observed state of HCPVaultSecretsApp
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               dynamicSecrets:
                 description: |-
                   DynamicSecrets lists the last observed state of any dynamic secrets
diff --git a/chart/crds/secrets.hashicorp.com_vaultauths.yaml b/chart/crds/secrets.hashicorp.com_vaultauths.yaml
index 3972c24..846be59 100644
--- a/chart/crds/secrets.hashicorp.com_vaultauths.yaml
+++ b/chart/crds/secrets.hashicorp.com_vaultauths.yaml
@@ -331,6 +331,9 @@ spec:
             description: VaultAuthStatus defines the observed state of VaultAuth
             properties:
               conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
                 items:
                   description: Condition contains details for one aspect of the current
                     state of this API Resource.
@@ -387,8 +390,12 @@ spec:
                   type: object
                 type: array
               error:
+                description: Error is a human-readable error message indicating why
+                  the VaultAuth is invalid.
                 type: string
               specHash:
+                description: SpecHash is a SHA256 hash of the spec, used to determine
+                  if the spec has changed.
                 type: string
               valid:
                 description: Valid auth mechanism.
diff --git a/chart/crds/secrets.hashicorp.com_vaultconnections.yaml b/chart/crds/secrets.hashicorp.com_vaultconnections.yaml
index 084191a..74baa48 100644
--- a/chart/crds/secrets.hashicorp.com_vaultconnections.yaml
+++ b/chart/crds/secrets.hashicorp.com_vaultconnections.yaml
@@ -74,6 +74,65 @@ spec:
           status:
             description: VaultConnectionStatus defines the observed state of VaultConnection
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               valid:
                 description: Valid auth mechanism.
                 type: boolean
diff --git a/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml b/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml
index 2bb1877..db28a0a 100644
--- a/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml
+++ b/chart/crds/secrets.hashicorp.com_vaultdynamicsecrets.yaml
@@ -305,6 +305,65 @@ spec:
           status:
             description: VaultDynamicSecretStatus defines the observed state of VaultDynamicSecret
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               lastGeneration:
                 description: LastGeneration is the Generation of the last reconciled
                   resource.
diff --git a/chart/crds/secrets.hashicorp.com_vaultpkisecrets.yaml b/chart/crds/secrets.hashicorp.com_vaultpkisecrets.yaml
index 552786e..45ed08a 100644
--- a/chart/crds/secrets.hashicorp.com_vaultpkisecrets.yaml
+++ b/chart/crds/secrets.hashicorp.com_vaultpkisecrets.yaml
@@ -345,6 +345,65 @@ spec:
           status:
             description: VaultPKISecretStatus defines the observed state of VaultPKISecret
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               error:
                 type: string
               expiration:
diff --git a/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml b/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml
index 5d22e13..187232b 100644
--- a/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml
+++ b/chart/crds/secrets.hashicorp.com_vaultstaticsecrets.yaml
@@ -288,6 +288,65 @@ spec:
           status:
             description: VaultStaticSecretStatus defines the observed state of VaultStaticSecret
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               lastGeneration:
                 description: LastGeneration is the Generation of the last reconciled
                   resource.
diff --git a/config/crd/bases/secrets.hashicorp.com_csisecrets.yaml b/config/crd/bases/secrets.hashicorp.com_csisecrets.yaml
index 3d6b722..fde2450 100644
--- a/config/crd/bases/secrets.hashicorp.com_csisecrets.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_csisecrets.yaml
@@ -553,6 +553,66 @@ spec:
             type: object
           status:
             description: CSISecretsStatus defines the observed state of CSISecrets
+            properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
             type: object
         type: object
     served: true
diff --git a/config/crd/bases/secrets.hashicorp.com_hcpauths.yaml b/config/crd/bases/secrets.hashicorp.com_hcpauths.yaml
index 8ce5039..2aa22bb 100644
--- a/config/crd/bases/secrets.hashicorp.com_hcpauths.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_hcpauths.yaml
@@ -94,6 +94,65 @@ spec:
           status:
             description: HCPAuthStatus defines the observed state of HCPAuth
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               error:
                 type: string
               valid:
diff --git a/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml b/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml
index f896fd0..4640386 100644
--- a/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_hcpvaultsecretsapps.yaml
@@ -266,6 +266,65 @@ spec:
           status:
             description: HCPVaultSecretsAppStatus defines the observed state of HCPVaultSecretsApp
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               dynamicSecrets:
                 description: |-
                   DynamicSecrets lists the last observed state of any dynamic secrets
diff --git a/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml b/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml
index 3972c24..846be59 100644
--- a/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_vaultauths.yaml
@@ -331,6 +331,9 @@ spec:
             description: VaultAuthStatus defines the observed state of VaultAuth
             properties:
               conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
                 items:
                   description: Condition contains details for one aspect of the current
                     state of this API Resource.
@@ -387,8 +390,12 @@ spec:
                   type: object
                 type: array
               error:
+                description: Error is a human-readable error message indicating why
+                  the VaultAuth is invalid.
                 type: string
               specHash:
+                description: SpecHash is a SHA256 hash of the spec, used to determine
+                  if the spec has changed.
                 type: string
               valid:
                 description: Valid auth mechanism.
diff --git a/config/crd/bases/secrets.hashicorp.com_vaultconnections.yaml b/config/crd/bases/secrets.hashicorp.com_vaultconnections.yaml
index 084191a..74baa48 100644
--- a/config/crd/bases/secrets.hashicorp.com_vaultconnections.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_vaultconnections.yaml
@@ -74,6 +74,65 @@ spec:
           status:
             description: VaultConnectionStatus defines the observed state of VaultConnection
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               valid:
                 description: Valid auth mechanism.
                 type: boolean
diff --git a/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml b/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml
index 2bb1877..db28a0a 100644
--- a/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_vaultdynamicsecrets.yaml
@@ -305,6 +305,65 @@ spec:
           status:
             description: VaultDynamicSecretStatus defines the observed state of VaultDynamicSecret
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               lastGeneration:
                 description: LastGeneration is the Generation of the last reconciled
                   resource.
diff --git a/config/crd/bases/secrets.hashicorp.com_vaultpkisecrets.yaml b/config/crd/bases/secrets.hashicorp.com_vaultpkisecrets.yaml
index 552786e..45ed08a 100644
--- a/config/crd/bases/secrets.hashicorp.com_vaultpkisecrets.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_vaultpkisecrets.yaml
@@ -345,6 +345,65 @@ spec:
           status:
             description: VaultPKISecretStatus defines the observed state of VaultPKISecret
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               error:
                 type: string
               expiration:
diff --git a/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml b/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml
index 5d22e13..187232b 100644
--- a/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml
+++ b/config/crd/bases/secrets.hashicorp.com_vaultstaticsecrets.yaml
@@ -288,6 +288,65 @@ spec:
           status:
             description: VaultStaticSecretStatus defines the observed state of VaultStaticSecret
             properties:
+              conditions:
+                description: |-
+                  Conditions hold information that can be used by other apps to determine the
+                  health of the resource instance.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
               lastGeneration:
                 description: LastGeneration is the Generation of the last reconciled
                   resource.
diff --git a/consts/reasons.go b/consts/reasons.go
index b2cf47e..8e0cbfc 100644
--- a/consts/reasons.go
+++ b/consts/reasons.go
@@ -4,39 +4,41 @@
 package consts
 
 const (
-	ReasonAccepted                   = "Accepted"
-	ReasonInvalidConfiguration       = "InvalidConfiguration"
-	ReasonInvalidResourceRef         = "InvalidResourceRef"
-	ReasonK8sClientError             = "K8sClientError"
-	ReasonRolloutRestartFailed       = "RolloutRestartFailed"
-	ReasonRolloutRestartTriggered    = "RolloutRestartTriggered"
-	ReasonRolloutRestartUnsupported  = "RolloutRestartUnsupported"
-	ReasonSecretLeaseRenewal         = "SecretLeaseRenewal"
-	ReasonSecretLeaseRevoke          = "SecretLeaseRevoke"
-	ReasonSecretLeaseRenewalError    = "SecretLeaseRenewalError"
-	ReasonSecretRotated              = "SecretRotated"
-	ReasonSecretSync                 = "SecretSync"
-	ReasonSecretSyncError            = "SecretSyncError"
-	ReasonSecretSynced               = "SecretSynced"
-	ReasonStatusUpdateError          = "StatusUpdateError"
-	ReasonUnrecoverable              = "Unrecoverable"
-	ReasonVaultClientConfigError     = "VaultClientConfigError"
-	ReasonHVSClientConfigError       = "HVSClientConfigError"
-	ReasonVaultClientError           = "VaultClientError"
-	ReasonVaultStaticSecret          = "VaultStaticSecretError"
-	ReasonHVSSecret                  = "HVSSecretError"
-	ReasonSecretDataDrift            = "SecretDataDrift"
-	ReasonInexistentDestination      = "InexistentDestination"
-	ReasonResourceUpdated            = "ResourceUpdated"
-	ReasonInitialSync                = "InitialSync"
-	ReasonInRenewalWindow            = "InRenewalWindow"
-	ReasonHMACDataError              = "HMACDataError"
-	ReasonCertificateRevocationError = "CertificateRevocationError"
-	ReasonTransformationError        = "TransformationError"
-	ReasonSecretDataBuilderError     = "SecretDataBuilderError"
-	ReasonForceSync                  = "ForceSync"
-	ReasonVaultTokenRotated          = "VaultTokenRotated"
-	ReasonVaultClientConfigChanged   = "VaultClientConfigChanged"
-	ReasonEventWatcherError          = "EventWatcherError"
-	ReasonEventWatcherStarted        = "EventWatcherStarted"
+	ReasonAccepted                      = "Accepted"
+	ReasonInvalidConfiguration          = "InvalidConfiguration"
+	ReasonInvalidResourceRef            = "InvalidResourceRef"
+	ReasonK8sClientError                = "K8sClientError"
+	ReasonRolloutRestartFailed          = "RolloutRestartFailed"
+	ReasonRolloutRestartTriggered       = "RolloutRestartTriggered"
+	ReasonRolloutRestartTriggeredFailed = "RolloutRestartTriggeredFailed"
+	ReasonRolloutRestartUnsupported     = "RolloutRestartUnsupported"
+	ReasonSecretLeaseRenewal            = "SecretLeaseRenewal"
+	ReasonSecretLeaseRevoke             = "SecretLeaseRevoke"
+	ReasonSecretLeaseRenewalError       = "SecretLeaseRenewalError"
+	ReasonSecretRotated                 = "SecretRotated"
+	ReasonSecretSync                    = "SecretSync"
+	ReasonSecretSyncError               = "SecretSyncError"
+	ReasonSecretSynced                  = "SecretSynced"
+	ReasonStatusUpdateError             = "StatusUpdateError"
+	ReasonUnrecoverable                 = "Unrecoverable"
+	ReasonVaultClientConfigError        = "VaultClientConfigError"
+	ReasonHVSClientConfigError          = "HVSClientConfigError"
+	ReasonVaultClientError              = "VaultClientError"
+	ReasonVaultStaticSecret             = "VaultStaticSecretError"
+	ReasonHVSSecret                     = "HVSSecretError"
+	ReasonSecretDataDrift               = "SecretDataDrift"
+	ReasonInexistentDestination         = "InexistentDestination"
+	ReasonResourceUpdated               = "ResourceUpdated"
+	ReasonInitialSync                   = "InitialSync"
+	ReasonInRenewalWindow               = "InRenewalWindow"
+	ReasonHMACDataError                 = "HMACDataError"
+	ReasonCertificateRevocationError    = "CertificateRevocationError"
+	ReasonTransformationError           = "TransformationError"
+	ReasonSecretDataBuilderError        = "SecretDataBuilderError"
+	ReasonForceSync                     = "ForceSync"
+	ReasonVaultTokenRotated             = "VaultTokenRotated"
+	ReasonVaultClientConfigChanged      = "VaultClientConfigChanged"
+	ReasonEventWatcherError             = "EventWatcherError"
+	ReasonEventWatcherStarted           = "EventWatcherStarted"
+	ReasonResourceValidation            = "ResourceValidation"
 )
diff --git a/consts/types.go b/consts/types.go
new file mode 100644
index 0000000..44f9c84
--- /dev/null
+++ b/consts/types.go
@@ -0,0 +1,11 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: BUSL-1.1
+
+package consts
+
+var (
+	TypeResourceValidation = "ResourceValidation"
+	TypeRolloutRestart     = "RolloutRestart"
+	TypeSecretSynced       = "SecretSynced"
+	TypeLeaseRenewal       = "LeaseRenewal"
+)
diff --git a/controllers/common.go b/controllers/common.go
index 920f245..6818367 100644
--- a/controllers/common.go
+++ b/controllers/common.go
@@ -277,12 +277,15 @@ func waitForStoppedCh(ctx context.Context, stoppedCh chan struct{}) error {
 // changed. It will also append new conditions to the existing conditions. All
 // updates are deduplicated based on their type and reason.
 func updateConditions(current []metav1.Condition, updates ...metav1.Condition) []metav1.Condition {
+	if len(updates) == 0 {
+		return current
+	}
+
 	seen := make(map[string]bool)
 	var ret []metav1.Condition
+
 	for _, newCond := range updates {
-		// we key conditions on their type and reason
-		// e.g: type=VaultAuthGlobal reason=Available, ...
-		key := fmt.Sprintf("%s/%s", newCond.Type, newCond.Reason)
+		key := newCond.Type
 		if seen[key] {
 			// drop duplicate conditions
 			continue
@@ -307,6 +310,7 @@ func updateConditions(current []metav1.Condition, updates ...metav1.Condition) [
 				break
 			}
 		}
+
 		if !updated {
 			if newCond.LastTransitionTime.IsZero() {
 				newCond.LastTransitionTime = metav1.NewTime(nowFunc())
@@ -314,5 +318,37 @@ func updateConditions(current []metav1.Condition, updates ...metav1.Condition) [
 			ret = append(ret, newCond)
 		}
 	}
-	return ret
+
+	var orig []metav1.Condition
+	for _, cond := range current {
+		if _, ok := seen[cond.Type]; ok {
+			continue
+		}
+		orig = append(orig, cond)
+	}
+
+	return append(orig, ret...)
+}
+
+func newConditionNow(o client.Object, typ, reason string, status metav1.ConditionStatus,
+	msgFmt string, msgArgs ...any,
+) metav1.Condition {
+	return newCondition(o, typ, reason, status, time.Now(), msgFmt, msgArgs...)
+}
+
+func newCondition(o client.Object, typ, reason string, status metav1.ConditionStatus,
+	t time.Time, msgFmt string, msgArgs ...any,
+) metav1.Condition {
+	return metav1.Condition{
+		ObservedGeneration: o.GetGeneration(),
+		LastTransitionTime: metav1.NewTime(t),
+		Type:               typ,
+		Reason:             reason,
+		Status:             status,
+		Message:            fmt.Sprintf(msgFmt, msgArgs...),
+	}
+}
+
+func newSyncCondition(o client.Object, status metav1.ConditionStatus, msgFmt string, msgArgs ...any) metav1.Condition {
+	return newConditionNow(o, consts.TypeSecretSynced, "Synced", status, msgFmt, msgArgs...)
 }
diff --git a/controllers/vaultconnection_controller.go b/controllers/vaultconnection_controller.go
index 91846a6..746d078 100644
--- a/controllers/vaultconnection_controller.go
+++ b/controllers/vaultconnection_controller.go
@@ -6,6 +6,10 @@ package controllers
 import (
 	"context"
 	"errors"
+	"fmt"
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -76,10 +80,21 @@ func (r *VaultConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Requ
 	}
 
 	var errs error
+	o.Status.Conditions = []metav1.Condition{}
 	vaultClient, err := vault.MakeVaultClient(ctx, vaultConfig, r.Client)
 	if err != nil {
 		logger.Error(err, "Failed to construct Vault client")
-		r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonVaultClientError, "Failed to construct Vault client: %s", err)
+		r.Recorder.Eventf(o, corev1.EventTypeWarning,
+			consts.ReasonVaultClientError,
+			"Failed to construct Vault client: %s", err)
+
+		o.Status.Conditions = append(o.Status.Conditions,
+			newConditionNow(o, "VaultClient",
+				consts.ReasonVaultClientConfigError,
+				metav1.ConditionFalse,
+				"Failed to setup Vault client, address=%s, err=%s",
+				o.Spec.Address, err),
+		)
 
 		errs = errors.Join(errs, err)
 	}
@@ -87,10 +102,24 @@ func (r *VaultConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Requ
 	if vaultClient != nil {
 		if _, err := vaultClient.Sys().SealStatusWithContext(ctx); err != nil {
 			logger.Error(err, "Failed to check Vault seal status, requeuing")
-			r.Recorder.Eventf(o, corev1.EventTypeWarning, "VaultClientError", "Failed to check Vault seal status: %s", err)
+			r.Recorder.Eventf(o, corev1.EventTypeWarning,
+				"VaultClientError",
+				"Failed to check Vault seal status: %s", err)
 			errs = errors.Join(errs, err)
+
+			o.Status.Conditions = append(o.Status.Conditions,
+				newConditionNow(o, "VaultPing",
+					consts.ReasonInvalidConfiguration,
+					metav1.ConditionFalse, "Vault ping failed, address=%s, errs=%s",
+					o.Spec.Address, err),
+			)
 		} else {
 			o.Status.Valid = ptr.To(true)
+			o.Status.Conditions = append(o.Status.Conditions,
+				newConditionNow(o, "VaultPing", consts.ReasonAccepted,
+					metav1.ConditionTrue, "Vault ping, address=%s",
+					o.Spec.Address),
+			)
 		}
 	}
 
@@ -108,16 +137,33 @@ func (r *VaultConnectionReconciler) Reconcile(ctx context.Context, req ctrl.Requ
 		errs = errors.Join(errs, err)
 	}
 
-	if err := r.updateStatus(ctx, o); err != nil {
-		errs = errors.Join(errs, err)
+	var horizon time.Duration
+
+	isValid := ptr.Deref(o.Status.Valid, false)
+	if !isValid {
+		errs = errors.Join(errs, fmt.Errorf("invalid VaultConnection %s", client.ObjectKeyFromObject(o)))
 	}
 
 	if errs != nil {
-		return ctrl.Result{}, errs
+		logger.Error(errs, "Resource validation failed")
+		r.Recorder.Event(o, corev1.EventTypeWarning, consts.ReasonInvalidConfiguration, "VaultConnection invalid")
+		horizon = computeHorizonWithJitter(requeueDurationOnError)
+		o.Status.Conditions = append(o.Status.Conditions,
+			newConditionNow(o, consts.TypeResourceValidation, consts.ReasonInvalidConfiguration,
+				metav1.ConditionFalse, "Failed to validate resource, address=%s, errs=%s",
+				o.Spec.Address, errs),
+		)
+	} else {
+		r.Recorder.Event(o, corev1.EventTypeNormal, consts.ReasonAccepted, "VaultConnection accepted")
 	}
 
-	r.Recorder.Event(o, corev1.EventTypeNormal, consts.ReasonAccepted, "VaultConnection accepted")
-	return ctrl.Result{}, nil
+	if err := r.updateStatus(ctx, o); err != nil {
+		return ctrl.Result{}, errors.Join(errs, err)
+	}
+
+	return ctrl.Result{
+		RequeueAfter: horizon,
+	}, nil
 }
 
 func (r *VaultConnectionReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultConnection) error {
diff --git a/controllers/vaultdynamicsecret_controller.go b/controllers/vaultdynamicsecret_controller.go
index 8b08765..7ba2527 100644
--- a/controllers/vaultdynamicsecret_controller.go
+++ b/controllers/vaultdynamicsecret_controller.go
@@ -178,12 +178,21 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 		syncReason = consts.ReasonVaultTokenRotated
 	}
 
+	var conditions []metav1.Condition
+
 	doSync := syncReason != ""
 	leaseID := o.Status.SecretLease.ID
 	if !doSync && r.runtimePodUID != "" && r.runtimePodUID != o.Status.LastRuntimePodUID {
 		// don't take part in the thundering herd on start up,
 		// and the lease is still within the renewal window.
 		horizon, inWindow := computeRelativeHorizonWithJitter(o, time.Second*1)
+		if len(o.Status.Conditions) == 0 {
+			// add a sync condition to the status if it does not exist.
+			conditions = append(conditions,
+				newSyncCondition(o, metav1.ConditionTrue,
+					"Updated after transitioning to a new leader/pod"),
+			)
+		}
 		logger.Info("Restart check",
 			"inWindow", inWindow,
 			"horizon", horizon,
@@ -194,7 +203,14 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 				r.Recorder.Eventf(o, corev1.EventTypeNormal, consts.ReasonSecretLeaseRenewal,
 					"Not in renewal window after transitioning to a new leader/pod, lease_id=%s, horizon=%s",
 					leaseID, horizon)
-				if err := r.updateStatus(ctx, o); err != nil {
+				conditions = append(conditions,
+					newConditionNow(o,
+						consts.TypeLeaseRenewal,
+						consts.ReasonSecretLeaseRenewal,
+						metav1.ConditionFalse,
+						"Not in rotation period after transitioning to a new leader/pod, horizon=%s", horizon),
+				)
+				if err := r.updateStatus(ctx, o, conditions...); err != nil {
 					return ctrl.Result{}, err
 				}
 				return ctrl.Result{RequeueAfter: horizon}, nil
@@ -205,7 +221,15 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 			r.Recorder.Eventf(o, corev1.EventTypeNormal, consts.ReasonSecretLeaseRenewal,
 				"In rotation period after transitioning to a new leader/pod, lease_id=%s, horizon=%s",
 				leaseID, horizon)
-			if err := r.updateStatus(ctx, o); err != nil {
+
+			conditions = append(conditions,
+				newConditionNow(o,
+					consts.TypeLeaseRenewal,
+					consts.ReasonSecretLeaseRenewal,
+					metav1.ConditionTrue,
+					"In rotation period after transitioning to a new leader/pod, horizon=%s", horizon),
+			)
+			if err := r.updateStatus(ctx, o, conditions...); err != nil {
 				return ctrl.Result{}, err
 			}
 			return ctrl.Result{RequeueAfter: horizon}, nil
@@ -213,6 +237,13 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 	}
 
 	if !doSync && r.isRenewableLease(&o.Status.SecretLease, o, true) && !o.Spec.AllowStaticCreds && leaseID != "" {
+		if len(o.Status.Conditions) == 0 {
+			// add a sync condition to the status if it does not exist.
+			conditions = append(conditions,
+				newSyncCondition(o, metav1.ConditionTrue,
+					"Updated for lease renewal"),
+			)
+		}
 		// Renew the lease and return from Reconcile if the lease is successfully renewed.
 		if secretLease, err := r.renewLease(ctx, vClient, o); err == nil {
 			if !r.isRenewableLease(secretLease, o, false) {
@@ -225,11 +256,18 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 				r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretLeaseRenewal, err.Error())
 				return ctrl.Result{}, err
 			}
+			conditions = append(conditions,
+				newConditionNow(o,
+					consts.TypeLeaseRenewal,
+					consts.ReasonSecretLeaseRenewal,
+					metav1.ConditionTrue,
+					"Successfully renewed lease"),
+			)
 
 			o.Status.StaticCredsMetaData = secretsv1beta1.VaultStaticCredsMetaData{}
 			o.Status.SecretLease = *secretLease
 			o.Status.LastRenewalTime = nowFunc().Unix()
-			if err := r.updateStatus(ctx, o); err != nil {
+			if err := r.updateStatus(ctx, o, conditions...); err != nil {
 				return ctrl.Result{}, err
 			}
 
@@ -257,6 +295,14 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 				vClient.Taint()
 			}
 			syncReason = consts.ReasonSecretLeaseRenewalError
+			conditions = append(conditions,
+				newConditionNow(o,
+					consts.TypeLeaseRenewal,
+					consts.ReasonSecretLeaseRenewalError,
+					metav1.ConditionFalse,
+					"Could not renew lease, err=%s",
+					err),
+			)
 		}
 	}
 
@@ -284,6 +330,16 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 		horizon := entry.NextBackOff()
 		r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonSecretSyncError,
 			"Failed to sync the secret, horizon=%s, err=%s", horizon, err)
+
+		conditions = append(conditions,
+			newSyncCondition(o, metav1.ConditionFalse,
+				"Failed to sync the secret, horizon=%s, err=%s", horizon, err,
+			))
+
+		if err := r.updateStatus(ctx, o, conditions...); err != nil {
+			return ctrl.Result{}, err
+		}
+
 		return ctrl.Result{
 			RequeueAfter: horizon,
 		}, nil
@@ -292,21 +348,45 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 	}
 
 	doRolloutRestart := (doSync && o.Status.LastGeneration > 1) || staticCredsUpdated
+
 	o.Status.SecretLease = *secretLease
 	o.Status.LastRenewalTime = nowFunc().Unix()
-	if err := r.updateStatus(ctx, o); err != nil {
-		return ctrl.Result{}, err
-	}
+	o.Status.LastGeneration = o.GetGeneration()
 
 	horizon := r.computePostSyncHorizon(ctx, o)
 	r.Recorder.Eventf(o, corev1.EventTypeNormal, reason,
 		"Secret synced, lease_id=%q, horizon=%s, sync_reason=%q",
 		secretLease.ID, horizon, syncReason)
 
+	conditions = append(
+		conditions,
+		newSyncCondition(o, metav1.ConditionTrue,
+			"Secret synced"),
+	)
+
 	if doRolloutRestart {
 		// rollout-restart errors are not retryable
 		// all error reporting is handled by helpers.HandleRolloutRestarts
-		_ = helpers.HandleRolloutRestarts(ctx, r.Client, o, r.Recorder)
+		if err = helpers.HandleRolloutRestarts(ctx, r.Client, o, r.Recorder); err != nil {
+			conditions = append(
+				conditions,
+				newConditionNow(o,
+					consts.TypeRolloutRestart,
+					consts.ReasonRolloutRestartTriggeredFailed,
+					metav1.ConditionFalse,
+					"Rollout restart trigger failed: %s",
+					err),
+			)
+		} else {
+			conditions = append(
+				conditions,
+				newConditionNow(o,
+					consts.TypeRolloutRestart,
+					consts.ReasonRolloutRestartTriggered,
+					metav1.ConditionTrue,
+					"Rollout restart triggered"),
+			)
+		}
 	}
 
 	if ok := r.SyncRegistry.Delete(req.NamespacedName); ok {
@@ -314,6 +394,10 @@ func (r *VaultDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.R
 			"obj", req.NamespacedName)
 	}
 
+	if err := r.updateStatus(ctx, o, conditions...); err != nil {
+		return ctrl.Result{}, err
+	}
+
 	if horizon.Seconds() == 0 {
 		// no need to requeue
 		logger.Info("Vault secret does not support periodic renewal/refresh via reconciliation",
@@ -547,12 +631,18 @@ func (r *VaultDynamicSecretReconciler) awaitVaultSecretRotation(ctx context.Cont
 	return staticCredsMeta, resp, nil
 }
 
-func (r *VaultDynamicSecretReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultDynamicSecret) error {
+func (r *VaultDynamicSecretReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultDynamicSecret, conditions ...metav1.Condition) error {
+	logger := log.FromContext(ctx).WithName("updateStatus")
+
 	if r.runtimePodUID != "" {
 		o.Status.LastRuntimePodUID = r.runtimePodUID
 	}
 
 	o.Status.LastGeneration = o.GetGeneration()
+
+	n := updateConditions(o.Status.Conditions, conditions...)
+	logger.V(consts.LogLevelDebug).Info("Updating status", "n", n, "o", o.Status.Conditions)
+	o.Status.Conditions = n
 	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)
diff --git a/controllers/vaultpkisecret_controller.go b/controllers/vaultpkisecret_controller.go
index 3d57164..9f61b8f 100644
--- a/controllers/vaultpkisecret_controller.go
+++ b/controllers/vaultpkisecret_controller.go
@@ -16,6 +16,7 @@ import (
 	"github.com/go-logr/logr"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/utils/ptr"
@@ -93,6 +94,8 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	destinationExists, _ := helpers.CheckSecretExists(ctx, r.Client, o)
 	// In the case where the secret should exist already, check that it does
 	// before proceeding to issue a cert
+	var conditions []metav1.Condition
+
 	if !o.Spec.Destination.Create && !destinationExists {
 		horizon := computeHorizonWithJitter(requeueDurationOnError)
 		msg := fmt.Sprintf("Kubernetes secret %q does not exist yet, horizon=%s",
@@ -100,7 +103,8 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		logger.Info(msg)
 		o.Status.Error = consts.ReasonK8sClientError
 		r.recordEvent(o, o.Status.Error, msg)
-		if err := r.updateStatus(ctx, o); err != nil {
+		if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse,
+			"Failed to sync the secret, horizon=%s, err=%s", horizon, consts.ReasonK8sClientError)); err != nil {
 			return ctrl.Result{}, err
 		}
 
@@ -148,7 +152,10 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		helpers.GetTransformationRefObjKeys(
 			o.Spec.Destination.Transformation, o.Namespace)...)
 
-	transOption, err := helpers.NewSecretTransformationOption(ctx, r.Client, o, r.GlobalTransformationOptions)
+	var data map[string][]byte
+	var err error
+	var transOption *helpers.SecretTransformationOption
+	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)
@@ -189,14 +196,16 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		msg := "Failed to issue certificate from Vault"
 		logger.Error(err, msg)
 		r.recordEvent(o, o.Status.Error, msg+": %s", err)
-		if err := r.updateStatus(ctx, o); err != nil {
+		entry, _ := r.BackOffRegistry.Get(req.NamespacedName)
+		horizon := entry.NextBackOff()
+		if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse,
+			"Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
 			return ctrl.Result{}, err
 		}
 
 		r.SyncRegistry.Add(req.NamespacedName)
-		entry, _ := r.BackOffRegistry.Get(req.NamespacedName)
 		return ctrl.Result{
-			RequeueAfter: entry.NextBackOff(),
+			RequeueAfter: horizon,
 		}, nil
 	} else {
 		r.BackOffRegistry.Delete(req.NamespacedName)
@@ -208,7 +217,7 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		msg := "Failed to unmarshal PKI response"
 		logger.Error(err, msg)
 		r.recordEvent(o, o.Status.Error, msg+": %s", err)
-		if err := r.updateStatus(ctx, o); err != nil {
+		if err := r.updateStatus(ctx, o, conditions...); err != nil {
 			return ctrl.Result{}, err
 		}
 		return ctrl.Result{
@@ -221,7 +230,7 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		msg := "Invalid Vault secret data, serial_number cannot be empty"
 		logger.Error(nil, msg)
 		r.recordEvent(o, o.Status.Error, msg)
-		if err := r.updateStatus(ctx, o); err != nil {
+		if err := r.updateStatus(ctx, o, conditions...); err != nil {
 			return ctrl.Result{}, err
 		}
 		return ctrl.Result{
@@ -229,13 +238,13 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		}, nil
 	}
 
-	data, err := resp.SecretK8sData(transOption)
+	data, err = resp.SecretK8sData(transOption)
 	if err != nil {
 		o.Status.Error = consts.ReasonK8sClientError
 		msg := "Failed to marshal Vault secret data"
 		logger.Error(err, msg)
 		r.recordEvent(o, o.Status.Error, msg+": %s", err)
-		if err := r.updateStatus(ctx, o); err != nil {
+		if err := r.updateStatus(ctx, o, conditions...); err != nil {
 			return ctrl.Result{}, err
 		}
 		return ctrl.Result{
@@ -257,7 +266,7 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		if err != nil {
 			logger.Error(err, "HMAC data")
 			o.Status.Error = consts.ReasonHMACDataError
-			if err := r.updateStatus(ctx, o); err != nil {
+			if err := r.updateStatus(ctx, o, conditions...); err != nil {
 				return ctrl.Result{}, err
 			}
 			return ctrl.Result{
@@ -270,11 +279,15 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	if err := helpers.SyncSecret(ctx, r.Client, o, data); err != nil {
 		logger.Error(err, "Sync secret")
 		o.Status.Error = consts.ReasonSecretSyncError
-		if err := r.updateStatus(ctx, o); err != nil {
+		requeueAfter := computeHorizonWithJitter(requeueDurationOnError)
+		conditions = append(conditions,
+			newSyncCondition(o, metav1.ConditionFalse,
+				"Secret sync error, horizon=%s", requeueAfter))
+		if err := r.updateStatus(ctx, o, conditions...); err != nil {
 			return ctrl.Result{}, err
 		}
 		return ctrl.Result{
-			RequeueAfter: computeHorizonWithJitter(requeueDurationOnError),
+			RequeueAfter: requeueAfter,
 		}, nil
 	}
 
@@ -297,19 +310,24 @@ func (r *VaultPKISecretReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		}
 	}
 
+	horizon, _ := computePKIRenewalWindow(ctx, o, .05)
+	conditions = append(conditions,
+		newSyncCondition(o, metav1.ConditionTrue,
+			"Secret synced, horizon=%s", horizon),
+	)
+
 	o.Status.Valid = ptr.To(true)
 	o.Status.Error = ""
 	o.Status.SerialNumber = certResp.SerialNumber
 	o.Status.Expiration = certResp.Expiration
 	o.Status.LastRotation = time.Now().Unix()
-	if err := r.updateStatus(ctx, o); err != nil {
+	if err := r.updateStatus(ctx, o, conditions...); err != nil {
 		logger.Error(err, "Failed to update the status")
 		return ctrl.Result{}, err
 	}
 
 	r.SyncRegistry.Delete(req.NamespacedName)
 
-	horizon, _ := computePKIRenewalWindow(ctx, o, .05)
 	r.recordEvent(o, reason, fmt.Sprintf("Secret synced, horizon=%s", horizon))
 	logger.Info("Successfully updated the secret", "horizon", horizon)
 	return ctrl.Result{
@@ -383,14 +401,14 @@ func (r *VaultPKISecretReconciler) finalizePKI(ctx context.Context, l logr.Logge
 	}
 
 	if !s.Spec.Destination.Create && s.Spec.Clear {
-		if err := r.clearSecretData(ctx, l, s); err != nil {
+		if err := r.clearSecretData(ctx, s); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func (r *VaultPKISecretReconciler) clearSecretData(ctx context.Context, l logr.Logger, s *secretsv1beta1.VaultPKISecret) error {
+func (r *VaultPKISecretReconciler) clearSecretData(ctx context.Context, s *secretsv1beta1.VaultPKISecret) error {
 	return helpers.SyncSecret(ctx, r.Client, s, nil)
 }
 
@@ -433,9 +451,13 @@ func (r *VaultPKISecretReconciler) recordEvent(o *secretsv1beta1.VaultPKISecret,
 	r.Recorder.Eventf(o, eventType, reason, msg, i...)
 }
 
-func (r *VaultPKISecretReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultPKISecret) error {
-	logger := log.FromContext(ctx)
-	logger.V(consts.LogLevelTrace).Info("Update status called")
+func (r *VaultPKISecretReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultPKISecret, conditions ...metav1.Condition) error {
+	logger := log.FromContext(ctx).WithName("updateStatus")
+	logger.V(consts.LogLevelDebug).Info("Updating status")
+
+	n := updateConditions(o.Status.Conditions, conditions...)
+	logger.V(consts.LogLevelDebug).Info("Updating status", "n", n, "o", o.Status.Conditions)
+	o.Status.Conditions = n
 
 	metrics.SetResourceStatus("vaultpkisecret", o, ptr.Deref(o.Status.Valid, false))
 
diff --git a/controllers/vaultstaticsecret_controller.go b/controllers/vaultstaticsecret_controller.go
index d5bd571..972e89a 100644
--- a/controllers/vaultstaticsecret_controller.go
+++ b/controllers/vaultstaticsecret_controller.go
@@ -95,7 +95,16 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 	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
+
+		horizon := computeHorizonWithJitter(requeueDurationOnError)
+		if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse,
+			"Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+			return ctrl.Result{}, err
+		}
+
+		return ctrl.Result{
+			RequeueAfter: horizon,
+		}, nil
 	}
 
 	var requeueAfter time.Duration
@@ -105,7 +114,15 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 			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
+
+			horizon := computeHorizonWithJitter(requeueDurationOnError)
+			if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse,
+				"Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+				return ctrl.Result{}, err
+			}
+			return ctrl.Result{
+				RequeueAfter: horizon,
+			}, nil
 		}
 		requeueAfter = computeHorizonWithJitter(d)
 	}
@@ -118,13 +135,28 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 	if err != nil {
 		r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonTransformationError,
 			"Failed setting up SecretTransformationOption: %s", err)
-		return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+
+		horizon := computeHorizonWithJitter(requeueDurationOnError)
+		if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse, "Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+			return ctrl.Result{}, err
+		}
+
+		return ctrl.Result{
+			RequeueAfter: horizon,
+		}, nil
 	}
 
 	kvReq, err := newKVRequest(o.Spec)
 	if err != nil {
+		horizon := computeHorizonWithJitter(requeueDurationOnError)
 		r.Recorder.Event(o, corev1.EventTypeWarning, consts.ReasonVaultStaticSecret, err.Error())
-		return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+		if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse, "Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+			return ctrl.Result{}, err
+		}
+
+		return ctrl.Result{
+			RequeueAfter: horizon,
+		}, nil
 	}
 
 	resp, err := c.Read(ctx, kvReq)
@@ -136,7 +168,15 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 		entry, _ := r.BackOffRegistry.Get(req.NamespacedName)
 		r.Recorder.Eventf(o, corev1.EventTypeWarning, consts.ReasonVaultClientError,
 			"Failed to read Vault secret: %s", err)
-		return ctrl.Result{RequeueAfter: entry.NextBackOff()}, nil
+
+		horizon := entry.NextBackOff()
+		if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse, "Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+			return ctrl.Result{}, err
+		}
+
+		return ctrl.Result{
+			RequeueAfter: horizon,
+		}, nil
 	} else {
 		r.BackOffRegistry.Delete(req.NamespacedName)
 	}
@@ -145,7 +185,15 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 	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
+
+		horizon := computeHorizonWithJitter(requeueDurationOnError)
+		if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse, "Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+			return ctrl.Result{}, err
+		}
+
+		return ctrl.Result{
+			RequeueAfter: horizon,
+		}, nil
 	}
 
 	var doRolloutRestart bool
@@ -163,7 +211,14 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 
 		macsEqual, messageMAC, err := helpers.HandleSecretHMAC(ctx, r.SecretsClient, r.HMACValidator, o, data)
 		if err != nil {
-			return ctrl.Result{RequeueAfter: computeHorizonWithJitter(requeueDurationOnError)}, nil
+			horizon := computeHorizonWithJitter(requeueDurationOnError)
+			if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse, "Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+				return ctrl.Result{}, err
+			}
+
+			return ctrl.Result{
+				RequeueAfter: horizon,
+			}, nil
 		}
 
 		// skip the next sync if the data has not changed since the last sync, and the
@@ -179,20 +234,54 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 			"targets", o.Spec.RolloutRestartTargets)
 	}
 
+	var conditions []metav1.Condition
+	reason := consts.ReasonSecretSynced
 	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)
+
+			horizon := computeHorizonWithJitter(requeueDurationOnError)
+			if err := r.updateStatus(ctx, o, newSyncCondition(o, metav1.ConditionFalse, "Failed to sync the secret, horizon=%s, err=%s", horizon, err)); err != nil {
+				return ctrl.Result{}, err
+			}
+
+			return ctrl.Result{
+				RequeueAfter: horizon,
+			}, nil
 		}
+
+		conditions = append(conditions,
+			newSyncCondition(o, metav1.ConditionTrue,
+				"Secret synced, horizon=%s", requeueAfter),
+		)
 		r.Recorder.Event(o, corev1.EventTypeNormal, reason, "Secret synced")
+
+	}
+
+	if doRolloutRestart {
+		reason = consts.ReasonSecretRotated
+		// rollout-restart errors are not retryable
+		// all error reporting is handled by helpers.HandleRolloutRestarts
+		if err = helpers.HandleRolloutRestarts(ctx, r.Client, o, r.Recorder); err != nil {
+			conditions = append(
+				conditions,
+				newConditionNow(o,
+					consts.TypeRolloutRestart,
+					consts.ReasonRolloutRestartTriggeredFailed,
+					metav1.ConditionFalse,
+					"Rollout restart trigger failed, err=%s",
+					err),
+			)
+		} else {
+			conditions = append(
+				conditions,
+				newConditionNow(o,
+					consts.TypeRolloutRestart,
+					consts.ReasonRolloutRestartTriggered,
+					metav1.ConditionTrue, "Rollout restart triggered"),
+			)
+		}
 	} else {
 		logger.V(consts.LogLevelDebug).Info("Secret sync not required")
 	}
@@ -208,7 +297,8 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 		r.unWatchEvents(o)
 	}
 
-	if err := r.updateStatus(ctx, o); err != nil {
+	o.Status.LastGeneration = o.GetGeneration()
+	if err := r.updateStatus(ctx, o, conditions...); err != nil {
 		return ctrl.Result{}, err
 	}
 
@@ -217,10 +307,14 @@ func (r *VaultStaticSecretReconciler) Reconcile(ctx context.Context, req ctrl.Re
 	}, nil
 }
 
-func (r *VaultStaticSecretReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultStaticSecret) error {
-	logger := log.FromContext(ctx)
+func (r *VaultStaticSecretReconciler) updateStatus(ctx context.Context, o *secretsv1beta1.VaultStaticSecret, conditions ...metav1.Condition) error {
+	logger := log.FromContext(ctx).WithName("updateStatus")
 	logger.V(consts.LogLevelDebug).Info("Updating status")
 	o.Status.LastGeneration = o.GetGeneration()
+	n := updateConditions(o.Status.Conditions, conditions...)
+	logger.V(consts.LogLevelDebug).Info("Updating status", "n", n, "o", o.Status.Conditions)
+	o.Status.Conditions = n
+
 	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)



Test patch:
diff --git a/test/integration/vaultdynamicsecret/terraform/postgres.tf b/test/integration/vaultdynamicsecret/terraform/postgres.tf
index f202ccf..8b56710 100644
--- a/test/integration/vaultdynamicsecret/terraform/postgres.tf
+++ b/test/integration/vaultdynamicsecret/terraform/postgres.tf
@@ -11,8 +11,25 @@ resource "helm_release" "postgres" {
   # ref: https://github.com/bitnami/charts/issues/30582#issuecomment-2494545610
   repository = "oci://registry-1.docker.io/bitnamicharts"
   chart      = "postgresql"
-  version    = "16.2.2"
+  version    = "16.7.27"
 
+  # the chart's default images are no longer supported, so we need to override them to come from the bitnami secure repo.
+  # ref: https://artifacthub.io/packages/helm/bitnami/postgresql#-important-notice-upcoming-changes-to-the-bitnami-catalog
+  set {
+    name  = "image.repository"
+    value = "bitnamisecure/postgresql"
+  }
+  set {
+    name = "image.tag"
+    # use latest to to be compatible with the linux/arm64/v8 platform.
+    value = "latest"
+  }
+  set {
+    # ironically, this is required to use the bitnami secure images with the bitnami Helm chart.
+    # ref: https://github.com/bitnami/charts/issues/35256#issuecomment-3109344715
+    name  = "global.security.allowInsecureImages"
+    value = "true"
+  }
   set {
     name  = "primary.persistence.enabled"
     value = var.postgres_enable_persistence ? "true" : "false"