Lab: Security and RBAC¶
Duration: 25 minutes
Objectives¶
- Create isolated team namespaces
- Grant limited permissions with RBAC
- Test access with
kubectl auth can-i - Apply ResourceQuota and LimitRange defaults
- Enforce Pod Security Standards
- Add basic NetworkPolicies for namespace isolation
Prerequisites¶
- Kind cluster running
- kubectl configured and working
- Basic understanding of namespaces and ServiceAccounts
Tasks¶
Task 1: Create Team Namespaces¶
Create two namespaces that represent separate tenants.
Requirements:
- Namespace
team-alpha - Namespace
team-beta - Add label
tenant=alphatoteam-alpha - Add label
tenant=betatoteam-beta - Add Pod Security labels to both namespaces
- Enforce the
restrictedprofile - Warn and audit with the
restrictedprofile
Hint
Pod Security labels live on the Namespace:
kubectl label namespace team-alpha \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
Repeat for team-beta.
Solution
kubectl create namespace team-alpha
kubectl create namespace team-beta
kubectl label namespace team-alpha tenant=alpha \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
kubectl label namespace team-beta tenant=beta \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
Task 2: Create Team ServiceAccounts¶
Create separate identities for each team.
Requirements:
- ServiceAccount
team-alpha-userin namespaceteam-alpha - ServiceAccount
team-beta-userin namespaceteam-beta - Disable automatic token mounting on both ServiceAccounts
- Verify the ServiceAccounts exist
Hint
Disable automatic token mounting in the ServiceAccount:
Solution
Create team-alpha-sa.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: team-alpha-user
namespace: team-alpha
automountServiceAccountToken: false
Create team-beta-sa.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: team-beta-user
namespace: team-beta
automountServiceAccountToken: false
Apply:
Task 3: Grant Least-Privilege Access¶
Grant each team permission to manage common workload resources only in its own namespace.
Requirements:
- Role name:
developer - Role exists in both namespaces
- Allow read access to Pods, Services, ConfigMaps, and Secrets
- Allow read/write access to Deployments
- Allow read access to Pod logs
- Bind
team-alpha-userto thedeveloperRole only inteam-alpha - Bind
team-beta-userto thedeveloperRole only inteam-beta
Hint
Deployments are in the apps API group. Pods, Services, ConfigMaps, Secrets, and Pod logs are in the core API group:
Solution
Create team-alpha-rbac.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: team-alpha
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Create team-alpha-binding.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: team-alpha
subjects:
- kind: ServiceAccount
name: team-alpha-user
namespace: team-alpha
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
Create team-beta-rbac.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: team-beta
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Create team-beta-binding.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: team-beta
subjects:
- kind: ServiceAccount
name: team-beta-user
namespace: team-beta
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
Apply:
Task 4: Test RBAC Boundaries¶
Use authorization checks to prove each identity is namespace-scoped.
Requirements:
team-alpha-usercan list Pods inteam-alphateam-alpha-usercannot list Pods inteam-betateam-alpha-usercan create Deployments inteam-alphateam-alpha-usercannot delete namespacesteam-beta-userhas equivalent access only inteam-beta
Hint
Use kubectl auth can-i with --as:
kubectl auth can-i list pods \
--as=system:serviceaccount:team-alpha:team-alpha-user \
-n team-alpha
Check a denied cross-namespace action:
Solution
kubectl auth can-i list pods \
--as=system:serviceaccount:team-alpha:team-alpha-user \
-n team-alpha
kubectl auth can-i list pods \
--as=system:serviceaccount:team-alpha:team-alpha-user \
-n team-beta
kubectl auth can-i create deployments \
--as=system:serviceaccount:team-alpha:team-alpha-user \
-n team-alpha
kubectl auth can-i delete namespaces \
--as=system:serviceaccount:team-alpha:team-alpha-user
kubectl auth can-i list pods \
--as=system:serviceaccount:team-beta:team-beta-user \
-n team-beta
Expected output:
Task 5: Apply Resource Controls¶
Add guardrails to prevent one team from consuming unlimited resources.
Requirements:
- ResourceQuota named
team-quotain both namespaces - Maximum Pods: 10
- Maximum CPU requests:
2 - Maximum memory requests:
4Gi - LimitRange named
default-limitsin both namespaces - Default CPU request:
100m - Default memory request:
128Mi - Default CPU limit:
500m - Default memory limit:
512Mi
Hint
ResourceQuota example:
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
spec:
hard:
pods: "10"
requests.cpu: "2"
requests.memory: 4Gi
LimitRange example:
Solution
Create team-alpha-quota.yaml:
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: team-alpha
spec:
hard:
pods: "10"
requests.cpu: "2"
requests.memory: 4Gi
Create team-alpha-limits.yaml:
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: team-alpha
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
cpu: 500m
memory: 512Mi
Create equivalent files for team-beta, replacing the namespace with team-beta.
Apply:
Task 6: Deploy a Restricted Workload¶
Deploy a workload that passes the restricted Pod Security profile.
Requirements:
- Deployment name:
secure-web - Namespace:
team-alpha - Image:
nginxinc/nginx-unprivileged:1.25-alpine - Replicas: 1
- Run as non-root user
101 - Drop all Linux capabilities
- Disable privilege escalation
- Use RuntimeDefault seccomp
- Use a read-only root filesystem
- Mount an
emptyDirvolume at/tmp - Expose it with a ClusterIP Service named
secure-web
Hint
Use a non-root nginx image and this security context shape:
Container security context:
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
The nginxinc/nginx-unprivileged image listens on port 8080, not 80.
Solution
Create secure-web-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-web
namespace: team-alpha
spec:
replicas: 1
selector:
matchLabels:
app: secure-web
template:
metadata:
labels:
app: secure-web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:1.25-alpine
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
Create secure-web-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: secure-web
namespace: team-alpha
spec:
selector:
app: secure-web
ports:
- port: 80
targetPort: 8080
Apply:
Task 7: Verify Pod Security Blocks Risky Pods¶
Try to create a Pod that violates the restricted profile.
Requirements:
- Pod name:
privileged-test - Namespace:
team-alpha - Image:
busybox:1.36 - Set
privileged: true - Confirm the API server rejects it
- Explain which namespace labels caused the rejection
Hint
Try a privileged Pod:
kubectl run privileged-test -n team-alpha --image=busybox:1.36 \
--overrides='{"spec":{"containers":[{"name":"privileged-test","image":"busybox:1.36","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'
The restricted profile should reject it.
Solution
kubectl run privileged-test -n team-alpha --image=busybox:1.36 \
--overrides='{"spec":{"containers":[{"name":"privileged-test","image":"busybox:1.36","command":["sleep","3600"],"securityContext":{"privileged":true}}]}}'
Expected result:
Error from server (Forbidden): pods "privileged-test" is forbidden: violates PodSecurity "restricted:latest": ...
The rejection is caused by pod-security.kubernetes.io/enforce=restricted on team-alpha. The restricted profile prohibits privileged containers, running as root, and disallowed capabilities.
Task 8: Add Network Isolation¶
Add basic NetworkPolicies for tenant isolation.
Requirements:
- Default deny ingress policy in
team-alpha - Allow ingress to
secure-webonly from Pods inteam-alpha - Include a note that enforcement depends on the cluster CNI supporting NetworkPolicy
- Test connectivity from a Pod in
team-alpha - Test connectivity from a Pod in
team-beta
Note: NetworkPolicy enforcement requires a CNI plugin that supports NetworkPolicy (e.g., Calico, Cilium). kind uses kindnet by default, which does not enforce NetworkPolicies. Use this task to practice writing NetworkPolicy YAML and observe that the policies are accepted by the API server even if they are not enforced by kindnet.
Hint
A default deny ingress policy selects all Pods:
Allow traffic from same-namespace Pods:
Because both team namespaces enforce the restricted Pod Security profile, connectivity test Pods also need a restricted security context:
kubectl run curl-alpha -n team-alpha --image=curlimages/curl:8.5.0 \
--restart=Never --rm -it \
--overrides='{"spec":{"securityContext":{"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}},"containers":[{"name":"curl-alpha","image":"curlimages/curl:8.5.0","args":["-m","5","http://secure-web"],"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}}]}}'
Solution
Create team-alpha-default-deny.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: team-alpha
spec:
podSelector: {}
policyTypes:
- Ingress
Create team-alpha-allow-same-namespace.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace-to-secure-web
namespace: team-alpha
spec:
podSelector:
matchLabels:
app: secure-web
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {}
ports:
- protocol: TCP
port: 8080
Apply:
kubectl apply -f team-alpha-default-deny.yaml
kubectl apply -f team-alpha-allow-same-namespace.yaml
kubectl get networkpolicy -n team-alpha
Test from team-alpha:
kubectl run curl-alpha -n team-alpha --image=curlimages/curl:8.5.0 \
--restart=Never --rm -it \
--overrides='{"spec":{"securityContext":{"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}},"containers":[{"name":"curl-alpha","image":"curlimages/curl:8.5.0","args":["-m","5","http://secure-web"],"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}}]}}'
Test from team-beta:
kubectl run curl-beta -n team-beta --image=curlimages/curl:8.5.0 \
--restart=Never --rm -it \
--overrides='{"spec":{"securityContext":{"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}},"containers":[{"name":"curl-beta","image":"curlimages/curl:8.5.0","args":["-m","5","http://secure-web.team-alpha.svc.cluster.local"],"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}}]}}'
Expected result: Same-namespace traffic succeeds. Cross-namespace traffic is blocked only when the cluster CNI enforces NetworkPolicy (not the case with default kindnet).
Verification¶
Check your work:
kubectl get namespaces --show-labels | grep team-
kubectl get role,rolebinding,resourcequota,limitrange -n team-alpha
kubectl get role,rolebinding,resourcequota,limitrange -n team-beta
kubectl auth can-i list pods --as=system:serviceaccount:team-alpha:team-alpha-user -n team-alpha
kubectl auth can-i list pods --as=system:serviceaccount:team-alpha:team-alpha-user -n team-beta
kubectl get networkpolicy -n team-alpha
Expected outcomes:
- Each ServiceAccount can operate only in its own namespace
- Risky Pods are rejected by Pod Security admission
- ResourceQuota and LimitRange exist in both namespaces
- NetworkPolicies exist for
team-alpha
Cleanup¶
Bonus Challenges¶
- Add a read-only ClusterRole that allows viewing Nodes.
- Create a separate
team-alpha-viewerServiceAccount with a matching RoleBinding. - Add an egress policy that allows DNS and blocks everything else.
- Try deploying a root nginx image and adjust it until it passes the restricted Pod Security profile.
Key takeaways¶
- RBAC uses Roles, ClusterRoles, and Bindings to grant least-privilege access — prefer RoleBindings scoped to a namespace over ClusterRoleBindings
kubectl auth can-iverifies effective permissions without trial and error- Pod Security Standards (baseline/restricted) block privilege escalation at admission time, before Pods are scheduled
- ResourceQuota caps total namespace consumption; LimitRange sets per-container defaults and prevents unbounded Pods
- NetworkPolicies restrict Pod-to-Pod traffic — a default-deny policy forces explicit allow rules for all communication
- Disabling automatic token mounting reduces the attack surface of Pods that do not need API access
- NetworkPolicies are enforced by the CNI plugin, not by kube-proxy — without a CNI that supports NetworkPolicy, rules are applied but silently ignored
Next section¶
Once you've reviewed the content and completed the lab, proceed to the next section.