Skip to content

feat(secrets): stop retaining Secret values (roadmap Track A, steps 1-3)#208

Open
sunib wants to merge 2 commits into
mainfrom
feat/secret-value-retention
Open

feat(secrets): stop retaining Secret values (roadmap Track A, steps 1-3)#208
sunib wants to merge 2 commits into
mainfrom
feat/secret-value-retention

Conversation

@sunib

@sunib sunib commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements Track A, steps 1–3 of the secret-handling roadmap
(docs/future/secret-handling-roadmap.md) —
the immediate process- and crypto-level security wins, ahead of the separate
RBAC-narrowing track (Track B). Detailed plan:
docs/future/secret-value-retention-plan.md.

The controller no longer keeps every cluster Secret value in memory, no longer
writes private age keys to disk, and reacts to out-of-band credential/age-key
changes on one steady 5-minute reconcile instead of a Secret informer.

What changed

Step 1 — stop caching Secret values, drop control-plane Secret watches

  • Remove the GitTarget control-plane Secret watch (and its now-orphaned
    encryptionSecretToGitTargets map function). A full-object Secret watch made
    the controller-runtime cache retain every Secret value in every namespace.
  • Disable the typed-Secret cache on the manager
    (Client.Cache.DisableFor: corev1.Secret), so typed Secret reads go straight
    to the API server and stay fresh (no stale-cache reads).
  • GitProvider stays pull-based — no auth-Secret watch, by design.

Step 2 — remove private age identities from the SOPS encrypt path

  • The write path only encrypts, so it needs public age recipients only.
  • Drop AgeIdentities and Environment from ResolvedEncryptionConfig; delete
    the age identity temp-file path / SOPS_AGE_KEY_FILE; stop passing blanket
    Secret data into the sops process environment. Recipients still reach sops
    through the target's committed .sops.yaml, so encryption is unchanged.
  • A BYO age-key Secret is still read to derive its public recipient; the
    private identity is never returned, written to disk, or handed to sops.

Step 3 — unify the control-plane reconcile fallback at 5 minutes

  • Collapse the short (2 min) / medium (5 min) / long (10 min) requeue intervals
    into a single RequeueSteadyInterval (5 min) for GitProvider, GitTarget,
    WatchRule, and ClusterWatchRule. Genuine spec changes still trigger
    immediately via existing watches; this is only the periodic fallback cadence.

Step 5 — resolve Git credentials once per push cycle (data-plane read reduction)

  • With the Secret cache disabled, each typed Secret read is a real API GET.
    commitPendingWrites resolved the credentials Secret on every commit but
    only used it on the first commit of a cycle (the one that fetches the remote
    tip via PrepareBranch). Later commits in a burst read it and threw it away.
  • Resolve auth lazily, only on the !hasPendingCommits branch — mirroring
    ensureRepositoryInitialized. No memo/cache is introduced (the plan warns
    that would reintroduce stale-credential behavior); each operation still resolves
    directly, once, reflecting current credentials. A unit test counts credential
    GETs across a two-commit cycle: 1 on the first, 0 on the second.

Docs

  • docs/security-model.md: the control-plane runtime now reads input Secrets
    directly by name (get), no longer list/watches them, and retains no Secret
    values — explicitly called out as a behavior change, not (yet) an RBAC change
    (the default install still grants secrets get;list;watch plus the
    dynamic-watch wildcard until the RBAC-narrowing track lands).
  • docs/design/reconcile-triggering.md: updated interval table + dependency
    table; the "watch the auth Secret" recommendation is now marked superseded.
  • docs/design/manifest/gitpathaccepted-projection-race-and-external-drift.md:
    dated note flagging the removed Secret watch and unified 5-minute interval.
  • Adds the roadmap and the two plan docs under docs/future/.

Incorporates review feedback: separated runtime behavior from the (unchanged)
RBAC grant in the security doc, and refreshed the stale gitpathaccepted design doc.

Out of scope

RBAC narrowing (Track B). The default install keeps secrets get;list;watch
until the RBAC generator lands; the runtime no longer needs list/watch on
controller-owned Secret inputs, so packaging can drop them later.

