ServiceAccount Hardening, ClusterRole Discipline, and the Access-Pattern Telemetry Pipeline
The least-privilege workload discipline says what a container may do at runtime — which syscalls it may call, whether it owns a writable root filesystem, which Linux capabilities it carries. That discipline governs execution. This lesson governs identity — who the container is to the Kubernetes API, what API resources it may touch, and what record exists when it acts.
In a single-tenant cluster running one application, identity is a formality. Multi-agent production systems break that posture the moment a second agent type joins the fleet. The reader agent and the writer agent may run the same container image. They have different jobs. They need different API authority. If they share a ServiceAccount, they share authority — and the audit log cannot tell them apart.
Neil Poulton states the problem plainly in The Kubernetes Book (Ch 13, pp. 179–180): "RBAC is a Kubernetes feature to control who can make what kind of API requests against what resources in which namespaces." The four-part subject-verb-object-scope structure resolves every RBAC question: subject (who), verb (which HTTP method), object (which resource type), scope (which namespace or cluster-wide).
Define permission sets: which verbs on which resource types. Role = namespace-scoped. ClusterRole = cluster-wide or reusable template.
Attach a permission set to subjects (User, Group, ServiceAccount). RoleBinding = namespace-scoped. ClusterRoleBinding = cluster-wide grant.
Runtime identity for Pods. Every Pod runs with one; the kube-apiserver resolves requests to system:serviceaccount:{ns}:{name}.
Records every API request: who, verb, resource, namespace, source IP, response code. The access-pattern telemetry surface for the fleet.
The combination that matters most: a ClusterRole defines the permission set for a role type, bound via namespace-scoped RoleBindings to the ServiceAccount in each namespace where that agent type runs. Policy definition separates from instantiation. Update the ClusterRole; the change propagates everywhere it is bound.
Every Pod, unless configured otherwise, mounts a ServiceAccount token at /var/run/secrets/kubernetes.io/serviceaccount/. From Kubernetes 1.24 onward, this token is a projected volume token — short-lived (default 3600 seconds), audience-bound, automatically rotated. The kube-apiserver extracts the bearer token from the request, validates it via TokenReview, resolves the identity to system:serviceaccount:{namespace}:{name}, and then evaluates RBAC.
Nothing in the container enforces RBAC. The policy lives in the API server. Hardening happens at two levels: restrict what the ServiceAccount is authorized to do, and prevent the token from being mounted where it is not needed.
Start from the role taxonomy, not from the infrastructure. For a multi-agent fleet, roles correspond to trust tiers:
One ServiceAccount per agent type per namespace. No sharing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: hedronite-intake-agent
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["hedronite.io"]
resources: ["intakerecords"]
verbs: ["create", "patch"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: intake-agent-binding
namespace: fleet-intake
subjects:
- kind: ServiceAccount
name: intake-agent
namespace: fleet-intake
roleRef:
kind: ClusterRole
name: hedronite-intake-agent
apiGroup: rbac.authorization.k8s.io
apiVersion: v1
kind: ServiceAccount
metadata:
name: intake-agent
namespace: fleet-intake
automountServiceAccountToken: false
Set automountServiceAccountToken: false at the ServiceAccount level, then set automountServiceAccountToken: true only in the PodSpec for Pods that genuinely need API access. This prevents accidental token exposure in Pods that never call the API.
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: None
resources:
- group: ""
resources: ["endpoints", "events"]
- level: Metadata
resources:
- group: ""
resources: ["configmaps", "serviceaccounts"]
verbs: ["get", "list", "watch"]
- level: Request
resources:
- group: "hedronite.io"
resources: ["intakerecords", "processedrecords"]
verbs: ["create", "patch", "update", "delete"]
- level: RequestResponse
userGroups: ["system:serviceaccounts"]
verbs: ["create", "update", "patch", "delete"]
omitStages: ["RequestReceived"]
kubectl auth can-i list pods \
--namespace=fleet-intake \
--as=system:serviceaccount:fleet-intake:intake-agent
# no — intake agent has no Pod-list authority
kubectl auth can-i list pods \
--namespace=fleet-orchestration \
--as=system:serviceaccount:fleet-orchestration:orchestrator-agent
# yes — orchestrator may list Pods cluster-wide
kubectl auth can-i delete intakerecords \
--namespace=fleet-intake \
--as=system:serviceaccount:fleet-intake:intake-agent
# no — intake agent creates and patches; it does not delete
jq 'select(.user.username == "system:serviceaccount:fleet-intake:intake-agent") |
{time: .requestReceivedTimestamp, verb: .verb,
resource: .objectRef.resource, name: .objectRef.name,
code: .responseStatus.code}' \
/var/log/kubernetes/audit.log
A clean run shows only create and patch on intakerecords with response codes 201 and 200. Any delete, any resource outside the ClusterRole's rules, or any 403 response requires investigation.
The β-Trust arc has built a layered defense. Supply chain security (June 3) validated what runs. Network segmentation (June 10) restricted where traffic flows. Runtime confinement (June 17) limited what the running process may do. Data integrity (June 23) signed what data flows. This lesson closes the access-identity gap: who the running process is to the API, what it may ask, and what record survives the request.
The posture holds only when all five layers are present. RBAC without audit logging is authorization without accountability. Audit logging without RBAC is accountability for a system with no meaningful access boundaries.
The JavaScript and HTML lesson today applies the same role-guard discipline to the browser. Where Kubernetes separates identity (ServiceAccount) from permission (ClusterRole) from binding (RoleBinding), the browser applies a JavaScript role object, a guard function, and a permission-scoped fetch wrapper. The audit trail carries too: structured JSON audit events to a logging endpoint, shaped like the kube-apiserver audit log.
The RBAC system in Kubernetes is not complicated. The objects are few; the verbs form a closed set; the API groups are enumerable. The difficulty is discipline — one ServiceAccount per agent type, no sharing across roles, audit policy deployed before the first production request, audit log routed to SIEM before the first incident.
Write on a card: RBAC controls who may make what kind of requests against what resources in which namespaces. Before approving any RBAC configuration, answer all four parts explicitly. If any part is "anything" or "everywhere," the configuration is not finished.
The audit log converts access control from a policy claim into observable fact. Examine this discipline well. Apply it to the first fleet namespace. Watch what the audit log says in the first week. The log will surface misconfigurations the kubectl auth can-i checks did not catch. That is the work.