Skip to content

Per-tenant credential isolation when atespaces are created at run time #2876

Open

Description

Problem

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.

Activity

  1. QuentinBisson commented on Oct 20, 2026

    @QuentinBisson
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/networkarea/securitySecurity related issue/prkind/featureAn enhancement / feature request or implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions