Integrate

Capsule tenant visibility

Use Capsule tenant membership to decide which Kubernetes-discovered resources a user sees in gdsgate. Keep connection and operation permissions in Cedar. This optional integration is available in 0.11.0-rc.8 and is disabled by default.

0.11.x

Use Capsule tenant membership to decide which Kubernetes-discovered resources a user sees in gdsgate. Keep connection and operation permissions in Cedar. This optional integration is available in 0.11.0-rc.8 and is disabled by default.

For an organization that already translates project membership into Capsule assignments, the existing tenant controller remains the source of membership. gdsgate reads its resolved state; administrators do not need to copy a separate user-to-namespace matrix into gdsgate. The Connector collects facts, and Authority combines them with the authenticated identity and the existing policy.

What the integration decides

For a protected discovery source, a resource appears in the user catalog only when its source facts are fresh, its Kubernetes object belongs to a verified tenant namespace, the user matches a tenant assignment, and Cedar permits view. Automatically published resources must also retain their normal live catalog lease and verified source provenance.

Decision Owner
Which Kubernetes objects discovery considers Connector source kinds and label_selector
Which tenant owns a namespace Reconciled Capsule state and reciprocal object UIDs
Whether a human belongs to that tenant Explicit tenant assignments matched to verified sub or IdP groups
Whether a member may see the resource Existing Cedar view policy
Whether a user may connect, query a DB or call Kubernetes Existing Cedar connect, dbQuery and kubeRequest policies
DB credentials and db_roles Existing backend configuration and DB authorization

Visibility does not replace connection authorization. A user who knows the exact ID of a hidden resource can still connect if Cedar permits connect. Removing tenant membership hides future catalog results; it does not terminate existing sessions. Put operational restrictions in the appropriate Cedar rules. Tenant ClusterRole names such as admin, edit, view or custom names do not become Cedar groups or DB roles, and do not automatically select RO/RW access.

Ordinary namespace or workload labels cannot grant tenant membership. They may select discovery targets; they are not evidence of ownership. Access to modify Tenant assignments and the controllers that produce them is therefore part of the integration's trust boundary.

Data flow

Interactive topology

Membership facts and catalogue access

Select a component to see its role and connections.

Illustration
P → TT → KK → CmTLS: independent fact publicationA → DI → RD → RR → UMembershipProject identityCapsuleTenant assignmentsKubernetes APITenants · namespacesConnectorCollects factsWrite leaderSeals publicationFact storeFixed expiryOIDC identityVerified subjectServing AuthorityMembership + CedarCatalogueFiltered resources
Selected componentSelect a component