Validation

  • task lint ✅ · task test ✅ (coverage ratchet holds) · task test-e2e
  • New/updated unit tests: SOPS env carries no private age material
    (TestConfigureSecretEncryptionWriter_NoPrivateAgeMaterial), recipient
    derivation returns recipient only, steady interval locked at 5 min. E2e SOPS/age
    encryption specs exercise the recipients-from-.sops.yaml path end-to-end.

…cret watches

Implements Track A, steps 1-3 of the secret-handling roadmap
(docs/future/secret-handling-roadmap.md) - the immediate process- and
crypto-level security wins, ahead of the separate RBAC-narrowing track.

Step 1 - stop caching Secret values, drop control-plane Secret watches:
- Remove the GitTarget control-plane Secret watch (and its now-orphaned
  encryptionSecretToGitTargets map func). A full-object Secret watch made the
  controller-runtime cache retain every Secret value in every namespace.
- Disable the typed-Secret cache on the manager (Client.Cache.DisableFor
  corev1.Secret), so typed Secret reads go straight to the API server and stay
  fresh. GitProvider stays pull-based (no auth-Secret watch, by design).

Step 2 - remove private age identities from the SOPS encrypt path:
- The write path only encrypts, so it needs public age recipients only.
- Drop AgeIdentities/Environment from ResolvedEncryptionConfig; delete the age
  identity temp-file path / SOPS_AGE_KEY_FILE; stop passing blanket Secret data
  into the sops process env. Recipients still reach sops via the target
  .sops.yaml, so encryption output is unchanged. A BYO age-key Secret is still
  read to derive its public recipient; the private identity is never returned,
  written to disk, or handed to sops.

Step 3 - unify the control-plane reconcile fallback at 5 minutes:
- Collapse the short (2m) / medium (5m) / long (10m) requeue intervals into a
  single RequeueSteadyInterval (5m) for GitProvider, GitTarget, WatchRule, and
  ClusterWatchRule. Genuine spec changes still trigger immediately via watches;
  this is only the periodic fallback that picks up out-of-band credential and
  age-key changes now that Secrets are not watched.

Docs: security-model.md and design/reconcile-triggering.md updated to the
direct-read model (behavior change, not yet an RBAC change - the default install
still grants secrets get;list;watch until the RBAC-narrowing track lands); the
gitpathaccepted-projection-race design doc gets a dated note for the removed
Secret watch and unified interval. Adds the roadmap and two plan docs under
docs/future/.

RBAC narrowing (Track B) is intentionally out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR unifies controller requeue intervals into a single RequeueSteadyInterval (5 minutes), removes the GitTarget Secret watch and disables client-side caching for Secrets in the manager, and refactors age encryption to resolve public recipients only, eliminating private identity retention. Accompanying design/roadmap documents describe the rationale.

Changes

Requeue interval unification and Secret retention removal

Layer / File(s) Summary
Steady requeue constant
internal/controller/constants.go, internal/controller/dependency_watches_test.go
Replaces RequeueShortInterval, RequeueMediumInterval, RequeueLongInterval with a single RequeueSteadyInterval (5 minutes), with a test verifying the value.
Manager Secret cache disablement
cmd/main.go
Manager client options disable caching for corev1.Secret objects to avoid retaining Secret data in memory.
GitTarget controller updates
internal/controller/gittarget_controller.go
Requeue paths switch to RequeueSteadyInterval; the Secret watch and encryptionSecretToGitTargets mapping helper are removed from SetupWithManager.
GitProvider controller updates
internal/controller/gitprovider_controller.go, internal/controller/gitprovider_controller_test.go
updateStatusAndRequeue drops its duration parameter and always uses RequeueSteadyInterval; call sites and tests updated.
WatchRule/ClusterWatchRule controller updates
internal/controller/watchrule_controller.go, internal/controller/clusterwatchrule_controller.go
Both controllers' status-update helpers drop the interval parameter and use RequeueSteadyInterval consistently.
Age encryption recipients-only refactor
internal/git/encryption.go, internal/git/encryption_test.go
ResolvedEncryptionConfig drops Environment/AgeIdentities; secret resolution returns recipients only; SOPS writer no longer receives private identity material; tests updated accordingly.
Comment update
internal/watch/manager.go
Updates a comment to reference the new steady interval instead of the prior long interval.

