# RBAC: how a ServiceAccount gets its permissions

> A CI deployer's permissions traced through RBAC: ServiceAccount, projected token, Role and RoleBinding, a ClusterRole reused in one namespace, and a ClusterRoleBinding for cluster-wide reads.

Interactive graph: https://k8wiz.com/graphs/rbac-serviceaccount-roles-bindings

RBAC permissions are always additive. A subject starts with nothing, and each binding adds the rules of one role. Nothing can deny. To answer "can this ServiceAccount do X?", you find every binding that names it and check their roles.

Where the binding lives decides the scope, not the role. A RoleBinding grants its role's rules in its own namespace only, even when the role is a ClusterRole. Only a ClusterRoleBinding grants across the whole cluster.

## Resources

### Pod ci-runner-8x2kq

created by a controller, v1, namespace ci

A CI job Pod that runs `kubectl apply` against the `apps` namespace. `serviceAccountName: deployer` decides which identity it gets.

### ServiceAccount deployer

you write it, v1, namespace ci

The identity. Its full username is `system:serviceaccount:ci:deployer`, and bindings in any namespace can name it. On its own it can do almost nothing.

### Projected token kube-api-access-…

created by a controller

The kubelet asks the API server for a short-lived token for `deployer` and mounts it at `/var/run/secrets/kubernetes.io/serviceaccount/token`. It is bound to this Pod, expires after an hour by default and is refreshed before that. No Secret is involved.

### API server kube-apiserver

comes with an add-on

Authenticates the token, then asks the RBAC authorizer whether `system:serviceaccount:ci:deployer` may do this verb on this resource in this namespace. If no binding allows it, the answer is 403 Forbidden.

### Role deployer

you write it, rbac.authorization.k8s.io/v1, namespace apps

Rules for deploying in `apps`: manage Deployments, Services, ConfigMaps and Ingresses. It leaves out Secrets on purpose, because `get` on Secrets would expose every credential in the namespace.

### RoleBinding deployer

you write it, rbac.authorization.k8s.io/v1, namespace apps

Gives the `deployer` Role to the ServiceAccount from the `ci` namespace. The binding lives in `apps`, so that's where the rules apply.

### ClusterRole view

comes with an add-on, rbac.authorization.k8s.io/v1

Built into every cluster. Read access to most namespaced objects, but not Secrets. It's an aggregated role, so CRDs can add their own rules to it with the label `rbac.authorization.k8s.io/aggregate-to-view: "true"`.

### RoleBinding deployer-view

you write it, rbac.authorization.k8s.io/v1, namespace monitoring

Reuses the `view` ClusterRole, but as a RoleBinding in `monitoring`, so the deployer can read there and nowhere else. This is how you avoid writing the same Role in every namespace.

### ClusterRole node-reader

you write it, rbac.authorization.k8s.io/v1

Read access to Nodes. Nodes are cluster-scoped, so only a ClusterRole can grant it, and only a ClusterRoleBinding makes it take effect.

### ClusterRoleBinding deployer-node-reader

you write it, rbac.authorization.k8s.io/v1

Grants `node-reader` across the whole cluster. The CI job uses it to wait for capacity before a rollout.

## How the resources connect

- Pod ci-runner-8x2kq → ServiceAccount deployer: serviceAccountName (references)
- Projected token kube-api-access-… → Pod ci-runner-8x2kq: mounted into (references)
- Projected token kube-api-access-… → ServiceAccount deployer: identifies as (references)
- Pod ci-runner-8x2kq → API server kube-apiserver: request + bearer token (traffic)
- API server kube-apiserver → RoleBinding deployer: checks (references)
- API server kube-apiserver → RoleBinding deployer-view: checks (references)
- API server kube-apiserver → ClusterRoleBinding deployer-node-reader: checks (references)
- RoleBinding deployer → ServiceAccount deployer: subject (references)
- RoleBinding deployer → Role deployer: roleRef (references)
- RoleBinding deployer-view → ServiceAccount deployer: subject (references)
- RoleBinding deployer-view → ClusterRole view: roleRef (references)
- ClusterRoleBinding deployer-node-reader → ServiceAccount deployer: subject (references)
- ClusterRoleBinding deployer-node-reader → ClusterRole node-reader: roleRef (references)

## How it works

1. **The Pod runs as the ServiceAccount.** The Pod names `deployer` in `serviceAccountName`. The kubelet requests a token bound to this Pod and mounts it, and kubectl inside the container finds it automatically.
2. **The API server checks every binding.** For `kubectl apply -n apps`, the API server authenticates the token as `system:serviceaccount:ci:deployer`, then looks for RoleBindings in `apps` and ClusterRoleBindings anywhere that name this subject. One allowing rule is enough.
3. **A RoleBinding grants a Role in one namespace.** The `deployer` RoleBinding in `apps` gives the `deployer` Role to the ServiceAccount from `ci`. The subject and the binding can be in different namespaces. The rules apply where the binding is.
4. **A RoleBinding can reuse a ClusterRole.** `deployer-view` points at the built-in `view` ClusterRole, but because it's a RoleBinding in `monitoring`, the deployer can read only that namespace. `kubectl get pods -n kube-system` still fails.
5. **Cluster-scoped objects need a ClusterRoleBinding.** Nodes don't belong to any namespace, so a RoleBinding can't grant them. The ClusterRoleBinding gives `node-reader` to the deployer everywhere.

## Common mistakes

- `kubectl auth can-i create deployments -n apps --as=system:serviceaccount:ci:deployer` tells you whether a request would pass, and `kubectl auth can-i --list` with the same flags shows everything the account can do.
- `roleRef` can't be changed after a binding is created. To point a binding at another role, delete it and create it again.
- `list` and `watch` on Secrets return their contents, not just their names, so they give the same access as `get`.
- Being allowed to create Pods in a namespace means being able to run as any ServiceAccount there, and so to use its permissions. Treat `create pods` as access to every ServiceAccount in the namespace.
- Set `automountServiceAccountToken: false` on Pods that never call the API. A token that isn't mounted can't be stolen.

## The manifests you write

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: deployer
  namespace: ci
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployer
  namespace: apps
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  - apiGroups: [""]
    resources: ["services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  - apiGroups: ["networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deployer
  namespace: apps
subjects:
  - kind: ServiceAccount
    name: deployer
    namespace: ci
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: deployer
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deployer-view
  namespace: monitoring
subjects:
  - kind: ServiceAccount
    name: deployer
    namespace: ci
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: node-reader
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: deployer-node-reader
subjects:
  - kind: ServiceAccount
    name: deployer
    namespace: ci
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: node-reader
```
