Repository navigation
fix(localresource): confine fromRef to the referencing resource's own namespace (#10) - #19
Merged
Merged
Conversation
… namespace (#10) LocalResource let its author name any object in any namespace via spec.fromResource.fromRef, and the provider fetched it with its OWN ServiceAccount — which holds apiGroups ["*"], resources ["*"], verbs ["get"] cluster-wide, precisely because cross-namespace refs were the intent. The object was then serialised in full (WriteK8sResource strips nothing, so Secret.data is included) and committed to a git remote the same CR names. So namespaced create rights on one CRD conferred cluster-wide read plus an egress path for the result. Confused deputy. fromRef must now name the referencing LocalResource's own namespace. An empty namespace stays permitted: that is the cluster-scoped read path, not the author-directed namespace escalation this closes. What this does NOT do, stated rather than implied: it removes the author's ability to AIM the provider's identity at another namespace. It does not make the read authorized. A same-namespace read is still performed as the provider, so whoever may create a LocalResource in a namespace can still read what the provider can read THERE. Closing that needs a SubjectAccessReview against the CR's author, which the CR does not record — left as explicit follow-up rather than half-built here. SECURITY.md says so too, so a future reporter is not told it is already handled. BREAKING for any LocalResource that reads across namespaces. Measured on krateo-057 first: of 42 LocalResources, 0 use fromRef at all, so the observed blast radius is nil — but that is one cluster, so the refusal names both the rejected and the permitted namespace instead of failing silently or returning an empty read. The rule is a pure function so it is testable: SyncLocalResources clones a git repository long before reaching it. Also adds SECURITY.md. Private vulnerability reporting is already enabled on the repository; the file points at it and describes the two properties that shape most reports here — the provider acts with its own identity, and it is an egress path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017tUn92TdGikKGEN5opGD7q
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.
Closes #10.
The shape
LocalResourcelet its author name any object in any namespace viaspec.fromResource.fromRef, and the provider fetched it with its own ServiceAccount — which holdsapiGroups:["*"] resources:["*"] verbs:["get"]cluster-wide, precisely because cross-namespace refs were the intent. The object was then serialised in full (WriteK8sResourcestrips nothing, soSecret.datais included) and committed to a git remote the same CR names.Namespaced create rights on one CRD therefore conferred cluster-wide read plus an egress path. Confused deputy.
Verified still present on
0.15.3before writing anything —localresource.go:422-435used the namespace verbatim, andgrep -rniE 'SubjectAccessReview|Impersonat|AuthorizationV1'returned nothing anywhere in the repo.The change
fromRefmust name the referencing LocalResource's own namespace. An empty namespace stays permitted — that's the cluster-scoped read path, not the author-directed escalation this closes.What it does NOT do — stated, not implied
It removes the author's ability to aim the provider's identity at another namespace. It does not make the read authorized: a same-namespace read still runs as the provider, so whoever may create a LocalResource in a namespace can still read what the provider can read there.
Closing that needs a
SubjectAccessReviewagainst the CR's author, which the CR doesn't record. Left as explicit follow-up rather than half-built — andSECURITY.mdsays so too, so a future reporter isn't told it's already handled.Breaking for any LocalResource reading across namespaces. On krateo-057: of 42 LocalResources, 0 use
fromRefat all. Observed blast radius nil.That's one cluster, so the refusal names both the rejected and the permitted namespace rather than failing silently or returning an empty read:
I deliberately did not add an allow-list or opt-in escape hatch: that's machinery for a use case with zero current users, and an authorization design deserves doing properly when someone needs it.
Tests
The rule is a pure function so it's testable —
SyncLocalResourcesclones a git repository long before reaching it. Covers same-namespace, empty, and two refusals, asserting the message names both namespaces.Also
Adds
SECURITY.md. Private vulnerability reporting is already enabled (the issue predates that); the file points at it and documents the two properties that shape most reports here — the provider acts with its own identity, and it is an egress path.🤖 Generated with Claude Code
https://claude.ai/code/session_017tUn92TdGikKGEN5opGD7q