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
Membership facts and catalogue access
Select a component to see its role and 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:
- One unambiguous namespace owner reference names the Tenant with the matching API version, name and UID.
- The Tenant's
status.spacescontains the same namespace name and UID. metadata.generationequalsstatus.observedGenerationon the Tenant.- 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
statusfields or ownership references to make this test pass; the tenant controller must produce them. - Agree on identity names. Direct
Userassignments must match the verified OIDCsub;Groupassignments 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:
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: gdsgateSet 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
[[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:
gdsgate --config operator-workstation.toml authority nodes listUse that UUID, not the local connector.id display name. Add the same rule on
each serving Authority, replacing the example UUID:
[[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:
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:
[[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:
gdsgate config check /etc/gdsgate/connector.toml --role connector
gdsgate config check /etc/gdsgate/authority.toml --role authorityAs a cluster administrator, inspect the controller-produced state using your explicit administrative kubeconfig:
kubectl --kubeconfig "$ADMIN_KUBECONFIG" get tenants.capsule.clastix.io -o json
kubectl --kubeconfig "$ADMIN_KUBECONFIG" get namespaces -o jsonCompare 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:
gdsgate --config client.toml login --browser
gdsgate --config client.toml resources list
gdsgate --config client.toml status --refresh --explainUse 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.