# kubernetes toolpass asks the API server with a `SubjectAccessReview`: "may user U with groups G do verb V on resource R in namespace N?". The API server answers from RBAC and every other authorizer it has (webhooks, ABAC, Node). Nothing is persisted. ## Credential A ServiceAccount whose only permission is creating SubjectAccessReviews. That is one ClusterRole rule: ```yaml rules: - apiGroups: ["authorization.k8s.io"] resources: ["subjectaccessreviews"] verbs: ["create"] ``` `test/kind/toolpass-rbac.yaml` has the full ServiceAccount, ClusterRole and ClusterRoleBinding. This is tighter than the built-in `system:auth-delegator`, which also allows TokenReviews. A projected token (`kubectl create token toolpass -n toolpass`, or a projected volume in a pod) expires; reference it with `credential: file:/path` and toolpass re-reads it on every use. ## Connection ```yaml - id: k8s-prod-eu integration: kubernetes url: https://10.20.0.5:6443 ca_file: /etc/toolpass/ca/prod-eu.pem # the cluster CA tls_server_name: kubernetes # when the URL is an IP credential: file:/secrets/k8s-prod-eu-token username_template: "oidc:{email}" # optional, default {email} group_prefix: "oidc:" # optional add_authenticated_group: "true" # optional, default true oidc_issuer: https://sso.example.com # optional: read the groups from the user's ID token oidc_audience: kubernetes # (the API server's --oidc-issuer-url and --oidc-client-id) # groups_connection: aws-prod # or: EKS access entries / a directory, from the email ``` | Key | Meaning | |---|---| | `url` | API server URL | | `credential` | ServiceAccount token, `env:` or `file:` | | `username_template` | How the API server names your users. Placeholders `{email}`, `{local}`, `{domain}`. Must contain `{email}` or `{local}`. A name that comes out starting with `system:` (Kubernetes' own ServiceAccounts and components) is refused (`invalid_request`). `{local}` drops the domain, so it suits a single-domain directory | | `group_prefix` | Prefix the API server puts on groups from your identity provider. Without it a caller group starting with `system:` is rejected (`invalid_request`), since it would name a built-in group such as `system:masters` | | `add_authenticated_group` | Also send `system:authenticated`, as the API server would for any logged-in user | | `oidc_issuer`, `oidc_audience`, `oidc_groups_claim`, `oidc_email_claim` | Read the user's groups from the ID token sent with the check when the caller sends none: the API server's `--oidc-issuer-url`, `--oidc-client-id` and `--oidc-groups-claim` (or the structured authentication config). See [Groups from the user's token](../guides/groups.md). Not with `groups_connection` | | `groups_connection` | The id of an `aws`, `googleworkspace`, `microsoft365`, `okta` or `fake` connection that names the user's groups from the email when the caller sends none. See [Groups from the email](#groups-from-the-email) | | `eks_cluster` | The EKS cluster name. Required when `groups_connection` is an `aws` connection, refused otherwise | | `eks_aws_auth` | `true` also reads the `kube-system/aws-auth` ConfigMap for principals with no access entry (default `false`; needs one more RBAC rule, below) | toolpass has no user directory to consult. The Kubernetes username is a pure transform of the email, so the template must match the API server's `--oidc-username-prefix` / `--oidc-username-claim` flags or its structured authentication config. A wrong template silently denies everyone. Run one check for a user you know is allowed after configuring. Groups come from the caller's `groups` field, or, when the caller sends none, from the user's ID token (`oidc_issuer`, exactly what the API server's OIDC authenticator sees, `group_prefix` put in front) or from `groups_connection` (next section). Kubernetes has no user directory: a review sent with no groups is evaluated for a user in no group, so on a cluster that grants access through groups every such check is a false deny. Caller-sent groups always win, so an existing caller sees no change. Every holder of the API key can assert any group in the `groups` field. Set `group_prefix` to match your identity provider; then a caller's value can never name a group the API server reserves. Without a prefix, groups starting with `system:` are rejected with `invalid_request` rather than sent. ## Groups from the email With `groups_connection` set, toolpass asks that connection for the user's groups whenever the caller sends none, so the email alone is enough: | `groups_connection` names | What toolpass sends | Fits | |---|---|---| | [`aws`](aws.md) (with `eks_cluster`) | the `kubernetesGroups` of the EKS access entries of the user's IAM principals (the Identity Center roles of their permission sets, their IAM user, or their `role_map_file` roles), unioned; the entry's `username` when exactly one entry matched | EKS | | [`googleworkspace`](googleworkspace.md) | the email addresses of the user's Google groups (`gke_security_group` on that connection keeps only the groups GKE honours), and the account's primary email as the username | GKE with Google Groups for RBAC | | [`microsoft365`](microsoft365.md) | the object ids of the user's Entra groups, lower-cased, transitive | AKS with Entra ID integration | | [`okta`](okta.md) | the names of the user's Okta groups | an API server whose OIDC groups claim comes from Okta | | `fake` | the `groups` field of the fake connection | tests | Directory groups (Google, Entra, Okta) are treated exactly like groups a caller sent: `group_prefix` is put in front, and without a prefix a group starting with `system:` is refused. EKS access entries are different: their `kubernetesGroups` are Kubernetes group names the cluster admin chose, so they are sent as they are, with no prefix, and `system:masters` is allowed. A username a source names replaces `username_template`; a username starting with `system:` is refused either way. A user the source does not know, a suspended or disabled one, and (EKS) an IAM principal with no access entry are answered `user_not_found`, a deny, naming the principals that were tried. A directory user in no group is checked by username alone. ### EKS ```yaml - id: aws-prod integration: aws # ... as in aws.md; eks_region if the cluster is not in `region` - id: k8s-eks-prod integration: kubernetes url: https://ABCDEF0123456789.gr7.eu-west-1.eks.amazonaws.com ca_file: /etc/toolpass/ca/eks-prod.pem credential: file:/secrets/eks-prod-token groups_connection: aws-prod eks_cluster: prod eks_aws_auth: "true" # optional: clusters still mapping users in aws-auth ``` The aws connection's account role needs `eks:DescribeAccessEntry` on the cluster (see [aws](aws.md#eks-access-entries)). Each principal ARN is looked up as it is and then with its IAM path removed, since `AWSReservedSSO_*` roles have a path and EKS (UNVERIFIED for access entries, documented for aws-auth) matches them without it. In a username, `{{AccountID}}` is filled from the principal and `{{SessionName}}` / `{{SessionNameRaw}}` with the email (UNVERIFIED: the session name of an Identity Center role is taken to be the email, with `@` turned into `-` for `{{SessionName}}`); any other placeholder leaves the username to `username_template`. A user with several principals (several permission sets) gets the groups of all their entries together. A real session is one role at a time, but RBAC only adds permissions, so one action is allowed for the union exactly when one of the roles allows it; only bindings to the per-role usernames are lost, since no single username applies then. With `eks_aws_auth: "true"`, each principal with no access entry is also looked up in the `mapRoles` and `mapUsers` of the `kube-system/aws-auth` ConfigMap, as EKS falls back to aws-auth per principal. The ConfigMap is read with toolpass's own ServiceAccount and reused for a minute across users; the ServiceAccount then needs one more rule, a Role in `kube-system`: ```yaml rules: - apiGroups: [""] resources: ["configmaps"] resourceNames: ["aws-auth"] verbs: ["get"] ``` `toolpass probe` accepts that rule without the over-privilege warning when `eks_aws_auth` is on, and warns when the ConfigMap is missing or unreadable. ## Resources | Resource | Meaning | |---|---| | `namespace:` | a namespaced resource; add `?resource=[.]&name=` | | `cluster` | a cluster-scoped resource; add `?resource=nodes&name=` | | `nonresource:` | a non-resource URL such as `/metrics` or `/version` | Examples: `namespace:payments`, `namespace:payments?resource=deployments.apps&name=api`, `cluster?resource=nodes`, `nonresource:/metrics`. ## Actions | Action | Kubernetes verb and resource | |---|---| | `raw::[.][/]` | exactly that, e.g. `raw:create:deployments.apps`, `raw:get:pods/log`, `raw:use:podsecuritypolicies.policy` | | `raw:` | the verb on a `nonresource:` path | | `pods.exec` | create pods/exec | | `pods.logs` | get pods/log | | `pods.portforward` | create pods/portforward | | `pods.attach` | create pods/attach | | `scale` | update `/scale` | | `secrets.read` / `secrets.list` | get / list secrets | | `impersonate` | impersonate users (or `?resource=groups`, `serviceaccounts`) | | `deployment.create/update/delete/restart` | create/update/delete/patch deployments.apps | | `namespace.create/delete` | create/delete namespaces | | `rbac.bind` | create rolebindings.rbac.authorization.k8s.io | When both the action and the request name a resource they must agree. ## Decisions | API server says | toolpass answers | |---|---| | `allowed: true` | allow | | `allowed: false`, no `evaluationError` | deny | | `evaluationError` set | unknown (`unsupported`) | | 401 | unknown (`credential_rejected`): the token is invalid | | 403 | unknown (`credential_rejected`): the ServiceAccount may not create SubjectAccessReviews | ## Probe `toolpass probe` posts a SubjectAccessReview for a throwaway subject to prove the token may create them, then a SelfSubjectRulesReview (any authenticated subject may) and warns when the token can do anything beyond that one rule, listing the extra verbs and resources. ## What it cannot see - Admission: ValidatingAdmissionPolicy, admission webhooks, Pod Security admission and resource quotas run after authorization and can still reject a request toolpass allowed. - A wrong `username_template` denies everyone silently. - Groups are whatever the caller sends, or what `groups_connection` names when the caller sends none. Only the `system:` namespace is protected, and only when `group_prefix` is unset. ## Managed clusters (UNVERIFIED) - **EKS**: set `groups_connection` to an aws connection and `eks_cluster` (above). EKS access *policies* (`AmazonEKSClusterAdminPolicy` and the other AWS-managed policies associated with an access entry) are evaluated by EKS's own authorizer, not RBAC; whether a SubjectAccessReview sees them is unverified. A user whose access comes only from an access policy may be denied by toolpass and allowed by the cluster. Give access through `kubernetesGroups` and RBAC bindings to be sure. - **GKE**: the username is the Google email; set `groups_connection` to a googleworkspace connection with `gke_security_group`. Whether IAM-granted access (roles/container.*) appears in a review is unverified. - **AKS** with Entra: the username is the UPN (set `username_template` to match); set `groups_connection` to a microsoft365 connection for the group object ids. Azure RBAC for Kubernetes is unverified. ## Test Unit tests run against a fake API server, including a groups source of every kind and the aws-auth fallback. `test/kind/run.sh` creates a kind cluster, applies `test/kind/toolpass-rbac.yaml` and `test/kind/fixtures.yaml`, and runs the end-to-end tests (`toolpass-py/tests/e2e`) against it: among them, a connection whose `groups_connection` is a fake directory putting dana in `platform-team`, so dana is allowed `deployment.create` with no groups sent (denied on the connection without one), and a mapped `system:masters` group that the real API server honours. The directory halves (EKS, Google, Entra, Okta) are tested against fakes, and against the vendors' API descriptions where one is fetched; none against a live account.