Skip to content

fix(pushsecret): honor updatePolicy: None for operator-managed secrets#81

Open
matheusvellone wants to merge 1 commit into
Infisical:mainfrom
matheusvellone:fix/push-secret-update-policy-none-managed
Open

fix(pushsecret): honor updatePolicy: None for operator-managed secrets#81
matheusvellone wants to merge 1 commit into
Infisical:mainfrom
matheusvellone:fix/push-secret-update-policy-none-managed

Conversation

@matheusvellone

@matheusvellone matheusvellone commented May 25, 2026

Copy link
Copy Markdown

Summary

Closes #79

InfisicalPushSecret with updatePolicy: None currently overwrites secrets in Infisical anyway whenever the secret was originally created by the same resource (i.e. its ID is in Status.ManagedSecrets). The None policy is only honored for pre-existing, unmanaged secrets — which makes it effectively a no-op for the most common use case: push a generated value once, never touch it again.

The most visible symptom is a Password ClusterGenerator pushed via InfisicalPushSecret with updatePolicy: None: every reconcile (including the implicit one fired by CreateFunc on operator pod restart or leader-election flip) regenerates the value and pushes it, breaking any consumer still holding the previous value (Redis server, env-var-injected app pods, etc.).

This is a one-line behavioral fix; no API changes.

Root cause

In internal/services/infisicalpushsecret/reconciler.go, inside ReconcileInfisicalPushSecret, the final loop (// Check if any of the existing secrets values have changed) had:

_, managedByOperator := infisicalPushSecret.Status.ManagedSecrets[existingSecret.ID]

if secretValue != existingSecret.SecretValue {
    if managedByOperator || updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
        // ... performs Update against Infisical ...
    }
}

The managedByOperator || clause short-circuits the policy check. Any secret whose ID is in status.managedSecrets was updated on every reconcile where the source value differed — and for generator-backed pushes, processGenerators produces a fresh value each call, so the values always differ.

The two earlier branches in the same function (first-reconcile-create and "secret added later") already gate on updatePolicy correctly; this final loop was the outlier.

A push.secret-style InfisicalPushSecret with updatePolicy: None appears to behave correctly because the source corev1.Secret is stable, so secretValue == existingSecret.SecretValue and the branch is skipped. But it is the same bug — just dormant until the source value changes.

The fix

Drop managedByOperator || so updatePolicy is the single gate, matching the documented behavior ("[updatePolicy] defaults to no replacement") on the InfisicalPushSecret CRD page.

- _, managedByOperator := infisicalPushSecret.Status.ManagedSecrets[existingSecret.ID]
-
- if secretValue != existingSecret.SecretValue {
-     if managedByOperator || updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
+ if secretValue != existingSecret.SecretValue {
+     if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {

(With this gate, the unused managedByOperator declaration is also removed.)

Verification

Reproduced and validated against my staging cluster running infisical/kubernetes-operator:v0.10.26:

  • Before the fix: InfisicalPushSecret (updatePolicy: None) backed by a Password ClusterGenerator. kubectl delete pod -n infisical-operator -l control-plane=controller-manager rotates the value in Infisical on every restart.
  • After the fix (locally-built image from this branch): same restart sequence; the Infisical value is unchanged.
  • Regression check: push.secret-style InfisicalPushSecret with updatePolicy: Replace still updates Infisical when the source kube Secret changes. That path is unaffected by this change.
  • go build ./... and go vet ./... both clean.

The existing infisicalpushsecret_controller_test.go is a scaffolded stub with no real assertions for this branch; meaningful coverage would require mocking the Infisical Go SDK client. Happy to follow up with that in a separate PR if you'd like — wanted to keep this one minimal and focused on the behavioral fix.

Backwards compatibility

For anyone currently relying on the bug (i.e. updatePolicy: None + a push.secret-style resource whose source Secret mutates and where they expected Infisical to get the updates), this is a behavior change — they should switch to updatePolicy: Replace, which is the documented way to opt into updates. I'd argue the current behavior was already a documentation/implementation mismatch rather than a contract, so a fix(pushsecret): commit is appropriate.


Disclosure: I investigated this with Claude Code (Anthropic) — it traced the reconcile logic with me and helped draft this PR. I reproduced the bug against my own cluster (v0.10.26), confirmed the fix locally, and reviewed every line of the diff and this PR body before opening.

The final "Check if any of the existing secrets values have changed" loop
in ReconcileInfisicalPushSecret updated secrets in Infisical whenever the
secret was originally created by the same resource, regardless of
updatePolicy. The earlier branches already gate on updatePolicy correctly;
this one short-circuited with `managedByOperator || ...`.

The most visible symptom is a Password ClusterGenerator pushed via
InfisicalPushSecret with updatePolicy: None: every reconcile (including
the implicit one fired by CreateFunc on operator pod restart or leader
election flip) regenerates the value and pushes it, even though the user
explicitly opted out of updates.

Drop the managedByOperator branch so updatePolicy is the single gate,
matching the documented "defaults to no replacement" behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 25, 2026 18:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR changes the reconciliation logic for updating existing Infisical secrets by removing the “managed by operator” condition from the update decision.

Changes:

  • Removes lookup of managedByOperator from Status.ManagedSecrets for existing secrets.
  • Restricts secret updates to only when the PUSH_SECRET_REPLACE_POLICY_ENABLED update policy is set.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if secretValue != existingSecret.SecretValue {

if managedByOperator || updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InfisicalPushSecret updating secrets regarding UpdatePolicy being None

2 participants