Skip to main content

Command Palette

Search for a command to run...

K8S-Security

Updated
5 min read

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

  1. Exec into the Pod:

     $ kubectl exec -it test-pod -- sh
    
  2. Check if the Service Account Token is mounted, as we do not have kubectl command, 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 token file contains the authentication token for the Service Account.

  3. 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!

  4. 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!

More from this blog

Clarence's Blog

56 posts

I share insights on programming, web development, cloud computing, computer networks, and AI, alongside financial knowledge, reading notes, and reflections on business and entrepreneurship.