Estimated code review effort: 4 (Complex) | ~60 minutes

Design and roadmap documentation

Layer / File(s) Summary
Existing design doc revisions
docs/design/manifest/gitpathaccepted-projection-race-and-external-drift.md, docs/design/reconcile-triggering.md, docs/security-model.md
Updates prior docs to reflect the removed Secret watch, unified 5-minute cadence, and direct Secret-read behavior.
New future-plan documents
docs/future/secret-handling-roadmap.md, docs/future/secret-value-retention-plan.md, docs/future/scoped-rbac-least-privilege-plan.md
Adds new documents outlining the Secret retention removal plan and a scoped least-privilege RBAC plan.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

Not applicable — the changes are primarily interval/config adjustments, watch removal, and documentation, without new multi-component interaction flows warranting a sequence diagram.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: stopping Secret value retention as part of the secret-handling roadmap.
Description check ✅ Passed The description is detailed and covers the summary, scope, docs, out-of-scope work, and validation steps.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/secret-value-retention

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/future/secret-value-retention-plan.md`:
- Around line 29-31: Update the “Current problem” wording to match the new
Secret cache exception introduced by Client.Cache.DisableFor for corev1.Secret.
In the secret-value-retention-plan document, revise the manager/cache baseline
text so it no longer says the manager is simply created without cache options;
make it clear that the shared cache still applies generally, but Secrets are now
explicitly excluded, and keep the distinction between the existing state and
what Track A changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 14b58067-3923-4abf-8f21-bbee92c19a97

📥 Commits

Reviewing files that changed from the base of the PR and between e5722a7 and b55ef89.

📒 Files selected for processing (17)
  • cmd/main.go
  • docs/design/manifest/gitpathaccepted-projection-race-and-external-drift.md
  • docs/design/reconcile-triggering.md
  • docs/future/scoped-rbac-least-privilege-plan.md
  • docs/future/secret-handling-roadmap.md
  • docs/future/secret-value-retention-plan.md
  • docs/security-model.md
  • internal/controller/clusterwatchrule_controller.go
  • internal/controller/constants.go
  • internal/controller/dependency_watches_test.go
  • internal/controller/gitprovider_controller.go
  • internal/controller/gitprovider_controller_test.go
  • internal/controller/gittarget_controller.go
  • internal/controller/watchrule_controller.go
  • internal/git/encryption.go
  • internal/git/encryption_test.go
  • internal/watch/manager.go

Comment on lines +29 to +31
The manager is created without cache options:
[cmd/main.go](../../cmd/main.go#L841). Any informer started by the shared cache is
cluster-wide unless configured otherwise.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the current-state wording to reflect the new Secret cache exception.

The "Current problem" section says the manager is created "without cache options," but this PR now adds Client.Cache.DisableFor for corev1.Secret. As written, the doc's baseline is no longer accurate and blurs what Track A is changing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/future/secret-value-retention-plan.md` around lines 29 - 31, Update the
“Current problem” wording to match the new Secret cache exception introduced by
Client.Cache.DisableFor for corev1.Secret. In the secret-value-retention-plan
document, revise the manager/cache baseline text so it no longer says the
manager is simply created without cache options; make it clear that the shared
cache still applies generally, but Secrets are now explicitly excluded, and keep
the distinction between the existing state and what Track A changes.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

…ommit

Follow-up to the secret-value-retention change (plan step 5). With the Secret
cache disabled, every typed Secret read is a real API GET, so redundant reads now
cost etcd round-trips.

commitPendingWrites resolved the credentials Secret unconditionally but only used
it on the first commit of a push cycle (the branch that runs PrepareBranch to
fetch the remote tip). Every subsequent commit in a burst read the credentials
Secret - plus its known_hosts lookups - and discarded it. Resolve auth lazily,
only inside the !hasPendingCommits branch that actually touches the remote,
mirroring ensureRepositoryInitialized.

No memo/cache is introduced (the plan cautions against that - it would reintroduce
stale-credential behavior): pushPendingCommits and the first commit still resolve
directly, once per operation, reflecting current credentials. This only removes
the reads that were never used.

Adds a unit test that counts credentials-Secret GETs across a two-commit cycle:
1 on the first commit, 0 on the second.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant