You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Per-tenant credential isolation when atespaces are created at run time #2876
We run Substrate as a shared platform for several tenants. Each tenant gets a Kubernetes namespace, labeled as a tenant and created at run time when the tenant is onboarded. Its agents run in an atespace of the same name, and its model keys, MCP headers and git credentials are Secrets in that namespace.
The Kubernetes secrets provider reads a fixed atespace-to-namespace list once at startup (--namespace-policy-file), so every onboarded tenant needs a policy edit and a provider restart. Since #2543 golden actors run in the template's atespace, so a per-atespace grant now covers template boots too.
Proposal
A namespace policy can carry ownNamespaceSelector (a LabelSelector) in place of atespace and allowedNamespaces: each namespace it matches is readable by the atespace of the same name, and by no other. The provider watches namespaces (only when a policy selects them) and matches each request against the namespace's current labels, so a tenant namespace created, labeled or deleted after startup needs no restart. secretSelector narrows a selected grant as it narrows a listed one, and the provider's ClusterRole gains list/watch on namespaces.
Equal names are a deployment convention here, not something Substrate would enforce (atespaces stay decoupled from namespaces, #18). If an explicit mapping is preferred, a namespace label whose value names the atespace works the same way: whoever can label namespace N can only grant access to N's own Secrets. Happy to go either way and send a PR once the direction is agreed.
Following the discussion on #2877 (reload in #2959 first, the selector too specific), here is the RBAC shape for Taahir Ahmed (@ahmedtd) to look at. Refs are main at af5b0726a.
Proposal: a kubernetes authorization mode on the provider, exclusive with the policy file.
authorize issues a SubjectAccessReview for user substrate:atespace:<atespace> (group substrate:atespaces), verb get, resource secrets, with the namespace and name from the URI. The atespace is the one the egress gateway already puts in actor_spiffe_id (kubeprovider.go:202); the clientset and authorization/v1 are already there (main.go:183-197, vendored).
A tenant grant is then a RoleBinding in the tenant's namespace to a Role with get on secrets (resourceNames to narrow). Nothing to reload or watch; the grant is written and audited like any other RBAC object.
The manifest adds create on subjectaccessreviews; secrets: get stays. Decisions cached briefly, bounded by the request deadline; an unanswered review is Unavailable, never a grant.
Tests: own namespace granted via a RoleBinding; another namespace refused before any Secret read; a RoleBinding added and removed after start takes effect without a restart; unanswered review is Unavailable; mode plus policy file fails loading.
Security point for review. The one convention is the identity mapping: the provider trusts the gateway's actor_spiffe_id as today and maps it to a user name in a reserved prefix. A RoleBinding to that name is only as safe as who can write RoleBindings in the namespace, which is the namespace admin who owns the Secrets anyway.
#2296's case (a prefix of atespaces reading Secrets labelled for them) is a ClusterRole bound to the substrate:atespaces group, or one RoleBinding per atespace with resourceNames.
Activity
QuentinBisson commentedon Oct 20, 2026
Following the discussion on #2877 (reload in #2959 first, the selector too specific), here is the RBAC shape for Taahir Ahmed (@ahmedtd) to look at. Refs are
mainataf5b0726a.Proposal: a
kubernetesauthorization mode on the provider, exclusive with the policy file.authorizeissues aSubjectAccessReviewfor usersubstrate:atespace:<atespace>(groupsubstrate:atespaces), verbget, resourcesecrets, with the namespace and name from the URI. The atespace is the one the egress gateway already puts inactor_spiffe_id(kubeprovider.go:202); the clientset andauthorization/v1are already there (main.go:183-197, vendored).getonsecrets(resourceNamesto narrow). Nothing to reload or watch; the grant is written and audited like any other RBAC object.createonsubjectaccessreviews;secrets: getstays. Decisions cached briefly, bounded by the request deadline; an unanswered review isUnavailable, never a grant.Unavailable; mode plus policy file fails loading.Security point for review. The one convention is the identity mapping: the provider trusts the gateway's
actor_spiffe_idas today and maps it to a user name in a reserved prefix. A RoleBinding to that name is only as safe as who can write RoleBindings in the namespace, which is the namespace admin who owns the Secrets anyway.#2296's case (a prefix of atespaces reading Secrets labelled for them) is a ClusterRole bound to the
substrate:atespacesgroup, or one RoleBinding per atespace withresourceNames.