Repository navigation
Mirror secrets to workspaces chosen by a label selector - #768
Merged
Merged
Conversation
A Secret in the primus-safe namespace could only be mirrored into the
workspaces named in its primus-safe.workspace.ids list. A workspace
created later never got its copy, and nothing re-read the list when one
appeared, so a Secret every sandbox mounts (sandbox.extra_ca_secret)
left new workspaces with Pods stuck on the volume.
- New annotation primus-safe.workspace.selector: a label selector in
kubectl -l syntax over Workspace labels. The Secret is mirrored into
every matching workspace that is not being deleted, in union with the
ids list, which keeps its meaning (and stays the authorisation that
image-secret and CI/CD proxy checks read; the selector grants nothing).
- The secret controller now watches Workspaces: create, delete and label
changes enqueue the Secrets that carry a selector or list the
workspace by id, so a workspace gets or loses its copy without anyone
touching the Secret.
- A workspace that stops matching loses the copy the controller made;
same-named Secrets it did not make are never overwritten through the
selector, nor deleted.
- An empty selector selects nothing (labels.Parse("") would be
Everything). An invalid one raises a Warning event, mirrors nothing
beyond the ids, and removes nothing until it is fixed.
- The workspace webhook derives primus-safe.workspace.scope.sandbox=true
from Spec.Scopes (no scopes, or Sandbox among them), so
"primus-safe.workspace.scope.sandbox=true" names every workspace that
runs sandboxes. values.yaml and the upgrade doc now say so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
The sandbox scope label was set only by the workspace webhook, so a workspace written before it existed carried none. A selector-only Secret then selected nothing there and its cleanup deleted the copy the workspace's sandboxes already mount. Match selectors against the workspace's labels with the sandbox scope label taken from Spec.Scopes, and add a small controller that writes the label onto workspaces whose label disagrees with their scopes; on start every existing workspace is seen once. Also enqueue on a scope change, cover deleting a selector-only Secret, and document that a selector should use primus-safe.* labels only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rget A workspace creator can set any label outside the primus-safe. prefix, so a Secret selector on one would hand the Secret to whoever sets it: treat such a selector as invalid. Also collect per-target errors instead of returning on the first, so one workspace whose namespace is not ready yet no longer starves the others or skips cleanup, and return them for default backoff rather than a 1s requeue. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…selector The reserved-prefix check looked at the label key only, so a negated requirement such as !<misspelt reserved key> or <reserved key>!=false passed, and matched every workspace that lacks the label. Only =, ==, in and exists are accepted now. An invalid selector used to skip cleanup and return nil, leaving every mirrored copy in place with no visible record. It now behaves like an empty selector: copies are reclaimed, and the reason is written to the primus-safe.workspace.selector.error annotation on the Secret, which is removed once the selector is valid again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lizer to hand-made Secrets An invalid selector used to reclaim every copy, which stops every sandbox that mounts the extra CA. It now freezes: nothing new is mirrored by the selector, nothing is removed, the reason goes to primus-safe.workspace.selector.error and a Warning event. The extra-ca volume is optional so a missing copy no longer blocks Pod admission. A selector is honoured only on a Secret carrying primus-safe.secret.broadcast =true: scope.sandbox follows Spec.Scopes, which a workspace's editors can change, so without the mark they could pull a credential into their own namespace. Secrets made with kubectl carry no finalizer, so deleting one left its copies behind; the reconcile now adds it when the Secret has mirror targets. Document that rolling back to a release without selector support drops copies reached only by the selector. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… mark, restrict selectable labels - The Secret update predicate now compares the broadcast annotation, so adding the mark after a freeze reconciles the Secret. - An invalid selector freezes only the copies the selector made, recorded in primus-safe.workspace.selector.applied while it was valid; ids-list additions and removals (copy and workspace reference) keep working. With no record nothing is removed. - A selector may only use the system-derived sandbox scope label; other keys, including primus-safe.* ones filled from user input, are invalid. - Revert optional: true on the extra-ca volume: with restartPolicy Never a Pod that starts without the copy would die for good instead of waiting. - Document the finalizer on hand-made Secrets for helm uninstall. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…only Secrets - With no selector.applied record the selector never mirrored anything, so a frozen selector no longer holds back the ids list: copies and workspace references of workspaces removed from ids are reclaimed. An unreadable record still freezes everything. - The Secret Create predicate also passes Secrets that carry our finalizer or are being deleted, so a Secret whose annotations were removed while the controller was down can still be cleaned up and released. - Document that removing or falsifying the broadcast annotation freezes the selector rather than removing copies; removing the selector annotation does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…kspace; sync frozen copies - Once a Secret has no mirror targets and its copies are cleaned, the finalizer added for them is removed (kept while a cluster references the Secret, since the delete path clears that reference). - cleanupMirroredSecrets now continues past a workspace whose cluster is gone or whose data plane client is not ready, and returns the errors together. - A frozen selector keeps syncing the copies it made (the applied record), so a rotated CA still reaches them; nothing is added or removed. - A workspace holding a Secret resource-manager did not make is not recorded as mirrored into; the record is written after the mirror loop. - Document the frozen semantics and the scale limit of the applied record. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…applied record - delete() cleans up mirrored copies unconditionally (the mirror label guards what it removes), so annotations removed before the delete cannot leave copies behind. - Remove the workspace scope label backfill controller: its patch went through the workspace webhook, which rejects workspaces whose NodeFlavor is gone and can reset Spec.Replica of legacy workspaces with no NodeFlavor. The mirror derives the label in memory and does not need it persisted. - Widen the applied record to old plus selected before the mirror loop, so a crash mid-loop cannot leave an incomplete record. - A blank selector is no mirror target, so it no longer holds the finalizer. - copySecret re-reads on AlreadyExists and reports a conflict for a Secret not carrying the mirror label. - Parse the applied record before testing it. - Document that scope editors can match the selector, so broadcast must never go on a credential, and how a damaged applied record is repaired. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s per cluster - Stop before touching any data plane when the widened applied record cannot be written; a copy made without it could later be removed by a frozen selector. - Treat a missing or unreadable applied record under an invalid selector as unknown: keep and sync every existing copy, ids removals included, until the selector is fixed or removed. The docs no longer say to delete the record. - On delete, clean up across data planes only for a Secret that may have been mirrored (ids, selector, broadcast mark or a selector record), so an SSH key is not held by an offline data plane. - Find copies with one List per data plane cluster (mirror label and name) instead of one Get per workspace. - Raise InvalidWorkspaceSelector only when the recorded reason changes. - Move the SECURITY note after the extra CA paragraph in values.yaml. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the applied record missing or unreadable, the set of existing copies was rebuilt from the data plane by the mirror label and then synced. Anyone who can create a Secret in their own namespace can put that label on it, so a credential whose selector was never valid could be written into it. Keep every copy as it is instead and sync only the ids list until the selector is fixed or removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Review round on f78289c, addressed in d803a7b and 256345e:
Not changed: Each new test fails with its fix reverted. |
Contributor
Author
|
Security verification round on 256345e: CLEAN.
|
weilei0120
approved these changes
Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A Secret in the
primus-safenamespace is mirrored by resource-manager only into the workspaces listed in itsprimus-safe.workspace.idsannotation. That list is hand-kept, and the controller does not watch Workspaces, so a workspace created afterwards never gets its copy. Forsandbox.extra_ca_secret, which the sandbox template mounts into every Sandbox Pod, that leaves a new workspace's Sandbox Pods stuck on the volume mount.Change
primus-safe.workspace.selector(new Secret annotation): a label selector inkubectl -lsyntax over Workspace labels, honoured only on a Secret that also carriesprimus-safe.secret.broadcast: "true". The Secret is mirrored into every matching Workspace that is not being deleted, in union with the ids list. The ids list keeps its meaning, including as the authorisation that image-secret binding and CI/CD proxy validation read; the selector only decides where copies go and is never written back into ids.primus-safe.workspace.scope.sandbox, and only positive operators (=,==,in, exists). Other labels take their value from what a workspace creator submits; a negative operator is true of a workspace that lacks the label.primus-safe.workspace.scope.sandbox=true: set by the workspace mutating webhook fromSpec.Scopes(no scopes, orSandboxamong them). The controller matches against the value derived fromSpec.Scopes, so a workspace written before the label existed is selected at once;kubectl -lmay disagree for such a workspace until its next update.primus-safe/secret-mirror=truelabel the controller sets. A same-named Secret reached only through the selector and not made by the controller is neither overwritten nor deleted (Warning eventMirrorConflict). Listing a workspace by id still takes over such a Secret, as before.primus-safe.workspace.selector.applied; while frozen, those copies are kept and kept in sync, and the ids list applies as usual. If that record is missing or unreadable (for example afterkubectl replace), it is treated as unknown: every existing copy is left as it is, ids removals included, and only the ids list is synced, until the selector is fixed (which rewrites the record) or removed. The set of existing copies is not rebuilt from the data plane: anyone who can create a Secret in their namespace can put the mirror label on it. The record is widened to old-plus-new before any data plane is touched, and if that write fails nothing is mirrored. The reason is inprimus-safe.workspace.selector.errorand raised as a WarningInvalidWorkspaceSelectoronce per new reason.kubectl), and released once the Secret has no targets and its copies are gone. On delete, the cross-data-plane cleanup runs only for a Secret that may have been mirrored (ids, selector, broadcast mark or a selector record); other Secrets, such as SSH keys, are released without reaching any data plane, so an offline data plane does not hold them.Data plane requests
Cleanup used to issue one
GET secrets/<name>per workspace on every reconcile (plus aDELETEper stale copy). It now issues oneLIST secretsper data plane cluster, across namespaces, filtered bylabelSelector=primus-safe/secret-mirror=trueandfieldSelector=metadata.name=<name>, so the response holds only this Secret's copies. With W workspaces on C clusters a reconcile goes from W GETs to C LISTs; the DELETEs are unchanged. This needslist secretsacross namespaces on the data plane; resource-manager reaches each data plane with the cluster's control-plane client certificate, the same client that creates and deletes workspace namespaces there.Tests
resource-manager/pkg/resource/secret_selector_test.go(fake controller client plus fake data-plane clientset) covers: matching and non-matching workspaces; workspaces created later (selector and ids); label/scope removal removing only the controller's copy; no take-over of a hand-made Secret; workspaces being deleted excluded; ids ∪ selector; ids-only behaviour unchanged; blank selector; invalid selectors (each rejected form) frozen with the reason recorded; broadcast mark required; ids edits under a frozen selector; unknown record afterkubectl replace, a forced server-side apply, or a damaged record (copies kept as they are, only ids synced, nothing added, freeze lifted by fixing the selector); a forged mirror label not trusted under an unknown record; applied record write failure stopping before any data plane write; Warning raised once per reason; delete of a selector-only, a kubectl-made, and an annotation-stripped Secret; delete of an unrelated Secret with an offline data plane; one LIST per cluster in cleanup; finalizer release; per-target failures and cluster-not-ready backoff.webhooks/pkg/workspace_webhook_test.go:TestWorkspaceMutateSandboxScopeLabel.Mutation check: reverting each fix of the latest commit (continue after a failed record write, always emit the event, always clean up on delete, drop the record from the delete check, per-workspace GET, missing record treated as empty, copies rebuilt from the data plane) fails the test written for it.
Not in scope: a source Secret deleted with all its mirror annotations removed before the controller reconciled the removal leaves its copies, as on main. A same-named Secret created by someone else between our Get and Create is detected after the AlreadyExists and treated as a conflict; the next event reconciles it.
🤖 Generated with Claude Code