Cluster access & authorization model (kubectl)¶
How a developer's kubectl request is authenticated and authorized end to end, and how access is granted and revoked. Code references point into inari-server unless noted.
Identity¶
- Tenancy is built on Keycloak Organizations in one
inarirealm. Users belong to groupstenant-<slug>/<team>. - Kubectl login uses the per-tenant public client
org-<slug>-kubectl, provisioned idempotently at tenant creation bytenancy.EnsureKubectlClient(internal/tenancy/identity.go:204, called fromtenancy.go). It allows the standard and device flows, mints tokens with audiencekubernetesand agroupsclaim carrying full group paths with a leading slash (/tenant-<slug>/<team>) — the exact strings the cluster-side RBAC bindings use as Group subjects. - Kubeconfigs are secret-free exec-credential configs rendered by
kubeproxy.RenderKubeconfig, served atGET /api/v1/tenants/{org}/clusters/{id}/kubeconfig(or the CLI). The hub never learns the tenant API-server URL (pull-only).
Authorization model (hub side)¶
- The permission catalog lives in
internal/authz/permissions.go.PermClustersKubectl = "clusters.kubectl"maps to the OpenFGA relationclusters_kubectlon the organization. The kubeproxy PEP checksRelationKubectl = "kubectl"on the cluster object per request (internal/kubeproxy/proxy.go), derived from the parent org'sclusters_kubectlvia the FGA model. - Built-in role bundles (
authz.BuiltinRoles): admin and operator includeclusters.kubectl; editor and viewer do not. Migration0031_roles_clusters_kubectl.sqlbackfilled existing admin/operator role rows. - Role and team management go through the tenancy REST API: roles CRUD
/api/v1/tenants/{org}/roles*, the permission catalog/api/v1/tenants/{org}/permissions/catalog, and team→role mappings. FGA tuples are written by the tuple-writer outbox handler (internal/authz/tuplewriter.go) from role/team/membership events. - PEP checks are cached by
authz.CachedAuthorizerkeyed(generation, user, relation, object)with TTLINARI_CACHE_PEP_TTL; every FGA write path goes throughauthz.InvalidatingStore, which bumps the generation. Cache failures fail open to direct OpenFGA.
Cluster-side enforcement¶
- The kubeproxy strips any client-supplied
Impersonate-*headers and mints its own from the JWT identity and/tenant-<slug>/...group claims. - The tunnel agent runs with an impersonate-only ServiceAccount and forwards the hub-minted headers (
inari-agent,internal/tunnelagent/relay.go); the inboundAuthorizationnever reaches the cluster. - Tenant-cluster RBAC binds the impersonated groups to anchor ClusterRoles
tenant-<slug>-{admin,operator,editor,viewer}plus per-team role-qualified bindingstenant-<slug>-<team>-<role>, materialized byinternal/rbacmaterializeintobaseline/rbac/of the tenant's<slug>-inari-staterepo and synced by the tenant-local ArgoCD.
Kill switches¶
Two distinct mechanisms:
kubectl_access.enabledruntime flag (kill-switch v2, ADR-0016) — reversible, platform- or cluster-scoped, set via the feature-flags REST API. Off → 410 on proxy/kubeconfig and tunnel-stream rejection, plusFlagWatcherclosing live sessions. An explicitly setINARI_KUBECTL_ACCESS_ENABLEDenv overrides runtime state.- Tunnel-client identity controls —
tenancy.DisableTunnelClient/RevokeTunnelClient(internal, Keycloak client disable/delete) and the terminal cluster lifecycle endpointsPOST /clusters/{id}/revokeand/decommission.
The UI connect tab reflects the flag via access-info (kubectlAccessEnabled, tunnelAvailable, tunnelUnavailableReason) and shows a read-only "disabled by administrator" notice.