Select a component to see its connections.

    The same Connector runs two independent tasks: ordinary target discovery and membership collection. They have separate intervals, generations and errors. There is no additional Collector service. A membership read failure does not abort the normal discovery probe or withdraw its routes.

    Authority accepts facts through a dedicated publication RPC from an active, enrolled Connector over mTLS. It takes the Connector's persistent node UUID from the authenticated peer and requires an exact central node/source rule. An arbitrary client cannot choose that identity or activate filtering itself.

    Facts are shared through the Authority database, with integrity protection, monotonic source generations and a fixed expiry. The current HA write leader accepts updates; each serving Authority reads and verifies the shared facts for user listings. There is no separate persistent Authority membership cache to synchronize. All serving Authorities need identical rules and the existing shared inventory integrity keys.

    How namespace ownership is verified

    The adapter reads capsule.clastix.io/v1beta2 Tenants and core namespaces. It requires all of the following:

    1. One unambiguous namespace owner reference names the Tenant with the matching API version, name and UID.
    2. The Tenant's status.spaces contains the same namespace name and UID.
    3. metadata.generation equals status.observedGeneration on the Tenant.
    4. The association and assignments remain unchanged across membership reads before and after the selected service/pod enumeration.

    Deleting, unassociated, ambiguous or unreconciled namespaces confer no visibility. Recreating a namespace under the same name does not preserve its old UID association. Cluster-scoped Node targets have no tenant namespace and are hidden when their source is protected.

    Resolved Tenant.status.owners is the input for owner assignments, including owners selected through TenantOwner resources. gdsgate does not independently evaluate their selectors or list TenantOwner objects. Capsule reconciliation is eventually consistent: matching observed generation is not proof that every external TenantOwner change has already propagated.

    The adapter was exercised with Capsule 0.14.6. It accepts only the stated v1beta2 schema and requires these status fields; the API version string alone does not establish compatibility with an older controller. For upstream setup, see the Capsule quickstart and permission model.

    Before enabling

    • Upgrade all serving Authorities and the collecting Connector to a build that contains this rc.8 integration. Older Authorities do not enforce these rules.
    • Have a working enrolled Connector, Kubernetes source and OIDC configuration. See discovery sources, enrollment and OIDC.
    • Verify that Capsule has reconciled the intended tenants and namespaces. Do not manufacture status fields or ownership references to make this test pass; the tenant controller must produce them.
    • Agree on identity names. Direct User assignments must match the verified OIDC sub; Group assignments must match gdsgate's verified canonical groups, with any comparison prefixes described below.
    • Keep Authority clocks synchronized and use the same rules on every instance.

    The following snippets extend existing role configurations. They do not replace transport, enrollment, OIDC, policy or integrity-key settings.

    1. Give the source read access

    Use the existing Kubernetes credential of the discovery source. For a Connector running in Kubernetes, the following is a minimal reader for services and pods. Apply it as a cluster administrator after creating the gdsgate namespace:

    yaml
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: gdsgate-discovery
      namespace: gdsgate
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: gdsgate-tenant-discovery
    rules:
      - apiGroups: ["capsule.clastix.io"]
        resources: ["tenants"]
        verbs: ["list"]
      - apiGroups: [""]
        resources: ["namespaces", "services", "pods"]
        verbs: ["list"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: gdsgate-tenant-discovery
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: gdsgate-tenant-discovery
    subjects:
      - kind: ServiceAccount
        name: gdsgate-discovery
        namespace: gdsgate

    Set spec.template.spec.serviceAccountName: gdsgate-discovery on the Connector workload and mount the normal CA and projected ServiceAccount token. If automatic mounting is disabled, configure a projected token volume explicitly. The source reads the token file so token rotation does not require embedding a token in TOML. For an external Connector, supply equivalent CA/token files through your existing credential-delivery mechanism.

    Remove pods from both RBAC and the source kinds if only services are needed. The adapter needs no Secret reads, writes, impersonation, or TenantOwner reads. tenant_allowlist is a collection filter, not a Kubernetes RBAC restriction: the credential must still be able to list the tenant and namespace collections.

    2. Configure the Connector

    toml
    [[connector.discovery_sources]]
    id = "prod-kube"
    kind = "kubernetes"
    api_url = "https://kubernetes.default.svc"
    ca_path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
    token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
    
    [connector.discovery_sources.discovery]
    mode = "list_kube_targets"
    interval_secs = 30
    label_selector = "gdsgate.io/expose=true"
    sources = ["service", "pod"]
    
    [connector.discovery_sources.tenant_visibility]
    enabled = true
    provider = "capsule"
    api_version = "v1beta2"
    refresh_interval_secs = 30
    tenant_allowlist = ["orders", "payments"]

    In automatic publication mode the enrollment assignment supplies the discovery scope. Setting [catalog].publication = "auto" on Authority sets the default for new enrollments; it does not convert an existing manual assignment. See publication modes.

    The label selector above chooses targets to enumerate. A selected target must still have a verified tenant association and pass membership and policy checks. Omit tenant_allowlist or set it to [] to include all tenants.

    Collection key Default Meaning
    enabled false Start independent membership collection for this Kubernetes source.
    provider "capsule" Only supported adapter.
    api_version "v1beta2" Only supported API schema.
    refresh_interval_secs 30 Poll interval, from 1 to 3600 seconds.
    tenant_allowlist [] Exact Tenant names; empty means all.

    Collection can be enabled only for a Kubernetes source. A PostgreSQL source cannot collect Capsule facts. Kubernetes-discovered database endpoints and explicitly associated manual database resources can use this filtering.

    3. Configure every Authority

    Find the Connector's persistent UUID in the node registry from an authorized operator workstation:

    Shell
    gdsgate --config operator-workstation.toml authority nodes list

    Use that UUID, not the local connector.id display name. Add the same rule on each serving Authority, replacing the example UUID:

    toml
    [[catalog.visibility_rules]]
    connector_id = "12345678-1234-1234-1234-123456789abc"
    source_id = "prod-kube"
    provider = "capsule"
    user_claim = "sub"
    user_prefix = ""
    group_prefix = ""
    include_additional_role_bindings = false
    max_staleness_secs = 90
    on_error = "hide"

    There is no separate Authority enabled flag. This rule activates the restriction for that exact enrolled Connector and local source ID. Other sources keep their existing behavior. Changing these Authority rules requires a restart; they are not hot-reloaded. Connector collection settings follow the normal Connector configuration reload lifecycle.

    Authority key Default Meaning
    connector_id required Enrolled node UUID authenticated by mTLS.
    source_id required Exact source id on that Connector.
    provider "capsule" Only supported adapter.
    user_claim "sub" Only supported direct-user identity claim.
    user_prefix "" Prepend to the verified subject for assignment comparison.
    group_prefix "" Prepend to each canonical IdP group for comparison.
    include_additional_role_bindings false Also count explicit subjects in spec.additionalRoleBindings.
    max_staleness_secs 90 Maximum original sample age, from 1 to 3600 seconds; see the publication budget below.
    on_error "hide" Only supported failure mode.
    manual_bindings [] Explicit manual resource/object associations.

    Configure both sides before relying on the filter. A central rule with no accepted facts hides the protected source. Collection without a matching rule does not filter anything, and its fact publication is refused. Both cases leave ordinary target discovery running.

    Match users and groups

    Suppose the authenticated identity has sub = "subject-123" and canonical groups ["orders-developers", "staff"]:

    Tenant assignment Rule setting Matches?
    User: subject-123 empty user_prefix yes
    User: alice@example.com empty user_prefix no, unless that value is actually the verified sub
    User: oidc:subject-123 user_prefix = "oidc:" yes
    Group: orders-developers empty group_prefix yes
    Group: oidc:orders-developers group_prefix = "oidc:" yes
    ServiceAccount: subject-123 any no

    Prefixes change only assignment comparison. They do not rewrite the principal, create groups or change Cedar entities. Configure group extraction in the OIDC claim mapping; arbitrary claim paths and username/email matching are not additional tenant-rule options. A person in several tenant groups sees the union of their eligible resources, still subject to Cedar view.

    By default, only resolved owners count. Set include_additional_role_bindings = true centrally if explicit User/Group subjects in additional role bindings should also count as members. This option does not inspect the privileges of their ClusterRole or translate it to a DB role. Do not enable it merely because a role happens to be named view.

    4. Keep policy explicit

    Existing Cedar policies remain active. Tenant membership cannot make a resource visible when view is denied, and view does not grant connect. For a manually declared example database, an ordinary group rule can remain:

    cedar
    permit (principal in Group::"orders-developers",
            action == Action::"view",
            resource == Database::"orders-db");
    permit (principal in Group::"orders-developers",
            action == Action::"connect",
            resource == Database::"orders-db");

    For automatically generated resources, use the real public resource IDs or your existing policy's resource attributes. Local Kubernetes object IDs are not interchangeable with public catalog IDs. Review policy examples and validate changes through the policy lifecycle.

    The same membership can therefore admit discovery while policy requires an approval or denies a production connection. Configure RO/RW profiles through the normal DB backend and Cedar context.db_role rules independently.

    Manual publication and existing resources

    For an already declared resource, bind its exact public catalog ID to a selected object in the same source. Add this table under its Authority visibility rule:

    toml
    [[catalog.visibility_rules.manual_bindings]]
    resource_id = "orders-db"
    object_id = "service/orders-dev/database"

    The object must exist, pass the source selector and kind selection, and appear in a fresh fact snapshot with the matching namespace UID. Accepted object ID forms are service/<namespace>/<name> and pod/<namespace>/<name>. Missing objects, excluded tenants or missing membership hide the associated resource. Bind the logical database parent to restrict its synthesized inventory children.

    This association is an administrative assertion that the resource belongs to that Kubernetes object. gdsgate does not compare the separately configured backend IP/address with the Service endpoint. Resource labels cannot create or override this association.

    With a manual enrollment, the Connector example above can omit discovery scope and collect facts only: it adds no catalog entries or routes. Keep the existing manual backend and resource declaration. If that source already publishes discovered targets manually, retain its configured scope. An automatic enrollment supplies a scope and continues ordinary automatic publication; omitting the local field does not turn automatic enrollment into facts-only mode.

    Freshness, failures and HA

    max_staleness_secs measures age from the original sample, not from the latest catalog read. Publication deducts collection time and the entire 20-second RPC budget. At the default 90 seconds, newly received facts have at most about 70 seconds left, less collection time. Choose a limit comfortably above the RPC budget plus collection time and refresh interval. Values at or below 20 seconds pass the numeric range check but leave no usable publication budget.

    Event User catalog effect Ordinary discovery/routes
    Successful fresh membership update Re-evaluate membership and Cedar view Continue normally
    Tenant API 403, timeout or invalid response Publish unavailable facts and hide protected resources when accepted Continue independently
    Failure replacement cannot reach Authority Previously accepted facts keep only their remaining lifetime, then hide Follow existing route/lease rules
    Missing facts, old Connector or wrong source ID Hide protected resources No membership-driven route withdrawal
    Namespace UID mismatch or unreconciled ownership Omit that namespace's resources Continue independently
    Fact-store read failure Listing reports unavailable; no unfiltered fallback Existing sessions continue
    Invalid fact integrity Facts confer no visibility Existing routing authorization remains in force
    Authority restart or follower read Use shared facts and their original expiry Existing HA behavior

    Increasing the configured TTL cannot extend an already sealed expiry; a stricter current limit can shorten it. Reads and restarts do not renew facts. Future receipt timestamps are hidden. Shared storage does not synchronize local rule files: distribute the same rules to all Authorities and restart them through your normal HA procedure.

    Collection attempts are bounded to five seconds, with at most four concurrent membership probes. Each Kubernetes list has an 8 MiB limit; incomplete paginated lists are refused. Fact payloads are limited to 1 MiB, with at most 5000 namespace entries and 5000 selected objects. Test representative tenant and object counts before applying this adapter to a large cluster; an allowlist does not remove the underlying full-list API size limit.

    Membership changes become effective after upstream reconciliation and the next accepted probe. IdP group changes also depend on the verified token being refreshed. This is not an immediate session-revocation mechanism.

    Verify with several identities

    First validate the completed files on their role hosts:

    Shell
    gdsgate config check /etc/gdsgate/connector.toml --role connector
    gdsgate config check /etc/gdsgate/authority.toml --role authority

    As a cluster administrator, inspect the controller-produced state using your explicit administrative kubeconfig:

    Shell
    kubectl --kubeconfig "$ADMIN_KUBECONFIG" get tenants.capsule.clastix.io -o json
    kubectl --kubeconfig "$ADMIN_KUBECONFIG" get namespaces -o json

    Compare status.owners, status.spaces, observed generation and namespace owner references. Inspect additional role bindings only if the central rule includes them. These administrative reads are distinct from logging in as an end user.

    After applying the reader permissions, reloading the Connector and restarting the configured Authorities, allow a successful membership probe. Check Connector/Authority logs for failed API reads or refused fact publication. Then test from separate OS accounts or isolated client state directories, each with its own login and Resident:

    Shell
    gdsgate --config client.toml login --browser
    gdsgate --config client.toml resources list
    gdsgate --config client.toml status --refresh --explain

    Use the normal Resident setup for status. Merely changing the config path is not proof that a running Resident now belongs to a different user; isolate state or stop/sign out the previous client first.

    For a test fixture with two selected database Services in the orders tenant and one in payments, the following are expected results, assuming policy allows view for each test identity and all facts and catalog leases are fresh:

    Identity assignment Expected visible Services
    Direct owner of orders 2 orders
    Group owner of payments 1 payments
    Member of both owner groups All 3
    Only an additional binding in orders 0 by default; 2 with the central option enabled
    No matching assignments 0

    Also deny view for one otherwise eligible resource and confirm it disappears. Test connect separately: membership alone grants no connection. Status source grouping contains only visible resources; a serving indicator describes route availability, not membership. Administrative authority resources list uses its existing manageCatalog authorization and is intentionally outside this user filter, so it is not a substitute for end-user checks.

    In an isolated test environment, revoke the reader's Tenant-list permission and confirm protected entries hide while normal target discovery continues. Restore it and confirm the next accepted probe restores eligible entries. Interrupt fact delivery separately to check finite expiry. An API failure and a delivery failure exercise different paths.

    Troubleshooting

    Symptom Check
    All protected entries disappear immediately Exact enrolled UUID/source ID, matching central rule, accepted fact publication, shared integrity keys and clocks.
    Collector reports 403 Reader ClusterRoleBinding, mounted credential and list permission for Tenants, namespaces and selected target collections.
    One namespace is absent Tenant allowlist, reciprocal namespace/Tenant UIDs, deletion state and observed generation.
    One selected resource is absent Label/kind selection, object present in snapshot, live catalog lease and Cedar view.
    User assignment looks correct but does not match Actual verified sub, canonical groups, prefixes and a refreshed login; a display name is not sub.
    Additional reader sees nothing Default is to exclude additional bindings; review the central option. ClusterRole names have no membership mapping.
    Manual DB does not appear Exact public resource/parent ID, exact selected object ID and fresh same-source facts.
    Collection runs but everyone still sees existing policy results Collection alone activates no filter; install the exact central rule everywhere.
    Results differ across Authorities Rule files/build versions differ, keys differ, unsynchronized clocks, or a database error.
    Resources appear and disappear between polls TTL too close to RPC/collection/refresh budget, slow API or publication failures.

    Upgrade and disable

    The additive m0088_tenant_visibility migration creates shared fact storage; it does not rewrite resource declarations. Back up the database and integrity keys together. Restoring old database state also restores old source generation floors; follow the existing coordinated restore procedure. See the upgrade guidance.

    When disabled on the Connector, collection does not read its fact journal or call the Tenant API. With no Authority visibility rules, user listing follows the existing Cedar path without reading tenant facts. The migration still runs as part of upgrading Authority.

    To stop collection while retaining the restriction, disable its Connector block: the central rule remains, and affected entries hide once existing facts expire. To return to policy-only discovery, deliberately remove the central rule from all serving Authorities and restart them; this broadens visibility to what Cedar view already permits. Then disable collection and remove unneeded reader permissions. Removing only the Connector block does not remove a central rule.

    Reference: Authority catalog settings and Connector collection settings.

    Search this documentation edition.