K8S-Security
Intro
Kubernetes security is built on three core pillars: authentication, authorization, and admission control. Properly managing access is crucial to prevent unauthorized access, enforce least privilege, and secure cluster resources.
Authentication: Determines who is accessing the cluster. Kubernetes supports authentication via Service Accounts (SA) and tokens, which are commonly used to identify users and workloads.
Authorization: Defines what authenticated entities can do. This is enforced through Role, ClusterRole, RoleBinding, and ClusterRoleBinding, which grant or restrict permissions within namespaces or across the cluster.
Admission Control: Ensures compliance and resource control before workloads run. Policies such as LimitRanger, ResourceQuota, and PodSecurityPolicy (PSP) help enforce security, resource limits, and operational constraints.
In this blog, we will explore how to secure Kubernetes access management by implementing best practices for authentication, authorization, and admission control to protect your workloads effectively.
Demo
In this section, we are going to demonstrate:
Creating a Service Account
Creating a Role and RoleBinding to grant permissions
Running a Pod using the Service Account
Create a Service Account
By default, Kubernetes assigns a default Service Account to every Pod. Let's create a custom Service Account:
apiVersion: v1
kind: ServiceAccount
metadata:
name: demo-sa
namespace: default
Apply this manifest:
$ kubectl apply -f sa.yaml
serviceaccount/demo-sa created
$ kubectl get sa
NAME SECRETS AGE
default 0 2d12h
demo-sa 0 4s
Create a Role with limited permissions
This Role will allow the Service Account to list and get Pods in the default namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
Apply this Role:
$ kubectl apply -f role.yaml
role.rbac.authorization.k8s.io/pod-reader created
$ kubectl get role
NAME CREATED AT
pod-reader 2025-02-08T22:21:00Z
Bind the Service Account to the Role
Now, we bind the Service Account to the Role using a RoleBinding:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-reader-binding
namespace: default
subjects:
- kind: ServiceAccount
name: demo-sa # Link to our Service Account
namespace: default
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
Apply this RoleBinding:
$ kubectl apply -f role_binding.yaml
rolebinding.rbac.authorization.k8s.io/pod-reader-binding created
$ kubectl get rolebindings
NAME ROLE AGE
pod-reader-binding Role/pod-reader 2m8s
Run a Pod Using the Service Account
Now, we run a Pod that uses this Service Account and test access:
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: default
spec:
serviceAccountName: demo-sa # Assign the Service Account
containers:
- name: test-container
image: curlimages/curl # Lightweight image with curl installed
command: ["sleep", "3600"] # Keep the pod running
Apply this Pod:
$ kubectl apply -f pod.yaml
pod/test-pod created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
test-pod 1/1 Running 0 39s
Verify Service Account Access
Exec into the Pod:
$ kubectl exec -it test-pod -- shCheck if the Service Account Token is mounted, as we do not have
kubectlcommand, we need to calla the API:/ # ls /var/run/secrets/kubernetes.io/serviceaccount/ ca.crt namespace token / # cat /var/run/secrets/kubernetes.io/serviceaccount/token eyJhbGciOiJSUzI1NiIsImtpZCI6IklzYTlFRjBnb1owNENHOHJlMkdoWnZYYlF1Ulc5blZhcjRzaURfZXRBVWcifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzcwNTg5Njg4LCJpYXQiOjE3MzkwNTM2ODgsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNGQ0ZGM3NTMtY2FhNC00ZjAxLWJlYWYtMTBiZTRkNTVlYjU4Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoia2luZC13b3JrZXIyIiwidWlkIjoiZGM2MWIwNGUtOTEwMi00NjVkLWJkMTAtZmUzNzMwNDZkZDkzIn0sInBvZCI6eyJuYW1lIjoidGVzdC1wb2QiLCJ1aWQiOiJjMGQxMWM1NS1jMzM4LTQwMzQtODhlOC01YjExMmY4YzVkMTQifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlbW8tc2EiLCJ1aWQiOiI4YTBjMjMyZi03MjM4LTQwYTktOTAyZC0yNmIxYTNlZGI5NWMifSwid2FybmFmdGVyIjoxNzM5MDU3Mjk1fSwibmJmIjoxNzM5MDUzNjg4LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZW1vLXNhIn0.tUVP8y2iSNjsfdmmuHGdD-ZbkPg8HEwaOfvtDm8v-2LfYoAqIZ9e-9GhyGIwLOsf_4QlgJDeO_sq5XRdAe1mp1IOqZ7VAopZxGihxtg8mP9b2rrVjovy9C4DYDm18m4fEsjwwNG_JxZxoL3rwL9xlQIX0Lp8j0OkBASZVtL93KPGY1dZjLT5C4qAYpDiwnkL3Wi1NkKLfC8lqGfHbML9jRw1dUVuJ0tBmCyIKwUJFsPzbcvwti0OrUed5mZYx--s--MQuH6BOAmtsm3KmkazzESwaEgtbEba2R5t_7wxL0yhZ0mdR19dlfDbWqvNHKbQjYRKJzK3-XNaCJGOqABP8Q/ #The
tokenfile contains the authentication token for the Service Account.Query the Kubernetes API using
curl: Inside the Pod, run:$ kubectl exec -it test-pod -- sh ~ $ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) ~ $ curl -s --header "Authorization: Bearer $TOKEN" --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes.default.svc/api/v1/namespac es/default/pods { "kind": "PodList", "apiVersion": "v1", "metadata": { "resourceVersion": "210587" }, "items": [ { "metadata": { "name": "test-pod", "namespace": "default", "uid": "35da751c-1fb0-4d7c-a7ad-c8c85e6c6e46", "resourceVersion": "210553", "creationTimestamp": "2025-02-08T22:40:04Z", "annotations": { "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"test-pod\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"command\":[\"sleep\",\"3600\"],\"image\":\"curlimages/curl\",\"name\":\"test-container\"}],\"serviceAccountName\":\"demo-sa\"}}\n" }, "managedFields": [ { "manager": "kubectl-client-side-apply", "operation": "Update", "apiVersion": "v1", "time": "2025-02-08T22:40:04Z", "fieldsType": "FieldsV1", "fieldsV1": { "f:metadata": { "f:annotations": { ".": {}, "f:kubectl.kubernetes.io/last-applied-configuration": {} } }, "f:spec": { "f:containers": { "k:{\"name\":\"test-container\"}": { ".": {}, "f:command": {}, "f:image": {}, "f:imagePullPolicy": {}, "f:name": {}, "f:resources": {}, "f:terminationMessagePath": {}, "f:terminationMessagePolicy": {} } }, "f:dnsPolicy": {}, "f:enableServiceLinks": {}, "f:restartPolicy": {}, "f:schedulerName": {}, "f:securityContext": {}, "f:serviceAccount": {}, "f:serviceAccountName": {}, "f:terminationGracePeriodSeconds": {} } } }, { "manager": "kubelet", "operation": "Update", "apiVersion": "v1", "time": "2025-02-08T22:40:09Z", "fieldsType": "FieldsV1", "fieldsV1": { "f:status": { "f:conditions": { "k:{\"type\":\"ContainersReady\"}": { ".": {}, "f:lastProbeTime": {}, "f:lastTransitionTime": {}, "f:status": {}, "f:type": {} }, "k:{\"type\":\"Initialized\"}": { ".": {}, "f:lastProbeTime": {}, "f:lastTransitionTime": {}, "f:status": {}, "f:type": {} }, "k:{\"type\":\"PodReadyToStartContainers\"}": { ".": {}, "f:lastProbeTime": {}, "f:lastTransitionTime": {}, "f:status": {}, "f:type": {} }, "k:{\"type\":\"Ready\"}": { ".": {}, "f:lastProbeTime": {}, "f:lastTransitionTime": {}, "f:status": {}, "f:type": {} } }, "f:containerStatuses": {}, "f:hostIP": {}, "f:hostIPs": {}, "f:phase": {}, "f:podIP": {}, "f:podIPs": { ".": {}, "k:{\"ip\":\"10.244.1.61\"}": { ".": {}, "f:ip": {} } }, "f:startTime": {} } }, "subresource": "status" } ] }, "spec": { "volumes": [ { "name": "kube-api-access-42twn", "projected": { "sources": [ { "serviceAccountToken": { "expirationSeconds": 3607, "path": "token" } }, { "configMap": { "name": "kube-root-ca.crt", "items": [ { "key": "ca.crt", "path": "ca.crt" } ] } }, { "downwardAPI": { "items": [ { "path": "namespace", "fieldRef": { "apiVersion": "v1", "fieldPath": "metadata.namespace" } } ] } } ], "defaultMode": 420 } } ], "containers": [ { "name": "test-container", "image": "curlimages/curl", "command": [ "sleep", "3600" ], "resources": {}, "volumeMounts": [ { "name": "kube-api-access-42twn", "readOnly": true, "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" } ], "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "imagePullPolicy": "Always" } ], "restartPolicy": "Always", "terminationGracePeriodSeconds": 30, "dnsPolicy": "ClusterFirst", "serviceAccountName": "demo-sa", "serviceAccount": "demo-sa", "nodeName": "kind-worker2", "securityContext": {}, "schedulerName": "default-scheduler", "tolerations": [ { "key": "node.kubernetes.io/not-ready", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300 }, { "key": "node.kubernetes.io/unreachable", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300 } ], "priority": 0, "enableServiceLinks": true, "preemptionPolicy": "PreemptLowerPriority" }, "status": { "phase": "Running", "conditions": [ { "type": "PodReadyToStartContainers", "status": "True", "lastProbeTime": null, "lastTransitionTime": "2025-02-08T22:40:09Z" }, { "type": "Initialized", "status": "True", "lastProbeTime": null, "lastTransitionTime": "2025-02-08T22:40:04Z" }, { "type": "Ready", "status": "True", "lastProbeTime": null, "lastTransitionTime": "2025-02-08T22:40:09Z" }, { "type": "ContainersReady", "status": "True", "lastProbeTime": null, "lastTransitionTime": "2025-02-08T22:40:09Z" }, { "type": "PodScheduled", "status": "True", "lastProbeTime": null, "lastTransitionTime": "2025-02-08T22:40:04Z" } ], "hostIP": "172.21.0.4", "hostIPs": [ { "ip": "172.21.0.4" } ], "podIP": "10.244.1.61", "podIPs": [ { "ip": "10.244.1.61" } ], "startTime": "2025-02-08T22:40:04Z", "containerStatuses": [ { "name": "test-container", "state": { "running": { "startedAt": "2025-02-08T22:40:09Z" } }, "lastState": {}, "ready": true, "restartCount": 0, "image": "docker.io/curlimages/curl:latest", "imageID": "docker.io/curlimages/curl@sha256:3dfa70a646c5d03ddf0e7c0ff518a5661e95b8bcbc82079f0fb7453a96eaae35", "containerID": "containerd://2599a892e6c01b69be16d90b946a5f8fc56a66591fe1dc2b45f95991aab423d1", "started": true, "volumeMounts": [ { "name": "kube-api-access-42twn", "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", "readOnly": true, "recursiveReadOnly": "Disabled" } ] } ], "qosClass": "BestEffort" } } ] }✅ This is our expected result: A list of Pods in JSON format!
Try creating a Pod (which should fail):
~ $ curl -s -X POST --header "Authorization: Bearer $TOKEN" --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt -H "Content-Type: application/json" -d '{}' https://kubernetes.default.svc/api/v1/namespaces/default/pods { "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "pods is forbidden: User \"system:serviceaccount:default:demo-sa\" cannot create resource \"pods\" in API group \"\" in the namespace \"default\"", "reason": "Forbidden", "details": { "kind": "pods" }, "code": 403 }❌ We are forbidden to create a pod because of RBAC!

