CertGrid CertGrid
Hands-on Lab·Kubernetes and Cloud Native Security Associate

The 4C Cloud Native Security Model

The 4Cs are usually four nested rings on a slide, which is why people can recite them without using them. This guide asks each layer a question the cluster can answer: what is under the nodes, who may call the API, what the runtime handed a Pod, and what is actually inside the images.

Cloud Native Security Guide 4 of 42 Beginner

Written against the versions above. A four-node kubeadm cluster on bare metal - `providerID` is empty, which is why the Cloud layer here is the physical network rather than a provider API. On a managed cluster that layer is much larger and the page says what changes.

The cka5001 cluster: one control plane and 3 schedulable workers, on Cilium.
Server NameIP AddressOSRolesCPURAMHDD
CKA5001192.168.0.41Ubuntu 26.04 LTSControl Plane Node (tainted NoSchedule)2 Core4 GB50 GB
CKA5001-NODE01192.168.0.42Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA5001-NODE02192.168.0.43Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA5001-NODE03192.168.0.44Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. Cloud: what is underneath, and what a Pod can reach of it

    The outermost C. On this cluster it is smaller than usual, and the reason matters.

    bash Example session
    kubectl get nodes -o custom-columns='NAME:.metadata.name,PROVIDER:.spec.providerID,INTERNAL-IP:.status.addresses[?(@.type=="InternalIP")].address'; echo "--- no providerID: bare metal, so there is no cloud metadata service to protect"NAME             PROVIDER   INTERNAL-IPcka5001          <none>     192.168.0.41cka5001-node01   <none>     192.168.0.42cka5001-node02   <none>     192.168.0.43cka5001-node03   <none>     192.168.0.44--- no providerID: bare metal, so there is no cloud metadata service to protectkubectl run kcsa-4c --image=docker.io/library/busybox:1.37 --restart=Never --command -- sh -c 'sleep 600' >/dev/null && kubectl wait --for=condition=Ready pod/kcsa-4c --timeout=120s >/dev/null && kubectl exec kcsa-4c -- sh -c 'wget -q -T 3 -O- http://169.254.169.254/latest/meta-data/ 2>&1 | head -2'; echo "--- 169.254.169.254 is the cloud metadata address. On a cloud node an unprotected Pod reads instance credentials from it"wget: download timed out--- 169.254.169.254 is the cloud metadata address. On a cloud node an unprotected Pod reads instance credentials from it

    Expected resultPROVIDER <none> on all four nodes, internal IPs on 192.168.0.0/24. Then wget: download timed out against 169.254.169.254.

    Success conditionYou know what the Cloud layer is on this cluster and what it would be elsewhere.

  2. Cluster: the API boundary, and who you are when you cross it

    The second C. Two questions: is the API reachable, and as whom.

    bash Example session
    kubectl get --raw '/readyz?verbose' 2>/dev/null | head -6; echo "--- unquoted, the ? is a shell glob and zsh refuses the command before kubectl sees it"[+]ping ok[+]log ok[+]etcd ok[+]etcd-readiness ok[+]informer-sync ok[+]poststarthook/start-apiserver-admission-initializer ok--- unquoted, the ? is a shell glob and zsh refuses the command before kubectl sees itkubectl get --raw '/livez?verbose' 2>/dev/null | tail -2; echo "---"; kubectl get --raw /api/v1/namespaces/default/pods 2>&1 | head -c 110; echo[+]poststarthook/apiservice-openapiv3-controller oklivez check passed---{"kind":"PodList","apiVersion":"v1","metadata":{"resourceVersion":"2680"},"items":[{"metadata":{"name":"kcsa-4

    Expected result[+]ping ok, [+]log ok, [+]etcd ok, [+]etcd-readiness ok, [+]informer-sync ok and the poststart hooks; then livez check passed and the version payload.

    Success conditionYou can read the apiserver's own health without credentials.

  3. Cluster: and the credential in your own kubeconfig

    Before auditing anyone else, find out what you are.

    bash Example session
    kubectl get --raw '/livez?verbose' 2>/dev/null | tail -2; echo "---"; kubectl get --raw /api/v1/namespaces/default/pods 2>&1 | head -c 110; echo[+]poststarthook/apiservice-openapiv3-controller oklivez check passed---{"kind":"PodList","apiVersion":"v1","metadata":{"resourceVersion":"2680"},"items":[{"metadata":{"name":"kcsa-4kubectl auth whoami 2>/dev/null; echo "--- who the kubeconfig actually is"ATTRIBUTE                                           VALUEUsername                                            kubernetes-adminGroups                                              [kubeadm:cluster-admins system:authenticated]Extra: authentication.kubernetes.io/credential-id   [X509SHA256=da024493843d4cf0b1166a0d2defc96e9ea89084fc6d817e1a2bb955b1a28503]--- who the kubeconfig actually is

    Expected resultA full PodList returned, then Username kubernetes-admin with groups including kubeadm:cluster-admins and an X509 credential id.

    Success conditionYou know how privileged your own access is.

  4. Container: what the runtime handed the Pod

    The third C. Nothing here was requested - it is all default.

    bash Example session
    kubectl get pod kcsa-4c -o jsonpath='{.spec.containers[0].securityContext}'; echo " <- container securityContext"; kubectl get pod kcsa-4c -o jsonpath='{.spec.securityContext}'; echo " <- pod securityContext"; echo "--- both empty, so every default below is in force" <- container securityContext{} <- pod securityContext--- both empty, so every default below is in forcekubectl exec kcsa-4c -- sh -c 'ls -l /proc/self/attr/ 2>/dev/null | head -3'; echo "--- AppArmor is the mandatory access control layer on an Ubuntu node"total 0dr-xr-xr-x    2 root     root             0 Aug 25 11:33 apparmor-rw-rw-rw-    1 root     root             0 Aug 25 11:33 current--- AppArmor is the mandatory access control layer on an Ubuntu nodekubectl exec kcsa-4c -- sh -c 'touch /newfile && echo "root filesystem is WRITABLE"'; echo "--- readOnlyRootFilesystem is not set, so the image is mutable at runtime"root filesystem is WRITABLE--- readOnlyRootFilesystem is not set, so the image is mutable at runtime

    Expected resultBoth securityContexts empty, an apparmor directory under /proc/self/attr/, and root filesystem is WRITABLE.

    Success conditionYou can state exactly what a Pod gets when nobody restricts it.

  5. Code: the image, the digest, and what is in it

    The innermost C, and the one you control most directly.

    bash Example session
    kubectl get pod kcsa-4c -o jsonpath='{.status.containerStatuses[0].image}{"\n"}{.status.containerStatuses[0].imageID}'; echo; echo "--- the tag you asked for, and the DIGEST you actually got"docker.io/library/busybox:1.37docker.io/library/busybox@sha256:9db7b59979c38555a39def84a31fb98b5296952f9e3afd4f6f11f05b07adfab0--- the tag you asked for, and the DIGEST you actually gotkubectl get pod kcsa-4c -o jsonpath='{.spec.containers[0].imagePullPolicy}'; echo " <- imagePullPolicy, defaulted because the tag is not :latest"IfNotPresent <- imagePullPolicy, defaulted because the tag is not :latestkubectl exec kcsa-4c -- sh -c 'ls /bin | wc -l; ls /bin/sh /bin/wget /bin/nc 2>/dev/null | tr "\n" " "'; echo; echo "--- busybox ships a shell, wget and netcat. Every one is a tool an attacker inherits"407/bin/nc /bin/sh /bin/wget--- busybox ships a shell, wget and netcat. Every one is a tool an attacker inherits

    Expected resultTag docker.io/library/busybox:1.37, digest busybox@sha256:9db7b599..., imagePullPolicy IfNotPresent, and 407 entries in /bin including sh, wget and nc.

    Success conditionYou know the difference between what you asked for and what you got.

Troubleshooting

Official sources