Repository navigation
feat(credentials): narrow a namespace grant by secret name or label - #1859
Merged
Eitan Yarmush (EItanya) merged 8 commits intoOct 1, 2026
Conversation
Jonathan Jamroga (jjamroga)
force-pushed
the
jjamroga/credential-provider-secret-scoping
branch
from
September 24, 2026 16:54
8d08a1e to
9f326e9
Compare
A grant is the whole namespace. The provider holds `get` on every Secret and the policy checks only the namespace, so granting an atespace the namespace its model credentials sit in also grants it everything else there. That is a thin line wherever actors and the control plane share a namespace -- installing Substrate as a subchart of its consumer puts them together -- because the same grant then reaches actor-id-ca-pool, and an actor holding the CA that signs actor identities can mint any actor. Add allowedSecretNames and secretSelector to a policy, so a grant can name the credentials it means and nothing else, or label them and let the label carry it. Both unset keeps the namespace-wide grant every existing policy already means, so no policy has to change. The criteria of one policy are AND-ed and the policies are OR-ed, as they are in a Kubernetes RBAC rule. A policy carrying a namespace, a name and a selector admits only a Secret in that namespace, with that name, carrying those labels; to admit either, write a second policy. Each policy therefore keeps its own grant: merging them would AND the selectors of separate policies together, and would let a narrow policy take away the namespace a broad one granted. secretSelector mirrors the shape of ateapi.Selector, so a policy selects Secrets the way an ActorTemplate selects workers, and matchLabels is exact equality as it is there. It is nested rather than a bare matchLabels: the common grant narrows by label alone, where the only neighbouring field names namespaces, and a bare matchLabels there reads as though it selected the namespaces. The shape is mirrored rather than imported -- this is a config file, not the API. Namespace and name are settled before anything is read, so a Secret no policy names is refused without a request to Kubernetes and cannot be probed for existence. A grant narrowed by label needs the Secret in hand, and there the refusal has to hide existence too: a Secret that is absent and one whose labels do not match are refused with the same code and the same message, so a caller cannot walk a list of names and learn what the namespace holds. A grant that is not narrowed by label has already been admitted to every name there, so NotFound tells it nothing new and is kept. A malformed name or label would narrow a grant to nothing and read as the policy being ignored, so loading rejects it rather than failing open later. Signed-off-by: Jonathan Jamroga <[email protected]>
Jonathan Jamroga (jjamroga)
force-pushed
the
jjamroga/credential-provider-secret-scoping
branch
from
September 24, 2026 17:03
9f326e9 to
50d1560
Compare
Jonathan Jamroga (jjamroga)
marked this pull request as ready for review
September 24, 2026 17:05
Eitan Yarmush (EItanya)
left a comment
Collaborator
There was a problem hiding this comment.
I like the idea of adding label selectors, but I think this PR is a bit overcomplicated. Can we stick with mostly what was here before with added label selection?
Keep only label-based narrowing (SecretSelector) for scoping which Secrets an atespace grant may resolve. Per review feedback, name-based narrowing (AllowedSecretNames) added unnecessary complexity; label selection alone covers the intended use case.
Eitan Yarmush (EItanya)
left a comment
Collaborator
There was a problem hiding this comment.
🤖 AI-generated review. I looked through the findings and agree with them.
Per review: replace the pre-fetch Decision enum with two plain methods on NamespaceAuthorizer. authorize() now returns just (atespace, error) before the Secret is fetched. AllowedUnconditionally reports whether any grant for the atespace/namespace needs no label check, used only to decide whether a missing Secret may be reported as NotFound. AllowedSecret checks the fetched Secret against any matching grant, including an unrestricted one. Existence is still hidden the same way for grants narrowed by label. Also switch namespace policy YAML parsing to yaml.UnmarshalStrict, so a typo like matchLabel, or an unsupported field like matchExpressions, fails to load instead of silently parsing as an empty selector that grants every Secret in the namespace.
Drop AllowedUnconditionally. AllowedSecret(atespace, namespace, nil) already answers the same question: a grant with no label selector matches any labels, including none, so the pre-fetch NotFound check reuses the one method instead of a separate one. NamespaceAuthorizer is now exactly two decision methods matching the two steps left to it: Allowed (is the namespace granted) and AllowedSecret (does this Secret, or the absence of one, satisfy a grant). The third step, checking the callers identity, already lived in Server.authorize and is unchanged.
Once a namespace policy is enforced, report PermissionDenied for a missing Secret unconditionally, instead of first asking the authorizer whether this atespace holds an unrestricted grant. The distinction was not worth its complexity: a caller gains nothing from a precise NotFound when enforcement is on, and dropping it removes the only caller of AllowedSecret with a nil labels argument. NotFound is still returned verbatim when nsAuth is nil, which only happens in tests: production always loads a policy because main requires --namespace-policy-file. Clarified that in the doc comments.
Eitan Yarmush (EItanya)
approved these changes
Oct 1, 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.
What
Adds an optional
secretSelectorfield so a credential-provider namespace grant can be narrowed by label to specific Secrets instead of the whole namespace:No selector set → unchanged behavior (full-namespace grant).
Why
A namespace-only grant lets any actor read any Secret in that namespace, including Substrate's own CA/JWT/DB Secrets when they share a namespace with actors (e.g. kagent as a Substrate subchart). RBAC
resourceNamescan't scope this per-atespace, since the provider is a single ServiceAccount. The policy is the only place with that context.Behavior
PermissionDenied— so a caller can't probe what exists in the namespace.Tests
New unit tests cover label narrowing, the no-probing property, and malformed-policy rejection. Existing tests pass unchanged.
go build ./...and lint/verify scripts pass.