Skip to main content
Kubernetes · lesson 7 of 10
Kubernetes advanced Lesson 7 of 10

Also part of: Cloud Security

Security, RBAC, and Observability

Learn Kubernetes RBAC for access control and how to monitor cluster and workload health with logs, metrics, and events.

By SkillByExample · Updated

Kubernetes Security Model

Kubernetes is designed to run multi-tenant workloads, so security is layered:

Request → Authentication → Authorization (RBAC) → Admission Control → etcd
LayerWhat it does
AuthenticationVerifies identity (certificates, tokens, OIDC)
Authorization (RBAC)Decides what the identity can do
Admission controlValidates/mutates resources before persistence
Network policiesControls pod-to-pod traffic
Pod securityRestricts what containers can do (runAsNonRoot, etc.)

RBAC — Roles and Bindings

Role (namespace-scoped)

# role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: default
rules:
  - apiGroups: [""]           # "" = core API group
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]

ClusterRole (cluster-wide)

# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: deployment-manager
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]

RoleBinding — Grant a Role to a User/ServiceAccount

# rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods-binding
  namespace: default
subjects:
  - kind: User
    name: alice
    apiGroup: rbac.authorization.k8s.io
  - kind: ServiceAccount
    name: ci-bot
    namespace: default
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f role.yaml -f rolebinding.yaml

# Check what a user/SA can do
kubectl auth can-i list pods --as alice
kubectl auth can-i delete deployments --as alice
kubectl auth can-i delete pods --as system:serviceaccount:default:ci-bot

# List all role bindings in a namespace
kubectl get rolebindings -n default
kubectl describe rolebinding read-pods-binding

ServiceAccounts

Every pod runs as a ServiceAccount. By default it’s the default SA with minimal permissions.

# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-bot
  namespace: default
# Use the ServiceAccount in a Pod
spec:
  serviceAccountName: ci-bot
  containers:
    - name: app
      image: my-app:1.0
kubectl create serviceaccount ci-bot
kubectl get serviceaccount

Pod Security — Non-Root Containers

spec:
  securityContext:
    runAsNonRoot: true          # pod-level: all containers run as non-root
    runAsUser: 1000
    fsGroup: 2000

  containers:
    - name: app
      image: my-app:1.0
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true    # container can't write to /
        capabilities:
          drop:
            - ALL                        # drop all Linux capabilities

Network Policies

Control which pods can talk to which other pods.

# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress
  namespace: production
spec:
  podSelector: {}          # applies to all pods in namespace
  policyTypes:
    - Ingress
  ingress: []              # no ingress allowed by default
# Allow only frontend → backend traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - port: 8080

Observability

Logs

# Stream logs from a pod
kubectl logs -f my-pod

# Logs from a specific container in a multi-container pod
kubectl logs my-pod -c sidecar

# Logs from a previous container run (after crash)
kubectl logs my-pod --previous

# Aggregate logs across all pods matching a label
kubectl logs -l app=api --all-containers=true --tail=100

Events

# Cluster-wide events (sorted by time — most useful for debugging)
kubectl get events --sort-by='.lastTimestamp'

# Events in a specific namespace
kubectl get events -n production

# Events for a specific resource
kubectl describe pod my-pod | grep -A 20 Events
kubectl describe node worker-1 | grep -A 20 Events

Metrics

# Enable metrics server on minikube
minikube addons enable metrics-server

# Pod CPU and memory usage
kubectl top pods
kubectl top pods -n production --sort-by=cpu

# Node resource usage
kubectl top nodes

Health Checks — Debugging Failed Pods

# Pod is in CrashLoopBackOff
kubectl describe pod <name>     # look at: State, Last State, Events
kubectl logs <name> --previous  # logs from last crash

# Pod is Pending
kubectl describe pod <name>     # look for: Insufficient cpu/memory, No nodes available

# Pod is not Ready
kubectl describe pod <name>     # look at: Readiness probe failures in Events

# Generic debugging — run a temporary debug pod
kubectl run debug --rm -it --image=busybox -- sh

# Copy files out of a pod
kubectl cp my-pod:/var/log/app.log ./app.log

Monitoring Stack (Production)

The standard Kubernetes observability stack:

ToolPurpose
PrometheusScrapes and stores metrics
GrafanaDashboards and visualization
LokiLog aggregation (Grafana Labs)
OpenTelemetryDistributed tracing
AlertmanagerAlert routing and silencing
# Install kube-prometheus-stack via Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace

