RBAC / NetworkPolicy / ResourceQuota
A walkthrough of the three policy objects that create isolation for multi-tenant operations where several teams · environments live together in one cluster. RBAC's Role · ClusterRole · ServiceAccount · RoleBinding model, NetworkPolicy's default-deny pattern and CNI dependency, and the pairing of ResourceQuota and LimitRange — all in one chapter, closing Part 2.
This is the last chapter of Part 2. From Chapter 8, StatefulSet / DaemonSet / Job / CronJob through Chapter 13, Autoscaling, we’ve stacked the model for operating workloads one layer at a time. The four kinds of controller, persistent data, the external entry point, resource requests · limits, health checks, and autoscaling — we’ve covered the flow of bringing up a single Pod stably and scaling it up and down to match load. This chapter covers a subject that stacks one more layer on top of that — the security and resource control of a situation where several teams · environments live together in one cluster. There are three keywords. RBAC (who can do what), NetworkPolicy (what traffic gets through), ResourceQuota (how much you can create). All three objects have in common that they’re namespace-level policies, and these three objects fill the gap that Chapter 7, Namespace and labels noted, that “a Namespace itself is not a security boundary.”
By the end of this chapter you’ll have the three dimensions of isolation that let several environments · teams live together safely on one cluster. It also includes a Part 2 retrospective and a guide to the next part (Part 3, Depth).
The common coordinate of the three policies — the namespace level #
When we covered Namespace in Chapter 7, we left one line — a Namespace itself is not a security boundary. It’s only a logical compartment that splits object names, and the real isolation is created by the policy objects stacked on top of it. Those policies are the three subjects of this chapter.
| Dimension | Object | Description |
|---|---|---|
| Permission | Role / ClusterRole / RoleBinding / ClusterRoleBinding | Who can use which verb on which object |
| Traffic | NetworkPolicy | Which Pod can communicate with which Pod |
| Resources | ResourceQuota / LimitRange | How much one namespace can create |
All three objects are applied at the namespace level (NetworkPolicy, ResourceQuota, LimitRange, Role / RoleBinding), or applied bound to a namespace (the ClusterRole + RoleBinding combination). The isolation of multi-tenant operations where dev / staging / prod, or team A / team B, live together on one cluster is completed only when these three dimensions are combined.
This chapter’s flow is simple. Starting from permissions — we cover RBAC in the most detail, then traffic (NetworkPolicy), then resources (ResourceQuota / LimitRange), in that order.
RBAC — who can do what #
RBAC (Role-Based Access Control) is a model that expresses permissions at the K8s API call level. Every action like kubectl get pods, kubectl create deployment, and kubectl delete secret is ultimately an HTTP request to the K8s API server. RBAC checks, for each and every one of those requests, “can this subject use this verb on this resource.”
The model is expressed with four objects. The permission bundles (Role / ClusterRole), and the objects that link those permissions and a subject (RoleBinding / ClusterRoleBinding).
| Object | What it is | Scope |
|---|---|---|
Role | A permission bundle (verbs + resources) | Namespace |
ClusterRole | A permission bundle (verbs + resources) | Cluster (shared across all namespaces) |
RoleBinding | Grants a Role or ClusterRole to a subject | Namespace |
ClusterRoleBinding | Grants a ClusterRole to a subject | Cluster-wide |
There’s one subtle part here. A RoleBinding can reference a ClusterRole too. Because a ClusterRole is a permission bundle and a RoleBinding is the object that decides in which namespace to give that bundle to whom, the pattern of “create one standard ClusterRole (e.g., view, edit) and grant it to different people per namespace with a RoleBinding” is the most common in operations.
This chapter covers RBAC’s manifests and operational patterns. Depth like Aggregated ClusterRole, impersonation, external IAM mapping such as EKS’s IRSA · GKE’s Workload Identity, and the token lifecycle is covered in Chapter 16, RBAC / ServiceAccount in depth.
Subject — the side that receives permission #
The subjects to which permission is granted are three.
- User — a human user. K8s itself has no user database, and external authentication (OIDC, x509 client certificates, cloud IAM mapping, etc.) tells it the user name.
- Group — a bundle of users. As with User, external authentication delivers the group information to K8s.
- ServiceAccount — the ID a Pod uses when accessing the K8s API. It’s the identity of a workload, not a person.
Among these three, the ServiceAccount is nearly the only one you create and manage directly with a K8s manifest. User and Group are products of external authentication, so they don’t exist as objects inside K8s. They’re only written in the subjects field of a RoleBinding.
ServiceAccount — a Pod’s ID #
Every Pod lives in the cluster holding the ID of some ServiceAccount. If you don’t write spec.serviceAccountName in the manifest, that namespace’s default ServiceAccount is automatically bound. When you access the K8s API with a tool like kubectl from inside a Pod, that call is performed with that ServiceAccount’s permissions.
kubectl get serviceaccounts -n defaultNAME SECRETS AGE
default 0 3dThe default default ServiceAccount has no permissions granted. In a production cluster with RBAC applied, it’s normal for the following message to come out when you try kubectl get pods from inside a Pod.
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default"Seeing this message and binding permissions with a RoleBinding makes the same command go through. Let’s follow that flow with the next example.
One bundle of an RBAC manifest #
Let’s organize the simplest scenario into a single manifest. Create a ServiceAccount called pod-reader in the dev namespace, grant that SA “the permission to read Pods,” and then check whether kubectl get pods goes through inside a Pod using that SA.
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-reader
namespace: dev
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: dev
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-reader
namespace: dev
subjects:
- kind: ServiceAccount
name: pod-reader
namespace: dev
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioLet’s note the responsibility of the three objects one line each.
- ServiceAccount
pod-reader— a new ID in thedevnamespace. It has no permissions yet. - Role
pod-reader— a permission bundle that allows theget,list,watchverbs on thepodsresource.apiGroups: [""]points to the core API group (Pod, Service, ConfigMap, etc.). - RoleBinding
pod-reader— the object that links the Role and the ServiceAccount.subjectsis the receiving side,roleRefis the giving side.
After applying these three objects at once, let’s bring up a Pod that uses that ServiceAccount.
apiVersion: v1
kind: Pod
metadata:
name: reader
namespace: dev
spec:
serviceAccountName: pod-reader
containers:
- name: kubectl
image: bitnami/kubectl:1.32
command: ["sleep", "3600"]kubectl apply -f rbac-pod-reader.yaml
kubectl apply -f reader-pod.yaml
kubectl exec -n dev reader -- kubectl get pods -n devNAME READY STATUS RESTARTS AGE
reader 1/1 Running 0 30sYou can also confirm that an action without permission is blocked from inside the same Pod.
kubectl exec -n dev reader -- kubectl create deployment nginx --image=nginx -n deverror: failed to create deployment: deployments.apps is forbidden: User "system:serviceaccount:dev:pod-reader" cannot create resource "deployments" in API group "apps" in the namespace "dev"Because the pod-reader Role gave only get / list / watch on Pods and didn’t give create on Deployments, it’s rejected at exactly that part.
The core of verbs and resources #
If we organize the verbs frequently used in a Role / ClusterRole’s rules into a table, it’s as follows.
| verb | Meaning |
|---|---|
get | Retrieve a single object |
list | Retrieve a list of objects |
watch | Subscribe to change events |
create | Create an object |
update | Update an object (whole) |
patch | Partially update an object |
delete | Delete an object |
deletecollection | Bulk-delete matching objects |
To allow read-only, you give the three get, list, watch together. It’s because a command like kubectl get internally uses list, and watch is used for refreshing the informer cache. To allow writes too, you add create, update, patch, delete.
resources is written with plural names like pods, services, configmaps. apiGroups is the API group that resource belongs to. The core group (Pod / Service / ConfigMap / Secret, etc.) is [""] (empty string), Deployment / StatefulSet / DaemonSet is ["apps"], Job / CronJob is ["batch"], and Ingress is ["networking.k8s.io"]. You can check at a glance which group a resource belongs to with kubectl api-resources.
kubectl api-resourceskubectl auth can-i — verifying permissions #
Once you’ve touched RBAC, the next question quickly follows — “so, can this user actually do this action?” There are many times when staring at the manifest for a while doesn’t make the result clear at a glance. The command by which K8s answers this question directly is kubectl auth can-i.
kubectl auth can-i create pods -n dev
kubectl auth can-i delete deployments -n prodOne of yes / no is output. If you want to ask from the standpoint of another user or ServiceAccount, you use impersonation with the --as option.
kubectl auth can-i create pods --as=alice -n dev
kubectl auth can-i list secrets --as=system:serviceaccount:dev:pod-reader -n devThe --as option itself also requires permission (the impersonation permission). It’s the pattern most frequently used when a production cluster’s admin checks whether a newly created RBAC policy works as intended. If you want to see all permissions at once, add --list.
kubectl auth can-i --list --as=alice -n devThe finished diagnostic flow and the general permission-denied troubleshooting tree are organized in Chapter 27, kubectl debugging patterns.
A common trap — a too-broad ClusterRole #
The mistake most frequently made when first applying RBAC is the pattern of giving in to the hassle and binding the ClusterRole cluster-admin to a ServiceAccount.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: app-admin
subjects:
- kind: ServiceAccount
name: app
namespace: dev
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.iocluster-admin is unlimited permission over the entire K8s API. If the token of the ServiceAccount with this ClusterRoleBinding leaks out from even a single Pod, with that token you can read every Secret of every namespace in the cluster and delete every object. Even one vulnerability in a library inside the container image spreads from that one point into an accident for the whole cluster.
The operational standard principle is least privilege. You precisely pick only the verbs and resources a workload truly needs, bundle them into a Role, and grant that Role with a RoleBinding. There’s a set of standard ClusterRoles K8s creates in advance, and the common starting point is the pattern of using these limited to a namespace with a RoleBinding.
| ClusterRole | Description |
|---|---|
view | Read the namespace’s objects. Excludes Secrets |
edit | view + write on most objects. Excludes RBAC objects like Role / RoleBinding |
admin | edit + management of the RBAC objects within that namespace |
cluster-admin | Unlimited over the entire cluster |
Limiting view / edit / admin to a specific namespace with a RoleBinding lets you quickly grant an appropriate permission bundle to a person · team · service, and the standard is the shape of granting cluster-admin only to a handful of cluster operators with a ClusterRoleBinding. A policy that blocks the manifest itself at the admission step (e.g., “ban the cluster-admin binding”) is the area covered by Chapter 17, Admission Controller’s OPA Gatekeeper · Kyverno.
automountServiceAccountToken #
A ServiceAccount’s token is by default automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/token inside the Pod. For a Pod that has no occasion to call the K8s API, it’s safer not to mount even that token. You can turn it off with automountServiceAccountToken: false in the manifest.
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
automountServiceAccountToken: false
containers:
- name: nginx
image: nginx:1.27Putting in this one line means that even if that Pod is compromised, it has no token to access the K8s API directly. It’s a frequent recommendation in security guides. The practical pattern for operating with “zero passwords” by combining the token with an external secret store · IRSA is covered in Chapter 29, Secret operations.
NetworkPolicy — controlling Pod-to-Pod traffic #
If RBAC handles permissions over the K8s API, NetworkPolicy handles the IP traffic between Pods. When two Pods in the same cluster know each other’s IPs and try to communicate, it’s the policy that checks whether that traffic is allowed.
The default is everything passes #
The default of the K8s network model is simple — without a NetworkPolicy, all Pods can communicate with one another. Whether the same namespace or a different namespace, if you just know a Pod’s IP you can freely send packets. In a multi-tenant cluster, if this default is left unchanged, the shape becomes one where a Pod of one namespace freely accesses a DB Pod of another namespace.
NetworkPolicy’s behavior rules are organized into the following two lines.
- For a Pod matched by even one NetworkPolicy, the traffic has default-deny applied for the directions written in that policy’s
policyTypes. - And only the traffic explicitly allowed in that policy’s
ingress/egressrules passes.
A Pod that no policy matches doesn’t trigger the rules above, so all traffic passes. If even one policy matches it, for that policy’s directions it switches to a whitelist model.
The CNI must support NetworkPolicy #
NetworkPolicy is a standard at the K8s manifest level, but the work of actually blocking traffic is done by the CNI (Container Network Interface) plugin. So if the CNI doesn’t support NetworkPolicy, nothing happens even if you apply the manifest. The traffic passes unchanged.
| CNI | NetworkPolicy support |
|---|---|
| Calico | Supported |
| Cilium | Supported (eBPF-based) |
| Antrea | Supported |
| flannel | Not supported |
| EKS’s amazon-vpc-cni | Needs a separate option enabled (you have to install Calico alongside or turn on vpc-cni’s NetworkPolicy option) |
If you plan to use NetworkPolicy in a production cluster, you must pick the CNI from the time you create the cluster. Applying a NetworkPolicy manifest on a default EKS cluster and going “why isn’t the traffic blocked?” is a common trap — if the CNI doesn’t support it, the manifest is merely an object sitting in etcd. The in-earnest model of the CNI data plane (iptables-based vs eBPF-based, Calico vs Cilium) is covered in Chapter 15, CNI in depth.
The default-deny → allow pattern #
The operational standard pattern is to lay one default-deny policy in the namespace and explicitly allow only the necessary communication.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: prod
spec:
podSelector: {}
policyTypes:
- Ingress
- EgresspodSelector: {} is an empty selector that points to “all Pods in this namespace.” Since both Ingress and Egress are written in policyTypes and there’s not a single ingress / egress rule, all Pods in this namespace have both incoming and outgoing traffic blocked.
If you leave it in this state, Pods can’t even do DNS lookups, so you have to at least open up DNS traffic.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: prod
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53After that, you add the necessary communication per workload one at a time — like 80/TCP for frontend going to backend, and 5432/TCP for backend going to the DB.
A NetworkPolicy manifest — frontend → backend #
If we write the most common single shot into one manifest, it’s as follows. It’s an ingress policy that has backend Pods receive only 8080/TCP traffic coming in from frontend Pods.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: prod
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080How to read it is as follows.
spec.podSelector— the Pods this policy applies to. It applies only to Pods with theapp=backendlabel.policyTypes: [Ingress]— specifies that it’s a policy for the incoming direction. egress isn’t controlled by this policy.ingress[0].from— where to allow traffic from. It comes from Pods with theapp=frontendlabel.ingress[0].ports— for which port. It allows only 8080 / TCP.
The selector of from can be chosen among three — these three can be used separately or together.
| Selector | Meaning |
|---|---|
podSelector | Matches Pod labels in the same namespace |
namespaceSelector | All Pods of another namespace (or that namespace + a podSelector combination) |
ipBlock | An IP range as a CIDR expression (an external IP or a node IP) |
Writing namespaceSelector and podSelector together in the same from item becomes “Pods of a specific label in a specific namespace.” Writing the two separately ends up meaning “all Pods of that namespace or the Pods of that label in the same namespace,” so you have to look at exactly what shape you intend and write it.
ingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
podSelector:
matchLabels:
app: frontendingress:
- from:
- namespaceSelector:
matchLabels:
env: prod
- podSelector:
matchLabels:
app: frontendThat the meanings of these two manifests differ is a frequent trap of NetworkPolicy. The manifest above is “only the frontend Pods of the env=prod namespace,” and the manifest below is “all Pods of the env=prod namespace, or the frontend Pods of the same namespace.”
egress rules — the outgoing direction #
The mirror of ingress is egress. A policy that allows only the communication where backend Pods go out to the DB on port 5432 takes the following shape.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-egress-to-db
namespace: prod
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432Once this single policy is applied, backend Pods can’t go out anywhere other than the 5432 / TCP of the postgres Pods. Combined with the default-deny + allow-dns combination made above, the outgoing communication of backend Pods is limited to two: “DNS + DB.” Narrowing the communication going to the external internet precisely with a whitelist per workload is the standard shape of operational security.
The limits of NetworkPolicy #
NetworkPolicy is an L3 / L4 policy. It looks only at IP, port, and protocol. An L7 policy like HTTP method or path is outside NetworkPolicy’s range — Cilium’s L7 policy or a service mesh like Istio / Linkerd handles that dimension (the area of a later K8s deep-dive book). And NetworkPolicy is a policy for traffic inside the cluster, while external inbound is controlled at the Ingress · LoadBalancer dimension of Chapter 10, Ingress, and external outbound is controlled separately at the NAT gateway’s security group. A cluster’s security shape is built together by policies at several layers.
ResourceQuota — the namespace resource aggregate cap #
In Chapter 11, resources.requests / limits we covered the container-level resource model — requests and limits. It was the object that sets the guarantee and upper bound of a single container. What stacks one more layer on top of that is ResourceQuota, the namespace-level aggregate cap.
The operational scenario is clear. When dev / staging / prod, or several teams, live together in one cluster, you have to prevent the accident where the dev namespace eats up all the nodes’ CPU and the prod workloads run short of resources. ResourceQuota fills that gap.
The ResourceQuota manifest #
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "50"
services: "20"
configmaps: "30"
secrets: "30"
persistentvolumeclaims: "10"
requests.storage: 100GiOnce this ResourceQuota is applied to the dev namespace, the following sums can’t exceed the bounds.
- The sum of
requests.cpuof all Pods in that namespace ≤ 4 cores - The sum of
requests.memory≤ 8Gi - The sum of
limits.cpu≤ 8 cores - The sum of
limits.memory≤ 16Gi - Object counts — 50 Pods, 20 Services, 30 each of ConfigMaps · Secrets, 10 PVCs
- The sum of PVCs’
requests.storage≤ 100Gi
Creating an object that exceeds a bound is rejected at the K8s API server. If, with 4 cores already allocated in the dev namespace, you try to create an additional Pod with requests.cpu: 1, that Pod-creation request is rejected at the admission step and the following message comes out.
Error from server (Forbidden): error when creating "...": pods "..." is forbidden: exceeded quota: dev-quota, requested: requests.cpu=1, used: requests.cpu=4, limited: requests.cpu=4Checking how ResourceQuota behaves #
kubectl get resourcequota -n dev
kubectl describe resourcequota dev-quota -n devName: dev-quota
Namespace: dev
Resource Used Hard
-------- ---- ----
configmaps 12 30
limits.cpu 6 8
limits.memory 12Gi 16Gi
pods 18 50
persistentvolumeclaims 3 10
requests.cpu 3 4
requests.memory 6Gi 8Gi
requests.storage 30Gi 100Gi
secrets 15 30
services 8 20It’s the shape where a Used / Hard table comes out on one page. In a production cluster, describe is the fastest tool when checking how full one namespace is.
Paired with LimitRange — container-level defaults · upper bounds #
One subtle trap of ResourceQuota is — when you create a Pod in a namespace where ResourceQuota is active, if you don’t write requests / limits on a container, it’s rejected. It’s because for ResourceQuota to compute the sum it needs the resource values of all containers, and a container not written in the manifest can’t have its sum computed.
The operational safety net for this part is LimitRange. LimitRange is the object that sets defaults and a max · min at the container level. It’s an object we noted once in Chapter 11, and in this chapter we organize it once more paired with ResourceQuota.
apiVersion: v1
kind: LimitRange
metadata:
name: dev-limits
namespace: dev
spec:
limits:
- type: Container
default:
cpu: 200m
memory: 256Mi
defaultRequest:
cpu: 100m
memory: 128Mi
max:
cpu: "2"
memory: 2Gi
min:
cpu: 50m
memory: 64MiWhen a Pod manifest comes into a namespace where this LimitRange is applied, the following happens.
- If a container has no
requests,defaultRequest(100m / 128Mi) is filled in automatically. - If a container has no
limits,default(200m / 256Mi) is filled in automatically. - If a container’s
requestsorlimitsexceedsmax(2 / 2Gi), it’s rejected. - If it’s below
min(50m / 64Mi), it’s rejected.
If we split the responsibilities of ResourceQuota and LimitRange, they become easy to summarize in one line each.
| Object | Unit | What it sets |
|---|---|---|
LimitRange | Container | Defaults · max · min |
ResourceQuota | Namespace | Aggregate cap, object-count cap |
The operational shape is to keep the two together. LimitRange fills in the gaps of a broken manifest, and ResourceQuota prevents the sum of those filled-in values from exceeding the bound. Only when the two are present together does the resource policy of multi-tenant operations roll stably.
scopes / scopeSelector — to only some Pods #
ResourceQuota by default applies to all Pods of the namespace, but you can narrow the scope. A frequently used pattern is separation by PriorityClass — for example, limiting only the resource sum of high-priority workloads separately, or limiting only BestEffort QoS Pods separately.
apiVersion: v1
kind: ResourceQuota
metadata:
name: high-priority-quota
namespace: dev
spec:
hard:
requests.cpu: "2"
requests.memory: 4Gi
scopeSelector:
matchExpressions:
- operator: In
scopeName: PriorityClass
values: ["high"]In the basic operational shape, it’s fine to start with one policy that applies to everything without a scope, and add a policy with a scope when needed.
The collaboration of the three policies — the isolation of a multi-tenant cluster #
If we organize the shape of isolation the three objects create into one picture, it’s as follows.
[ RBAC ] who can create an object
│ (verbs × resources × namespace)
│
[ NetworkPolicy ] whom the created Pod communicates with
│ (podSelector × from/to × ports)
│
[ ResourceQuota ] how much that namespace can create
│ (cpu/memory sum + object count)
│
[ LimitRange ] container-level defaults · upper bounds
(default / max / min)The three objects seem to roll separately, but in reality they make a namespace’s isolation together. You apply RBAC to the dev namespace so only the dev team can touch the objects inside it, you block with NetworkPolicy the traffic of dev’s Pods going to prod’s DB, and you limit with ResourceQuota the CPU · memory · object count dev can eat up. When all three are applied, the isolation holds where an accident occurring in dev doesn’t leak to staging and prod.
For this isolation to work cleanly, one more premise is needed — splitting the namespaces themselves well. The policy of how to split namespaces by environment (dev / staging / prod), by team (team-a / team-b), or per service is a decision you have to settle when first setting up the cluster. If you leave this decision blurry and try to apply RBAC / NetworkPolicy / ResourceQuota, where to align the policy wavers each time.
Part 2 retrospective — what came into your hands over seven chapters #
Since this is the last chapter of Part 2, let’s organize once. If Part 1 took you to the stage of reading and writing a single manifest, Part 2 added an operational layer one step at a time on top of that.
- Chapter 8 — StatefulSet / DaemonSet / Job / CronJob. With four kinds of controller that aren’t Deployment, it covers the four patterns of workloads that need identity and a disk, workloads that must run one per node, one-off work, and periodic execution.
- Chapter 9 — PV / PVC / StorageClass. As the persistent-data model, it organizes static · dynamic provisioning, accessModes, reclaimPolicy, volumeBindingMode, allowVolumeExpansion, and explains what a StatefulSet’s volumeClaimTemplates creates.
- Chapter 10 — Ingress and the Ingress Controller. It covers the object that gathers the external entry point into one place and the controller that resolves its manifest into actual traffic routing, organizing HTTP / HTTPS, TLS termination, virtual hosts, and path-based routing.
- Chapter 11 — resources.requests / limits. It explains container-level resource requests and upper bounds, and covers that requests is the basis of scheduling and limits is the basis of OOM · CPU throttling, and the flow where the three QoS tiers decide eviction priority.
- Chapter 12 — Health checks. With the three liveness / readiness / startup probes, it looks at how K8s judges a container’s aliveness · service readiness · initialization phase.
- Chapter 13 — HPA / VPA / Cluster Autoscaler. It explains the three dimensions where Pod count, Pod resources, and node count change automatically to match load, and notes the metrics-server · custom metrics · HPA + VPA conflict trap.
- Chapter 14 (this chapter) — RBAC / NetworkPolicy / ResourceQuota. Through security and resource policy, it covers the three dimensions of multi-tenant cluster isolation: who, what traffic, and how much is allowed.
At the point you’ve followed all seven of these chapters, you’re at the stage where, whatever kind of object you meet in your company cluster’s manifest directory, you can read its intent and operational traps in a single line. Seeing kind: StatefulSet you naturally recall volumeClaimTemplates and the headless Service, seeing kind: Ingress you ask what the Ingress Controller behind it is, and seeing an empty limits under resources you simultaneously suspect the OOM risk and the absence of a LimitRange. It’s the stage where the model of how a single manifest rolls inside the cluster has settled in your head.
Exercises #
- After applying the body’s
rbac-pod-reader.yamlandreader-pod.yamlin order, record the results of the two commandskubectl exec -n dev reader -- kubectl get pods -n devandkubectl exec -n dev reader -- kubectl create deployment nginx --image=nginx -n dev. Next, write in one line of manifest how you should add to the Role’srulesto also grant thecreatepermission ondeploymentsto the same SA, and verify withkubectl auth can-i create deployments --as=system:serviceaccount:dev:pod-reader -n dev. - After applying the two policies
default-deny-allandallow-dnsfrom §“The default-deny → allow pattern” to theprodnamespace, add one policy that allows only frontend → backend 8080 / TCP. Next, record in time order whether acurlfrom inside a backend Pod to an arbitrary Service of another namespace is blocked, and whether it goes through from the allowed frontend. Note in one paragraph how this connects to the model of §“The CNI must support NetworkPolicy,” that on a cluster whose CNI doesn’t support NetworkPolicy (e.g., default flannel) the same policy can’t block the traffic. - Apply the two objects
dev-quotaanddev-limitstogether to thedevnamespace. Record, respectively, the values LimitRange fills in automatically when you apply a Deployment manifest that left outrequests/limits, the message rejecting a manifest that exceedsmax, and the message rejecting new Pod creation when ResourceQuota’s sum has reached the bound. Organize in one paragraph at which step (admission, validation) the three messages come out, matching it against the picture of §“The collaboration of the three policies.”
In one line: A multi-tenant cluster is properly isolated only when the three dimensions of RBAC (K8s API permissions), NetworkPolicy (IP traffic between Pods), and ResourceQuota + LimitRange (namespace resource sum + container-level defaults) work together. The operational standard principles are RBAC’s least privilege, NetworkPolicy’s default-deny + allow, and operating ResourceQuota and LimitRange as a pair. A Namespace itself is not a security boundary; real isolation appears only when these three policies are layered on top.
Next chapter #
Part 2 is over. From Chapter 15, CNI in depth, the first chapter of Part 3, Depth, the viewpoint moves one more tier — we go into the depth of the data plane · policy engines · API extension that hold up a manifest’s behavior. The answer to how this chapter’s NetworkPolicy actually gets blocked is Chapter 15’s Calico / Cilium / eBPF.
The whole storyline of Part 3 is as follows.
| Chapter | Subject |
|---|---|
| Chapter 15 | CNI in depth — Calico · Cilium · eBPF |
| Chapter 16 | RBAC · ServiceAccount in depth — Aggregated ClusterRole · Impersonation · IRSA · Workload Identity |
| Chapter 17 | Admission Controller — OPA Gatekeeper · Kyverno |
| Chapter 18 | CRD and the Operator pattern — controller-runtime |
| Chapter 19 | Observability — Prometheus · Grafana · Loki · OpenTelemetry |
| Chapter 20 | GitOps — ArgoCD · Flux |
Finishing Part 3 brings you to the stage of being able to see K8s with the view of someone who sets up a cluster. After that, Part 4, EKS in Production follows the flow of putting up and operating a real service on AWS EKS from scratch.