# Access Grafana locally
kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80
# Open http://localhost:3000 (admin / prom-operator)

Learning Outcomes

You can:

  • Write Role, ClusterRole, RoleBinding manifests for least-privilege access
  • Verify permissions with kubectl auth can-i
  • Secure pods with non-root contexts and capability drops
  • Apply NetworkPolicy to restrict inter-pod traffic
  • Diagnose issues using logs, events, and kubectl describe
  • Understand the standard monitoring stack (Prometheus + Grafana)

Practice

The outputs come from a single-node minikube profile named practice-k8s. With the default profile, your node and its system Pods are named minikube instead.

  1. Apply role.yaml and rolebinding.yaml, then use kubectl auth can-i to answer: can alice list pods, read pod logs, delete pods, list pods in kube-system, or list secrets?
Solution
kubectl apply -f role.yaml -f rolebinding.yaml
kubectl auth can-i list pods --as alice
kubectl auth can-i get pods --subresource=log --as alice
kubectl auth can-i delete pods --as alice
kubectl auth can-i list pods --as alice -n kube-system
kubectl auth can-i list secrets --as alice
$ kubectl apply -f role.yaml -f rolebinding.yaml
role.rbac.authorization.k8s.io/pod-reader created
rolebinding.rbac.authorization.k8s.io/read-pods-binding created
$ kubectl auth can-i list pods --as alice
yes
$ kubectl auth can-i get pods --subresource=log --as alice
yes
$ kubectl auth can-i delete pods --as alice
no
$ kubectl auth can-i list pods --as alice -n kube-system
no
$ kubectl auth can-i list secrets --as alice
no

alice gets exactly what pod-reader lists: read verbs on pods and pods/log, only in default, where the RoleBinding lives. Everything else is denied, because RBAC only grants. Note the separate -f before each file: kubectl apply -f role.yaml rolebinding.yaml fails with error: Unexpected args: [rolebinding.yaml].

  1. Create the ci-bot ServiceAccount and give it the deployment-manager ClusterRole in the default namespace only. Check whether it can delete deployments in default and in kube-system, and whether it can delete pods.
Solution
# ci-bot-binding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-bot-deployments
  namespace: default
subjects:
  - kind: ServiceAccount
    name: ci-bot
    namespace: default
roleRef:
  kind: ClusterRole            # a ClusterRole, granted only inside "default"
  name: deployment-manager
  apiGroup: rbac.authorization.k8s.io
$ kubectl create serviceaccount ci-bot
serviceaccount/ci-bot created
$ kubectl apply -f clusterrole.yaml -f ci-bot-binding.yaml
clusterrole.rbac.authorization.k8s.io/deployment-manager created
rolebinding.rbac.authorization.k8s.io/ci-bot-deployments created
$ kubectl auth can-i delete deployments --as system:serviceaccount:default:ci-bot
yes
$ kubectl auth can-i delete deployments --as system:serviceaccount:default:ci-bot -n kube-system
no
$ kubectl auth can-i delete pods --as system:serviceaccount:default:ci-bot
no
$ kubectl auth can-i list pods --as system:serviceaccount:default:ci-bot
yes

A RoleBinding that points at a ClusterRole grants the ClusterRole’s rules in the binding’s namespace only, so kube-system is a no. That lets you define a role once and hand it out namespace by namespace. A ClusterRoleBinding would grant it everywhere. Pods are read-only in this ClusterRole, so delete pods is denied. In default, ci-bot’s list pods is granted twice: by this binding and by the lesson’s rolebinding.yaml from task 1.

  1. Run the “Pod Security” settings on a real Pod. Use busybox:1.36 running sleep 3600, then check which user the container runs as and whether it can write to /.
Solution
# secure-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: locked-down
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 2000
  containers:
    - name: app
      image: busybox:1.36
      command: ["sleep", "3600"]
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
$ kubectl apply -f secure-pod.yaml
pod/locked-down created
$ kubectl wait --for=condition=Ready pod/locked-down
pod/locked-down condition met
$ kubectl exec locked-down -- id
uid=1000 gid=0(root) groups=0(root),2000
$ kubectl exec locked-down -- touch /hello
touch: /hello: Read-only file system
command terminated with exit code 1

The UID is 1000 and fsGroup 2000 was added as a supplementary group, but the primary group is still 0(root), because nothing set runAsGroup. Add runAsGroup: 1000 beside runAsUser to close that gap. The touch fails before permissions are even checked: the whole root filesystem is mounted read-only.

  1. Write a Pod that crashes on its first start and runs normally after that, using an emptyDir as a marker. Then use kubectl logs and kubectl logs --previous to see both starts.
Solution
# crasher.yaml
apiVersion: v1
kind: Pod
metadata:
  name: crasher
spec:
  volumes:
    - name: state
      emptyDir: {}            # survives container restarts, not Pod deletion
  containers:
    - name: app
      image: busybox:1.36
      volumeMounts:
        - name: state
          mountPath: /state
      command:
        - sh
        - -c
        - |
          if [ -f /state/crashed ]; then
            echo "second start: running"; sleep 3600
          fi
          touch /state/crashed
          echo "first start"; echo "fatal: DATABASE_URL is not set" >&2; exit 1
$ kubectl apply -f crasher.yaml
pod/crasher created
$ kubectl wait --for=condition=Ready pod/crasher --timeout=120s
pod/crasher condition met
$ kubectl get pod crasher
NAME      READY   STATUS    RESTARTS     AGE
crasher   1/1     Running   1 (1s ago)   2s
$ kubectl logs crasher
second start: running
$ kubectl logs crasher --previous
first start
fatal: DATABASE_URL is not set
$ kubectl describe pod crasher | grep -A 3 'Last State'
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
      Started:      Sun, 11 Oct 2026 10:53:08 +0530

Plain logs shows only the current container, which is healthy and has no trace of the crash. The fatal message is only in --previous. That is why a Pod that is Running now, but has a non-zero RESTARTS count, still deserves a look.

  1. Enable metrics-server and use kubectl top to find the hungriest Pod in kube-system and the node’s overall usage.
Solution
minikube -p practice-k8s addons enable metrics-server   # drop -p with the default profile
kubectl top pods -n kube-system --sort-by=memory
kubectl top nodes
$ minikube -p practice-k8s addons enable metrics-server
* metrics-server is an addon maintained by Kubernetes. For any concerns contact minikube on GitHub.
You can view the list of minikube maintainers at: https://github.com/kubernetes/minikube/blob/master/OWNERS
  - Using image registry.k8s.io/metrics-server/metrics-server:v0.8.1
* The 'metrics-server' addon is enabled
$ kubectl top pods -n kube-system --sort-by=memory
NAME                                   CPU(cores)   MEMORY(bytes)   
kube-apiserver-practice-k8s            64m          233Mi           
kube-controller-manager-practice-k8s   26m          70Mi            
etcd-practice-k8s                      38m          51Mi            
kube-scheduler-practice-k8s            10m          28Mi            
storage-provisioner                    3m           20Mi            
kube-proxy-9dr62                       1m           20Mi            
metrics-server-9d74bb658-9rtvr         4m           20Mi            
coredns-7d764666f9-lwqfk               3m           17Mi            
$ kubectl top nodes
NAME           CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)   
practice-k8s   272m         2%       1195Mi          15%

The API server is the biggest Pod by a wide margin. CPU is in millicores (64m is 0.064 of a core), and the node figure includes everything on the machine, not only Pods. metrics-server samples on a cycle, so a Pod that only just started is missing from top at first.

Clean up with:

kubectl delete -f crasher.yaml -f secure-pod.yaml
kubectl delete -f ci-bot-binding.yaml -f clusterrole.yaml -f rolebinding.yaml -f role.yaml
kubectl delete serviceaccount ci-bot
$ kubectl delete -f crasher.yaml -f secure-pod.yaml
pod "crasher" deleted from default namespace
pod "locked-down" deleted from default namespace
$ kubectl delete -f ci-bot-binding.yaml -f clusterrole.yaml -f rolebinding.yaml -f role.yaml
rolebinding.rbac.authorization.k8s.io "ci-bot-deployments" deleted from default namespace
clusterrole.rbac.authorization.k8s.io "deployment-manager" deleted
rolebinding.rbac.authorization.k8s.io "read-pods-binding" deleted from default namespace
role.rbac.authorization.k8s.io "pod-reader" deleted from default namespace
$ kubectl delete serviceaccount ci-bot
serviceaccount "ci-bot" deleted from default namespace

Frequently Asked Questions

What is RBAC?
RBAC (Role-Based Access Control) controls who can perform which actions (get, list, create, delete) on which Kubernetes resources (pods, secrets, deployments).
What is the difference between a Role and a ClusterRole?
A Role is namespace-scoped — it grants access within one namespace. A ClusterRole applies cluster-wide and can also be bound in a specific namespace using a RoleBinding.