You can access this page also inside the Remote Desktop by using the icons on the desktop
- Score
- Questions and Answers
- Preview Questions and Answers
- Exam Tips
You can access this page also inside the Remote Desktop by using the icons on the desktop
Once you've gained access to your terminal it might be wise to spend ~1 minute to setup your environment. You could set these:
xalias k=kubectl # will already be pre-configured
export do="--dry-run=client -o yaml" # k create deploy nginx --image=nginx $do
export now="--force --grace-period 0" # k delete pod x $nowThe following settings will already be configured in your real exam environment in ~/.vimrc. But it can never hurt to be able to type these down:
xxxxxxxxxxset tabstop=2set expandtabset shiftwidth=2
More setup suggestions are in the tips section.
You have access to multiple clusters from your main terminal through kubectl contexts. Write all context names into /opt/course/1/contexts, one per line.
From the kubeconfig extract the certificate of user restricted@infra-prod and write it decoded to /opt/course/1/cert.
Maybe the fastest way is just to run:
xxxxxxxxxxk config get-contexts # copy by hand
k config get-contexts -o name > /opt/course/1/contextsOr using jsonpath:
xxxxxxxxxxk config view -o jsonpath="{.contexts[*].name}"k config view -o jsonpath="{.contexts[*].name}" | tr " " "\n" # new linesk config view -o jsonpath="{.contexts[*].name}" | tr " " "\n" > /opt/course/1/contextsThe content could then look like:
xxxxxxxxxx# /opt/course/1/contextsgianna@infra-prodinfra-prodrestricted@infra-prodworkload-prodworkload-stage
For the certificate we could just run
xxxxxxxxxxk config view --rawAnd copy it manually. Or we do:
xxxxxxxxxxk config view --raw -ojsonpath="{.users[2].user.client-certificate-data}" | base64 -d > /opt/course/1/certOr even:
xxxxxxxxxxk config view --raw -ojsonpath="{.users[?(.name == 'restricted@infra-prod')].user.client-certificate-data}" | base64 -d > /opt/course/1/certxxxxxxxxxx# /opt/course/1/cert-----BEGIN CERTIFICATE-----MIIDHzCCAgegAwIBAgIQN5Qe/Rj/PhaqckEI23LPnjANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJlcm5ldGVzMB4XDTIwMDkyNjIwNTUwNFoXDTIxMDkyNjIwNTUwNFowKjETMBEGA1UEChMKcmVzdHJpY3RlZDETMBEGA1UEAxMKcmVzdHJpY3RlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL/Jaf/QQdijyJTWIDijqa5p4oAh+xDBX3jR9R0G5DkmPU/FgXjxej3rTwHJbuxg7qjTuqQbf9Fb2AHcVtwHgUjC12ODUDE+nVtap+hCe8OLHZwH7BGFWWscgInZOZW2IATK/YdqyQL5OKpQpFkxiAknVZmPa2DTZ8FoyRESboFSTZj6y+JVA7ot0pM09jnxswstal9GZLeqioqfFGY6YBO/Dg4DDsbKhqfUwJVT6Ur3ELsktZIMTRS5By4Xz18798eBiFAHvgJGq1TTwuPMEhBfwYwgYbalL8DSHeFrelLBKgciwUKjr1lolnnuc1vhkX1peV1J3xrf6o2KkyMclY0CAwEAAaNWMFQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUPrspZIWR7YMN8vT5DF3s/LvpxPQwDQYJKoZIhvcNAQELBQADggEBAIDq0Zt77gXI1s+uW46zBw4mIWgAlBLl2QqCuwmVkd86eH5bD0FCtWlb6vGdcKPdFccHh8Z6z2LjjLu6UoiGUdIJaALhbYNJiXXi/7cfM7sqNOxpxQ5X5hyvOBYD1W7d/EzPHV/lcbXPUDYFHNqBYs842LWSTlPQioDpupXpFFUQPxsenNXDa4TbmaRvnK2jka0yXcqdiXuIteZZovp/IgNkfmx2Ld4/Q+XlnscfCFtWbjRa/0W/3EW/ghQ7xtC7bgcOHJesoiTZPCZ+dfKuUfH6d1qxgj6Jwt0HtyEfQTQSc66BdMLnw5DMObs4lXDo2YE6LvMrySdXm/S7img5YzU=-----END CERTIFICATE-----
Use context: kubectl config use-context workload-prod
Falco is installed with default configuration on node cluster1-node1. Connect using ssh cluster1-node1. Use it to:
Find a Pod running image nginx which creates unwanted package management processes inside its container.
Find a Pod running image httpd which modifies /etc/passwd.
Save the Falco logs for case 1 under /opt/course/2/falco.log in format:
xxxxxxxxxxtime-with-nanosconds,container-id,container-name,user-nameNo other information should be in any line. Collect the logs for at least 30 seconds.
Afterwards remove the threads (both 1 and 2) by scaling the replicas of the Deployments that control the offending Pods down to 0.
Falco, the open-source cloud-native runtime security project, is the de facto Kubernetes threat detection engine.
NOTE: Other tools you might have to be familar with are sysdig or tracee
Use Falco as service
First we can investigate Falco config a little:
xxxxxxxxxx➜ ssh cluster1-node1
➜ root@cluster1-node1:~# service falco status● falco.service - LSB: Falco syscall activity monitoring agent Loaded: loaded (/etc/init.d/falco; generated) Active: active (running) since Sat 2020-10-10 06:36:15 UTC; 2h 1min ago...
➜ root@cluster1-node1:~# cd /etc/falco
➜ root@cluster1-node1:/etc/falco# lsfalco.yaml falco_rules.local.yaml falco_rules.yaml k8s_audit_rules.yaml rules.available rules.dThis is the default configuration, if we look into falco.yaml we can see:
xxxxxxxxxx# /etc/falco/falco.yaml
...# Where security notifications should go.# Multiple outputs can be enabled.
syslog_output enabledtrue...This means that Falco is writing into syslog, hence we can do:
xxxxxxxxxx➜ root@cluster1-node1:~# cat /var/log/syslog | grep falcoSep 15 08:44:04 ubuntu2004 falco: Falco version 0.29.1 (driver version 17f5df52a7d9ed6bb12d3b1768460def8439936d)Sep 15 08:44:04 ubuntu2004 falco: Falco initialized with configuration file /etc/falco/falco.yamlSep 15 08:44:04 ubuntu2004 falco: Loading rules from file /etc/falco/falco_rules.yaml:...Yep, quite some action going on in there. Let's investigate the first offending Pod:
xxxxxxxxxx➜ root@cluster1-node1:~# cat /var/log/syslog | grep falco | grep nginx | grep processSep 16 06:23:47 ubuntu2004 falco: 06:23:47.376241377: Error Package management process launched in container (user=root user_loginuid=-1 command=apk container_id=7a5ea6a080d1 container_name=nginx image=docker.io/library/nginx:1.19.2-alpine)...
➜ root@cluster1-node1:~# crictl ps -id 7a5ea6a080d1CONTAINER ID IMAGE NAME ... POD ID7a5ea6a080d1b 6f715d38cfe0e nginx ... 7a864406b9794
root@cluster1-node1:~# crictl pods -id 7a864406b9794POD ID ... NAME NAMESPACE ...7a864406b9794 ... webapi-6cfddcd6f4-ftxg4 team-blue ...First Pod is webapi-6cfddcd6f4-ftxg4 in Namespace team-blue.
xxxxxxxxxx➜ root@cluster1-node1:~# cat /var/log/syslog | grep falco | grep httpd | grep passwdSep 16 06:23:48 ubuntu2004 falco: 06:23:48.830962378: Error File below /etc opened for writing (user=root user_loginuid=-1 command=sed -i $d /etc/passwd parent=sh pcmdline=sh -c echo hacker >> /etc/passwd; sed -i '$d' /etc/passwd; true file=/etc/passwdngFmAl program=sed gparent=<NA> ggparent=<NA> gggparent=<NA> container_id=b1339d5cc2de image=docker.io/library/httpd)
➜ root@cluster1-node1:~# crictl ps -id b1339d5cc2deCONTAINER ID IMAGE NAME ... POD IDb1339d5cc2dee f6b40f9f8ad71 httpd ... 595af943c3245
root@cluster1-node1:~# crictl pods -id 595af943c3245POD ID ... NAME NAMESPACE ...595af943c3245 ... rating-service-68cbdf7b7-v2p6g team-purple ...Second Pod is rating-service-68cbdf7b7-v2p6g in Namespace team-purple.
Eliminate offending Pods
The logs from before should allow us to find and "eliminate" the offending Pods:
xxxxxxxxxx➜ k get pod -A | grep webapiteam-blue webapi-6cfddcd6f4-ftxg4 1/1 Running
➜ k -n team-blue scale deploy webapi --replicas 0deployment.apps/webapi scaled
➜ k get pod -A | grep rating-serviceteam-purple rating-service-68cbdf7b7-v2p6g 1/1 Running
➜ k -n team-purple scale deploy rating-service --replicas 0deployment.apps/rating-service scaled
Use Falco from command line
We can also use Falco directly from command line, but only if the service is disabled:
xxxxxxxxxx➜ root@cluster1-node1:~# service falco stop
➜ root@cluster1-node1:~# falcoThu Sep 16 06:33:11 2021: Falco version 0.29.1 (driver version 17f5df52a7d9ed6bb12d3b1768460def8439936d)Thu Sep 16 06:33:11 2021: Falco initialized with configuration file /etc/falco/falco.yamlThu Sep 16 06:33:11 2021: Loading rules from file /etc/falco/falco_rules.yaml:Thu Sep 16 06:33:11 2021: Loading rules from file /etc/falco/falco_rules.local.yaml:Thu Sep 16 06:33:11 2021: Loading rules from file /etc/falco/k8s_audit_rules.yaml:Thu Sep 16 06:33:12 2021: Starting internal webserver, listening on port 876506:33:17.382603204: Error Package management process launched in container (user=root user_loginuid=-1 command=apk container_id=7a5ea6a080d1 container_name=nginx image=docker.io/library/nginx:1.19.2-alpine)...We can see that rule files are loaded and logs printed afterwards.
Create logs in correct format
The task requires us to store logs for "unwanted package management processes" in format time,container-id,container-name,user-name. The output from falco shows entries for "Error Package management process launched" in a default format. Let's find the proper file that contains the rule and change it:
xxxxxxxxxx➜ root@cluster1-node1:~# cd /etc/falco/
➜ root@cluster1-node1:/etc/falco# grep -r "Package management process launched" ../falco_rules.yaml: Package management process launched in container (user=%user.name user_loginuid=%user.loginuid
➜ root@cluster1-node1:/etc/falco# cp falco_rules.yaml falco_rules.yaml_ori
➜ root@cluster1-node1:/etc/falco# vim falco_rules.yamlFind the rule which looks like this:
xxxxxxxxxx# Container is supposed to be immutable. Package management should be done in building the image.ruleLaunch Package Management Process in Container descPackage management process ran inside container condition spawned_process and container and user.name != "_apt" and package_mgmt_procs and not package_mgmt_ancestor_procs and not user_known_package_manager_in_container output Package management process launched in container (user=%user.name user_loginuid=%user.loginuid command=%proc.cmdline container_id=%container.id container_name=%container.name image=%container.image.repository:%container.image.tag) priorityERROR tagsprocess mitre_persistenceShould be changed into the required format:
xxxxxxxxxx# Container is supposed to be immutable. Package management should be done in building the image.ruleLaunch Package Management Process in Container descPackage management process ran inside container condition spawned_process and container and user.name != "_apt" and package_mgmt_procs and not package_mgmt_ancestor_procs and not user_known_package_manager_in_container output Package management process launched in container %evt.time,%container.id,%container.name,%user.name priorityERROR tagsprocess mitre_persistenceFor all available fields we can check https://falco.org/docs/rules/supported-fields, which should be allowed to open during the exam.
Next we check the logs in our adjusted format:
xxxxxxxxxx➜ root@cluster1-node1:/etc/falco# falco | grep "Package management"
06:38:28.077150666: Error Package management process launched in container 06:38:28.077150666,090aad374a0a,nginx,root06:38:33.058263010: Error Package management process launched in container 06:38:33.058263010,090aad374a0a,nginx,root06:38:38.068693625: Error Package management process launched in container 06:38:38.068693625,090aad374a0a,nginx,root06:38:43.066159360: Error Package management process launched in container 06:38:43.066159360,090aad374a0a,nginx,root06:38:48.059792139: Error Package management process launched in container 06:38:48.059792139,090aad374a0a,nginx,root06:38:53.063328933: Error Package management process launched in container 06:38:53.063328933,090aad374a0a,nginx,rootThis looks much better. Copy&paste the output into file /opt/course/2/falco.log on your main terminal. The content should be cleaned like this:
xxxxxxxxxx# /opt/course/2/falco.log06:38:28.077150666,090aad374a0a,nginx,root06:38:33.058263010,090aad374a0a,nginx,root06:38:38.068693625,090aad374a0a,nginx,root06:38:43.066159360,090aad374a0a,nginx,root06:38:48.059792139,090aad374a0a,nginx,root06:38:53.063328933,090aad374a0a,nginx,root06:38:58.070912841,090aad374a0a,nginx,root06:39:03.069592140,090aad374a0a,nginx,root06:39:08.064805371,090aad374a0a,nginx,root06:39:13.078109098,090aad374a0a,nginx,root06:39:18.065077287,090aad374a0a,nginx,root06:39:23.061012151,090aad374a0a,nginx,root
For a few entries it should be fast to just clean it up manually. If there are larger amounts of entries we could do:
xxxxxxxxxxcat /opt/course/2/falco.log.dirty | cut -d" " -f 9 > /opt/course/2/falco.logThe tool cut will split input into fields using space as the delimiter (-d""). We then only select the 9th field using -f 9.
Local falco rules
There is also a file /etc/falco/falco_rules.local.yaml in which we can override existing default rules. This is a much cleaner solution for production. Choose the faster way for you in the exam if nothing is specified in the task.
Use context: kubectl config use-context workload-prod
You received a list from the DevSecOps team which performed a security investigation of the k8s cluster1 (workload-prod). The list states the following about the apiserver setup:
Accessible through a NodePort Service
Change the apiserver setup so that:
Only accessible through a ClusterIP Service
In order to modify the parameters for the apiserver, we first ssh into the master node and check which parameters the apiserver process is running with:
xxxxxxxxxx➜ ssh cluster1-controlplane1
➜ root@cluster1-controlplane1:~# ps aux | grep kube-apiserverroot 27622 7.4 15.3 1105924 311788 ? Ssl 10:31 11:03 kube-apiserver --advertise-address=192.168.100.11 --allow-privileged=true --authorization-mode=Node,RBAC --client-ca-file=/etc/kubernetes/pki/ca.crt --enable-admission-plugins=NodeRestriction --enable-bootstrap-token-auth=true --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key --etcd-servers=https://127.0.0.1:2379 --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --kubernetes-service-node-port=31000 --proxy-client-cert-...We may notice the following argument:
xxxxxxxxxx--kubernetes-service-node-port=31000
We can also check the Service and see it's of type NodePort:
xxxxxxxxxx➜ root@cluster1-controlplane1:~# kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes NodePort 10.96.0.1 <none> 443:31000/TCP 5d2hThe apiserver runs as a static Pod, so we can edit the manifest. But before we do this we also create a copy in case we mess things up:
xxxxxxxxxx➜ root@cluster1-controlplane1:~# cp /etc/kubernetes/manifests/kube-apiserver.yaml ~/3_kube-apiserver.yaml
➜ root@cluster1-controlplane1:~# vim /etc/kubernetes/manifests/kube-apiserver.yamlWe should remove the unsecure settings:
xxxxxxxxxx# /etc/kubernetes/manifests/kube-apiserver.yamlapiVersionv1kindPodmetadata annotations kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint192.168.100.116443 creationTimestampnull labels componentkube-apiserver tiercontrol-plane namekube-apiserver namespacekube-systemspec containerscommandkube-apiserver--advertise-address=192.168.100.11--allow-privileged=true--authorization-mode=Node,RBAC--client-ca-file=/etc/kubernetes/pki/ca.crt--enable-admission-plugins=NodeRestriction--enable-bootstrap-token-auth=true--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt--etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt--etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key--etcd-servers=https://127.0.0.1:2379--kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt--kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname# - --kubernetes-service-node-port=31000 # delete or set to 0--proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt--proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key...Once the changes are made, give the apiserver some time to start up again. Check the apiserver's Pod status and the process parameters:
xxxxxxxxxx➜ root@cluster1-controlplane1:~# kubectl -n kube-system get pod | grep apiserverkube-apiserver-cluster1-controlplane1 1/1 Running 0 38s
➜ root@cluster1-controlplane1:~# ps aux | grep kube-apiserver | grep node-portThe apiserver got restarted without the unsecure settings. However, the Service kubernetes will still be of type NodePort:
xxxxxxxxxx➜ root@cluster1-controlplane1:~# kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes NodePort 10.96.0.1 <none> 443:31000/TCP 5d3hWe need to delete the Service for the changes to take effect:
xxxxxxxxxx➜ root@cluster1-controlplane1:~# kubectl delete svc kubernetesservice "kubernetes" deletedAfter a few seconds:
xxxxxxxxxx➜ root@cluster1-controlplane1:~# kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6sThis should satisfy the DevSecOps team.
Use context: kubectl config use-context workload-prod
There is Deployment container-host-hacker in Namespace team-red which mounts /run/containerd as a hostPath volume on the Node where it's running. This means that the Pod can access various data about other containers running on the same Node.
To prevent this configure Namespace team-red to enforce the baseline Pod Security Standard. Once completed, delete the Pod of the Deployment mentioned above.
Check the ReplicaSet events and write the event/log lines containing the reason why the Pod isn't recreated into /opt/course/4/logs.
Making Namespaces use Pod Security Standards works via labels. We can simply edit it:
xxxxxxxxxxk edit ns team-redNow we configure the requested label:
xxxxxxxxxx# kubectl edit namespace team-redapiVersionv1kindNamespacemetadata labels kubernetes.io/metadata.nameteam-red pod-security.kubernetes.io/enforcebaseline # add nameteam-red...This should already be enough for the default Pod Security Admission Controller to pick up on that change. Let's test it and delete the Pod to see if it'll be recreated or fails, it should fail!
xxxxxxxxxx➜ k -n team-red get podNAME READY STATUS RESTARTS AGEcontainer-host-hacker-dbf989777-wm8fc 1/1 Running 0 115s
➜ k -n team-red delete pod container-host-hacker-dbf989777-wm8fc pod "container-host-hacker-dbf989777-wm8fc" deleted
➜ k -n team-red get podNo resources found in team-red namespace.Usually the ReplicaSet of a Deployment would recreate the Pod if deleted, here we see this doesn't happen. Let's check why:
xxxxxxxxxx➜ k -n team-red get rsNAME DESIRED CURRENT READY AGEcontainer-host-hacker-dbf989777 1 0 0 5m25s
➜ k -n team-red describe rs container-host-hacker-dbf989777Name: container-host-hacker-dbf989777Namespace: team-red...Events: Type Reason Age From Message ---- ------ ---- ---- -------... Warning FailedCreate 2m41s replicaset-controller Error creating: pods "container-host-hacker-dbf989777-bjwgv" is forbidden: violates PodSecurity "baseline:latest": hostPath volumes (volume "containerdata") Warning FailedCreate 2m2s (x9 over 2m40s) replicaset-controller (combined from similar events): Error creating: pods "container-host-hacker-dbf989777-kjfpn" is forbidden: violates PodSecurity "baseline:latest": hostPath volumes (volume "containerdata")There we go! Finally we write the reason into the requested file so that Mr Scoring will be happy too!
xxxxxxxxxx# /opt/course/4/logsWarning FailedCreate 2m2s (x9 over 2m40s) replicaset-controller (combined from similar events): Error creating: pods "container-host-hacker-dbf989777-kjfpn" is forbidden: violates PodSecurity "baseline:latest": hostPath volumes (volume "containerdata")
Pod Security Standards can give a great base level of security! But when one finds themselves wanting to deeper adjust the levels like baseline or restricted... this isn't possible and 3rd party solutions like OPA could be looked at.
Use context: kubectl config use-context infra-prod
You're ask to evaluate specific settings of cluster2 against the CIS Benchmark recommendations. Use the tool kube-bench which is already installed on the nodes.
Connect using ssh cluster2-controlplane1 and ssh cluster2-node1.
On the master node ensure (correct if necessary) that the CIS recommendations are set for:
The --profiling argument of the kube-controller-manager
The ownership of directory /var/lib/etcd
On the worker node ensure (correct if necessary) that the CIS recommendations are set for:
The permissions of the kubelet configuration /var/lib/kubelet/config.yaml
The --client-ca-file argument of the kubelet
First we ssh into the master node run kube-bench against the master components:
xxxxxxxxxx➜ ssh cluster2-controlplane1
➜ root@cluster2-controlplane1:~# kube-bench run --targets=master...== Summary ==41 checks PASS13 checks FAIL11 checks WARN0 checks INFOWe see some passes, fails and warnings. Let's check the required task (1) of the controller manager:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# kube-bench run --targets=master | grep kube-controller -A 31.3.1 Edit the Controller Manager pod specification file /etc/kubernetes/manifests/kube-controller-manager.yamlon the master node and set the --terminated-pod-gc-threshold to an appropriate threshold,for example:--terminated-pod-gc-threshold=10--1.3.2 Edit the Controller Manager pod specification file /etc/kubernetes/manifests/kube-controller-manager.yamlon the master node and set the below parameter.--profiling=false
1.3.6 Edit the Controller Manager pod specification file /etc/kubernetes/manifests/kube-controller-manager.yamlon the master node and set the --feature-gates parameter to include RotateKubeletServerCertificate=true.--feature-gates=RotateKubeletServerCertificate=trueThere we see 1.3.2 which suggests to set --profiling=false, so we obey:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# vim /etc/kubernetes/manifests/kube-controller-manager.yamlEdit the corresponding line:
xxxxxxxxxx# /etc/kubernetes/manifests/kube-controller-manager.yamlapiVersionv1kindPodmetadata creationTimestampnull labels componentkube-controller-manager tiercontrol-plane namekube-controller-manager namespacekube-systemspec containerscommandkube-controller-manager--allocate-node-cidrs=true--authentication-kubeconfig=/etc/kubernetes/controller-manager.conf--authorization-kubeconfig=/etc/kubernetes/controller-manager.conf--bind-address=127.0.0.1--client-ca-file=/etc/kubernetes/pki/ca.crt--cluster-cidr=10.244.0.0/16--cluster-name=kubernetes--cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt--cluster-signing-key-file=/etc/kubernetes/pki/ca.key--controllers=*,bootstrapsigner,tokencleaner--kubeconfig=/etc/kubernetes/controller-manager.conf--leader-elect=true--node-cidr-mask-size=24--port=0--requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt--root-ca-file=/etc/kubernetes/pki/ca.crt--service-account-private-key-file=/etc/kubernetes/pki/sa.key--service-cluster-ip-range=10.96.0.0/12--use-service-account-credentials=true--profiling=false # add...We wait for the Pod to restart, then run kube-bench again to check if the problem was solved:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# kube-bench run --targets=master | grep kube-controller -A 31.3.1 Edit the Controller Manager pod specification file /etc/kubernetes/manifests/kube-controller-manager.yamlon the master node and set the --terminated-pod-gc-threshold to an appropriate threshold,for example:--terminated-pod-gc-threshold=10--1.3.6 Edit the Controller Manager pod specification file /etc/kubernetes/manifests/kube-controller-manager.yamlon the master node and set the --feature-gates parameter to include RotateKubeletServerCertificate=true.--feature-gates=RotateKubeletServerCertificate=trueProblem solved and 1.3.2 is passing:
xxxxxxxxxxroot@cluster2-controlplane1:~# kube-bench run --targets=master | grep 1.3.2[PASS] 1.3.2 Ensure that the --profiling argument is set to false (Scored)
Next task (2) is to check the ownership of directory /var/lib/etcd, so we first have a look:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# ls -lh /var/lib | grep etcddrwx------ 3 root root 4.0K Sep 11 20:08 etcdLooks like user root and group root. Also possible to check using:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# stat -c %U:%G /var/lib/etcdroot:rootBut what has kube-bench to say about this?
xxxxxxxxxx➜ root@cluster2-controlplane1:~# kube-bench run --targets=master | grep "/var/lib/etcd" -B5
1.1.12 On the etcd server node, get the etcd data directory, passed as an argument --data-dir,from the below command:ps -ef | grep etcdRun the below command (based on the etcd data directory found above).For example, chown etcd:etcd /var/lib/etcdTo comply we run the following:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# chown etcd:etcd /var/lib/etcd
➜ root@cluster2-controlplane1:~# ls -lh /var/lib | grep etcddrwx------ 3 etcd etcd 4.0K Sep 11 20:08 etcdThis looks better. We run kube-bench again, and make sure test 1.1.12. is passing.
xxxxxxxxxx➜ root@cluster2-controlplane1:~# kube-bench run --targets=master | grep 1.1.12[PASS] 1.1.12 Ensure that the etcd data directory ownership is set to etcd:etcd (Scored)Done.
To continue with number (3), we'll head to the worker node and ensure that the kubelet configuration file has the minimum necessary permissions as recommended:
xxxxxxxxxx➜ ssh cluster2-node1
➜ root@cluster2-node1:~# kube-bench run --targets=node...== Summary ==13 checks PASS10 checks FAIL2 checks WARN0 checks INFOAlso here some passes, fails and warnings. We check the permission level of the kubelet config file:
xxxxxxxxxx➜ root@cluster2-node1:~# stat -c %a /var/lib/kubelet/config.yaml777777 is highly permissive access level and not recommended by the kube-bench guidelines:
xxxxxxxxxx➜ root@cluster2-node1:~# kube-bench run --targets=node | grep /var/lib/kubelet/config.yaml -B2
4.1.9 Run the following command (using the config file location identified in the Audit step)chmod 644 /var/lib/kubelet/config.yamlWe obey and set the recommended permissions:
xxxxxxxxxx➜ root@cluster2-node1:~# chmod 644 /var/lib/kubelet/config.yaml
➜ root@cluster2-node1:~# stat -c %a /var/lib/kubelet/config.yaml644And check if test 2.2.10 is passing:
xxxxxxxxxx➜ root@cluster2-node1:~# kube-bench run --targets=node | grep 4.1.9[PASS] 2.2.10 Ensure that the kubelet configuration file has permissions set to 644 or more restrictive (Scored)
Finally for number (4), let's check whether --client-ca-file argument for the kubelet is set properly according to kube-bench recommendations:
xxxxxxxxxx➜ root@cluster2-node1:~# kube-bench run --targets=node | grep client-ca-file[PASS] 4.2.3 Ensure that the --client-ca-file argument is set as appropriate (Automated)This looks passing with 4.2.3. The other ones are about the file that the parameter points to and can be ignored here.
To further investigate we run the following command to locate the kubelet config file, and open it:
xxxxxxxxxx➜ root@cluster2-node1:~# ps -ef | grep kubeletroot 5157 1 2 20:28 ? 00:03:22 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.2root 19940 11901 0 22:38 pts/0 00:00:00 grep --color=auto kubelet
➜ root@croot@cluster2-node1:~# vim /var/lib/kubelet/config.yamlxxxxxxxxxx# /var/lib/kubelet/config.yamlapiVersionkubelet.config.k8s.io/v1beta1authentication anonymous enabledfalse webhook cacheTTL0s enabledtrue x509 clientCAFile/etc/kubernetes/pki/ca.crt...The clientCAFile points to the location of the certificate, which is correct.
(can be solved in any kubectl context)
There are four Kubernetes server binaries located at /opt/course/6/binaries. You're provided with the following verified sha512 values for these:
kube-apiserver
f417c0555bc0167355589dd1afe23be9bf909bf98312b1025f12015d1b58a1c62c9908c0067a7764fa35efdac7016a9efa8711a44425dd6692906a7c283f032c
kube-controller-manager
60100cc725e91fe1a949e1b2d0474237844b5862556e25c2c655a33boa8225855ec5ee22fa4927e6c46a60d43a7c4403a27268f96fbb726307d1608b44f38a60
kube-proxy
52f9d8ad045f8eee1d689619ef8ceef2d86d50c75a6a332653240d7ba5b2a114aca056d9e513984ade24358c9662714973c1960c62a5cb37dd375631c8a614c6
kubelet
4be40f2440619e990897cf956c32800dc96c2c983bf64519854a3309fa5aa21827991559f9c44595098e27e6f2ee4d64a3fdec6baba8a177881f20e3ec61e26c
Delete those binaries that don't match with the sha512 values above.
We check the directory:
xxxxxxxxxx➜ cd /opt/course/6/binaries
➜ lskube-apiserver kube-controller-manager kube-proxy kubeletTo generate the sha512 sum of a binary we do:
xxxxxxxxxx➜ sha512sum kube-apiserver f417c0555bc0167355589dd1afe23be9bf909bf98312b1025f12015d1b58a1c62c9908c0067a7764fa35efdac7016a9efa8711a44425dd6692906a7c283f032c kube-apiserverLooking good, next:
xxxxxxxxxx➜ sha512sum kube-controller-manager60100cc725e91fe1a949e1b2d0474237844b5862556e25c2c655a33b0a8225855ec5ee22fa4927e6c46a60d43a7c4403a27268f96fbb726307d1608b44f38a60 kube-controller-managerOkay, next:
xxxxxxxxxx➜ sha512sum kube-proxy52f9d8ad045f8eee1d689619ef8ceef2d86d50c75a6a332653240d7ba5b2a114aca056d9e513984ade24358c9662714973c1960c62a5cb37dd375631c8a614c6 kube-proxyAlso good, and finally:
xxxxxxxxxx➜ sha512sum kubelet7b720598e6a3483b45c537b57d759e3e82bc5c53b3274f681792f62e941019cde3d51a7f9b55158abf3810d506146bc0aa7cf97b36f27f341028a54431b335be kubeletCatch! Binary kubelet has a different hash!
But did we actually compare everything properly before? Let's have a closer look at kube-controller-manager again:
xxxxxxxxxx➜ sha512sum kube-controller-manager > compare
➜ vim compare Edit to only have the provided hash and the generated one in one line each:
xxxxxxxxxx# ./compare60100cc725e91fe1a949e1b2d0474237844b5862556e25c2c655a33b0a8225855ec5ee22fa4927e6c46a60d43a7c4403a27268f96fbb726307d1608b44f38a6060100cc725e91fe1a949e1b2d0474237844b5862556e25c2c655a33boa8225855ec5ee22fa4927e6c46a60d43a7c4403a27268f96fbb726307d1608b44f38a60
Looks right at a first glance, but if we do:
xxxxxxxxxx➜ cat compare | uniq60100cc725e91fe1a949e1b2d0474237844b5862556e25c2c655a33b0a8225855ec5ee22fa4927e6c46a60d43a7c4403a27268f96fbb726307d1608b44f38a6060100cc725e91fe1a949e1b2d0474237844b5862556e25c2c655a33boa8225855ec5ee22fa4927e6c46a60d43a7c4403a27268f96fbb726307d1608b44f38a60This shows they are different, by just one character actually.
To complete the task we do:
xxxxxxxxxxrm kubelet kube-controller-manager
Use context: kubectl config use-context infra-prod
The Open Policy Agent and Gatekeeper have been installed to, among other things, enforce blacklisting of certain image registries. Alter the existing constraint and/or template to also blacklist images from very-bad-registry.com.
Test it by creating a single Pod using image very-bad-registry.com/image in Namespace default, it shouldn't work.
You can also verify your changes by looking at the existing Deployment untrusted in Namespace default, it uses an image from the new untrusted source. The OPA contraint should throw violation messages for this one.
We look at existing OPA constraints, these are implemeted using CRDs by Gatekeeper:
xxxxxxxxxx➜ k get crdNAME CREATED ATblacklistimages.constraints.gatekeeper.sh 2020-09-14T19:29:31Zconfigs.config.gatekeeper.sh 2020-09-14T19:29:04Zconstraintpodstatuses.status.gatekeeper.sh 2020-09-14T19:29:05Zconstrainttemplatepodstatuses.status.gatekeeper.sh 2020-09-14T19:29:05Zconstrainttemplates.templates.gatekeeper.sh 2020-09-14T19:29:05Zrequiredlabels.constraints.gatekeeper.sh 2020-09-14T19:29:31ZSo we can do:
xxxxxxxxxx➜ k get constraintNAME AGEblacklistimages.constraints.gatekeeper.sh/pod-trusted-images 10m
NAME AGErequiredlabels.constraints.gatekeeper.sh/namespace-mandatory-labels 10mand then look at the one that is probably about blacklisting images:
xxxxxxxxxxk edit blacklistimages pod-trusted-imagesxxxxxxxxxx# kubectl edit blacklistimages pod-trusted-imagesapiVersionconstraints.gatekeeper.sh/v1beta1kindBlacklistImagesmetadata...spec match kindsapiGroups"" kindsPodIt looks like this constraint simply applies the template to all Pods, no arguments passed. So we edit the template:
xxxxxxxxxxk edit constrainttemplates blacklistimagesxxxxxxxxxx# kubectl edit constrainttemplates blacklistimagesapiVersiontemplates.gatekeeper.sh/v1beta1kindConstraintTemplatemetadata...spec crd spec names kindBlacklistImages targetsrego package k8strustedimages
images { image := input.review.object.spec.containers[_].image not startswith(image, "docker-fake.io/") not startswith(image, "google-gcr-fake.com/") not startswith(image, "very-bad-registry.com/") # ADD THIS LINE }
violation[{"msg": msg}] { not images msg := "not trusted image!" } target: admission.k8s.gatekeeper.shWe simply have to add another line. After editing we try to create a Pod of the bad image:
xxxxxxxxxx➜ k run opa-test --image=very-bad-registry.com/imageError from server ([denied by pod-trusted-images] not trusted image!): admission webhook "validation.gatekeeper.sh" denied the request: [denied by pod-trusted-images] not trusted image!Nice! After some time we can also see that Pods of the existing Deployment "untrusted" will be listed as violators:
xxxxxxxxxx➜ k describe blacklistimages pod-trusted-images... Total Violations: 2 Violations: Enforcement Action: deny Kind: Namespace Message: you must provide labels: {"security-level"} Name: sidecar-injector Enforcement Action: deny Kind: Pod Message: not trusted image! Name: untrusted-68c4944d48-tfsnb Namespace: defaultEvents: <none>Great, OPA fights bad registries !
Use context: kubectl config use-context workload-prod
The Kubernetes Dashboard is installed in Namespace kubernetes-dashboard and is configured to:
Allow users to "skip login"
Allow insecure access (HTTP without authentication)
Allow basic authentication
Allow access from outside the cluster
You are asked to make it more secure by:
Deny users to "skip login"
Deny insecure access, enforce HTTPS (self signed certificates are ok for now)
Add the --auto-generate-certificates argument
Enforce authentication using a token (with possibility to use RBAC)
Allow only cluster internal access
Head to https://github.com/kubernetes/dashboard/tree/master/docs to find documentation about the dashboard. This link is not on the allowed list of urls during the real exam. This means you should be provided will all information necessary in case of a task like this.
First we have a look in Namespace kubernetes-dashboard:
xxxxxxxxxx➜ k -n kubernetes-dashboard get pod,svcNAME READY STATUS RESTARTS AGEpod/dashboard-metrics-scraper-7b59f7d4df-fbpd9 1/1 Running 0 24mpod/kubernetes-dashboard-6d8cd5dd84-w7wr2 1/1 Running 0 24m
NAME TYPE ... PORT(S) AGEservice/dashboard-metrics-scraper ClusterIP ... 8000/TCP 24mservice/kubernetes-dashboard NodePort ... 9090:32520/TCP,443:31206/TCP 24mWe can see one running Pod and a NodePort Service exposing it. Let's try to connect to it via a NodePort, we can use IP of any Node:
(your port might be a different)
xxxxxxxxxx➜ k get node -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP ...cluster1-controlplane1 Ready master 37m v1.30.1 192.168.100.11 ...cluster1-node1 Ready <none> 36m v1.30.1 192.168.100.12 ...cluster1-node2 Ready <none> 34m v1.30.1 192.168.100.13 ...
➜ curl http://192.168.100.11:32520<!--Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0The dashboard is not secured because it allows unsecure HTTP access without authentication and is exposed externally. It's is loaded with a few parameter making it insecure, let's fix this.
First we create a backup in case we need to undo something:
xxxxxxxxxxk -n kubernetes-dashboard get deploy kubernetes-dashboard -oyaml > 8_deploy_kubernetes-dashboard.yamlThen:
xxxxxxxxxxk -n kubernetes-dashboard edit deploy kubernetes-dashboardThe changes to make are :
xxxxxxxxxx template spec containersargs--namespace=kubernetes-dashboard --authentication-mode=token # change or delete, "token" is default--auto-generate-certificates # add #- --enable-skip-login=true # delete or set to false #- --enable-insecure-login # delete imagekubernetesui/dashboardv2.0.3 imagePullPolicyAlways namekubernetes-dashboard
Next, we'll have to deal with the NodePort Service:
xxxxxxxxxxk -n kubernetes-dashboard get svc kubernetes-dashboard -o yaml > 8_svc_kubernetes-dashboard.yaml # backup
k -n kubernetes-dashboard edit svc kubernetes-dashboardAnd make the following changes:
xxxxxxxxxx spec clusterIP10.107.176.19 externalTrafficPolicyCluster # delete internalTrafficPolicyCluster portsnamehttp nodePort32513 # delete port9090 protocolTCP targetPort9090namehttps nodePort32441 # delete port443 protocolTCP targetPort8443 selector k8s-appkubernetes-dashboard sessionAffinityNone typeClusterIP # change or deletestatus loadBalancer
Let's confirm the changes, we can do that even without having a browser:
xxxxxxxxxx➜ k run tmp --image=nginx:1.19.2 --restart=Never --rm -it -- bashIf you don't see a command prompt, try pressing enter.root@tmp:/# curl http://kubernetes-dashboard.kubernetes-dashboard:9090curl: (7) Failed to connect to kubernetes-dashboard.kubernetes-dashboard port 9090: Connection refused
➜ root@tmp:/# curl https://kubernetes-dashboard.kubernetes-dashboardcurl: (60) SSL certificate problem: self signed certificateMore details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could notestablish a secure connection to it. To learn more about this situation andhow to fix it, please visit the web page mentioned above.
➜ root@tmp:/# curl https://kubernetes-dashboard.kubernetes-dashboard -k<!--Copyright 2017 The Kubernetes Authors.We see that insecure access is disabled and HTTPS works (using a self signed certificate for now). Let's also check the remote access:
(your port might be a different)
xxxxxxxxxx➜ curl http://192.168.100.11:32520curl: (7) Failed to connect to 192.168.100.11 port 32520: Connection refused
➜ k -n kubernetes-dashboard get svcNAME TYPE CLUSTER-IP ... PORT(S)dashboard-metrics-scraper ClusterIP 10.111.171.247 ... 8000/TCPkubernetes-dashboard ClusterIP 10.100.118.128 ... 9090/TCP,443/TCPMuch better.
Use context: kubectl config use-context workload-prod
Some containers need to run more secure and restricted. There is an existing AppArmor profile located at /opt/course/9/profile for this.
Install the AppArmor profile on Node cluster1-node1. Connect using ssh cluster1-node1
Add label security=apparmor to the Node
Create a Deployment named apparmor in Namespace default with:
One replica of image nginx:1.19.2
NodeSelector for security=apparmor
Single container named c1 with the AppArmor profile enabled only for this container
The Pod might not run properly with the profile enabled. Write the logs of the Pod into /opt/course/9/logs so another team can work on getting the application running.
https://kubernetes.io/docs/tutorials/clusters/apparmor
First we have a look at the provided profile:
xxxxxxxxxxvim /opt/course/9/profilexxxxxxxxxx# /opt/course/9/profile
#include <tunables/global> profile very-secure flags=(attach_disconnected) { #include <abstractions/base>
file,
# Deny all file writes. deny /** w,}Very simple profile named very-secure which denies all file writes. Next we copy it onto the Node:
xxxxxxxxxx➜ scp /opt/course/9/profile cluster1-node1:~/Warning: Permanently added the ECDSA host key for IP address '192.168.100.12' to the list of known hosts.profile 100% 161 329.9KB/s 00:00
➜ ssh cluster1-node1
➜ root@cluster1-node1:~# lsprofileAnd install it:
xxxxxxxxxx➜ root@cluster1-node1:~# apparmor_parser -q ./profileVerify it has been installed:
xxxxxxxxxx➜ root@cluster1-node1:~# apparmor_statusapparmor module is loaded.17 profiles are loaded.17 profiles are in enforce mode. /sbin/dhclient... man_filter man_groff very-secure0 profiles are in complain mode.56 processes have profiles defined.56 processes are in enforce mode....0 processes are in complain mode.0 processes are unconfined but have a profile defined.There we see among many others the very-secure one, which is the name of the profile specified in /opt/course/9/profile.
We label the Node:
xxxxxxxxxxk label -h # show examples
k label node cluster1-node1 security=apparmor
Now we can go ahead and create the Deployment which uses the profile.
xxxxxxxxxxk create deploy apparmor --image=nginx:1.19.2 $do > 9_deploy.yaml
vim 9_deploy.yamlx
# 9_deploy.yamlapiVersionapps/v1kindDeploymentmetadata creationTimestampnull labels appapparmor nameapparmor namespacedefaultspec replicas1 selector matchLabels appapparmor strategy template metadata creationTimestampnull labels appapparmor spec nodeSelector# add securityapparmor # add containersimagenginx1.19.2 namec1 # change securityContext# add appArmorProfile# add typeLocalhost # add localhostProfilevery-secure # addxxxxxxxxxxk -f 9_deploy.yaml createWhat's the damage?
xxxxxxxxxx➜ k get pod -owide | grep apparmorapparmor-85c65645dc-jbch8 0/1 CrashLoopBackOff ... cluster1-node1
➜ k logs apparmor-85c65645dc-w852p/docker-entrypoint.sh: 13: /docker-entrypoint.sh: cannot create /dev/null: Permission denied/docker-entrypoint.sh: No files found in /docker-entrypoint.d/, skipping configuration2021/09/15 11:51:57 [emerg] 1#1: mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)This looks alright, the Pod is running on cluster1-node1 because of the nodeSelector. The AppArmor profile simply denies all filesystem writes, but Nginx needs to write into some locations to run, hence the errors.
It looks like our profile is running but we can confirm this as well by inspecting the container:
x
➜ ssh cluster1-node1
➜ root@cluster1-node1:~# crictl pods | grep apparmorbe5c0aecee7c7 4 minutes ago Ready apparmor-85c65645dc-jbch8 ...
➜ root@cluster1-node1:~# crictl ps -a | grep be5c0aecee7c7e4d91cbdf72fb ... Exited c1 6 be5c0aecee7c7
➜ root@cluster1-node1:~# crictl inspect e4d91cbdf72fb | grep -i profile "apparmor_profile": "localhost/very-secure", "apparmorProfile": "very-secure",First we find the Pod by it's name and get the pod-id. Next we use crictl ps -a to also show stopped containers. Then crictl inspect shows that the container is using our AppArmor profile. Notice to be fast between ps and inspect as K8s will restart the Pod periodically when in error state.
To complete the task we write the logs into the required location:
xxxxxxxxxxk logs apparmor-85c65645dc-jbch8 > /opt/course/9/logsFixing the errors is the job of another team, lucky us.
Use context: kubectl config use-context workload-prod
Team purple wants to run some of their workloads more secure. Worker node cluster1-node2 has container engine containerd already installed and it's configured to support the runsc/gvisor runtime.
Create a RuntimeClass named gvisor with handler runsc.
Create a Pod that uses the RuntimeClass. The Pod should be in Namespace team-purple, named gvisor-test and of image nginx:1.19.2. Make sure the Pod runs on cluster1-node2.
Write the dmesg output of the successfully started Pod into /opt/course/10/gvisor-test-dmesg.
We check the nodes and we can see that all are using containerd:
xxxxxxxxxx➜ k get node -o wideNAME STATUS ROLES ... CONTAINER-RUNTIMEcluster1-controlplane1 Ready control-plane ... containerd://1.5.2cluster1-node1 Ready <none> ... containerd://1.5.2cluster1-node2 Ready <none> ... containerd://1.5.2But just one has containerd configured to work with runsc/gvisor runtime which is cluster1-node2.
(Optionally) we ssh into the worker node and check if containerd+runsc is configured:
xxxxxxxxxx➜ ssh cluster1-node2
➜ root@cluster1-node2:~# runsc --versionrunsc version release-20201130.0spec: 1.0.1-dev
➜ root@cluster1-node2:~# cat /etc/containerd/config.toml | grep runsc [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc] runtime_type = "io.containerd.runsc.v1"Now we best head to the k8s docs for RuntimeClasses https://kubernetes.io/docs/concepts/containers/runtime-class, steal an example and create the gvisor one:
xxxxxxxxxxvim 10_rtc.yamlxxxxxxxxxx# 10_rtc.yamlapiVersionnode.k8s.io/v1kindRuntimeClassmetadata namegvisorhandlerrunscxxxxxxxxxxk -f 10_rtc.yaml createAnd the required Pod:
xxxxxxxxxxk -n team-purple run gvisor-test --image=nginx:1.19.2 $do > 10_pod.yaml
vim 10_pod.yamlxxxxxxxxxx# 10_pod.yamlapiVersionv1kindPodmetadata creationTimestampnull labels rungvisor-test namegvisor-test namespaceteam-purplespec nodeNamecluster1-node2 # add runtimeClassNamegvisor # add containersimagenginx1.19.2 namegvisor-test resources dnsPolicyClusterFirst restartPolicyAlwaysstatusxxxxxxxxxxk -f 10_pod.yaml createAfter creating the pod we should check if it's running and if it uses the gvisor sandbox:
xxxxxxxxxx➜ k -n team-purple get pod gvisor-testNAME READY STATUS RESTARTS AGEgvisor-test 1/1 Running 0 30s
➜ k -n team-purple exec gvisor-test -- dmesg[ 0.000000] Starting gVisor...[ 0.417740] Checking naughty and nice process list...[ 0.623721] Waiting for children...[ 0.902192] Gathering forks...[ 1.258087] Committing treasure map to memory...[ 1.653149] Generating random numbers by fair dice roll...[ 1.918386] Creating cloned children...[ 2.137450] Digging up root...[ 2.369841] Forking spaghetti code...[ 2.840216] Rewriting operating system in Javascript...[ 2.956226] Creating bureaucratic processes...[ 3.329981] Ready!Looking good. And as required we finally write the dmesg output into the file:
xxxxxxxxxxk -n team-purple exec gvisor-test > /opt/course/10/gvisor-test-dmesg -- dmesg
Use context: kubectl config use-context workload-prod
There is an existing Secret called database-access in Namespace team-green.
Read the complete Secret content directly from ETCD (using etcdctl) and store it into /opt/course/11/etcd-secret-content. Write the plain and decoded Secret's value of key "pass" into /opt/course/11/database-password.
Let's try to get the Secret value directly from ETCD, which will work since it isn't encrypted.
First, we ssh into the master node where ETCD is running in this setup and check if etcdctl is installed and list it's options:
xxxxxxxxxx➜ ssh cluster1-controlplane1
➜ root@cluster1-controlplane1:~# etcdctlNAME: etcdctl - A simple command line client for etcd.
WARNING: Environment variable ETCDCTL_API is not set; defaults to etcdctl v2. Set environment variable ETCDCTL_API=3 to use v3 API or ETCDCTL_API=2 to use v2 API.
USAGE: etcdctl [global options] command [command options] [arguments...]... --cert-file value identify HTTPS client using this SSL certificate file --key-file value identify HTTPS client using this SSL key file --ca-file value verify certificates of HTTPS-enabled servers using this CA bundle...Among others we see arguments to identify ourselves. The apiserver connects to ETCD, so we can run the following command to get the path of the necessary .crt and .key files:
xxxxxxxxxxcat /etc/kubernetes/manifests/kube-apiserver.yaml | grep etcdThe output is as follows :
xxxxxxxxxx--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt--etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt--etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key--etcd-servers=https://127.0.0.1:2379 # optional since we're on same nodeWith this information we query ETCD for the secret value:
xxxxxxxxxx➜ root@cluster1-controlplane1:~# ETCDCTL_API=3 etcdctl \--cert /etc/kubernetes/pki/apiserver-etcd-client.crt \--key /etc/kubernetes/pki/apiserver-etcd-client.key \--cacert /etc/kubernetes/pki/etcd/ca.crt get /registry/secrets/team-green/database-accessETCD in Kubernetes stores data under /registry/{type}/{namespace}/{name}. This is how we came to look for /registry/secrets/team-green/database-access. There is also an example on a page in the k8s documentation which you could save as a bookmark to access fast during the exam.
The tasks requires us to store the output on our terminal. For this we can simply copy&paste the content into a new file on our terminal:
xxxxxxxxxx# /opt/course/11/etcd-secret-content/registry/secrets/team-green/database-accessk8sv1Secretdatabase-accessteam-green"*$3e0acd78-709d-4f07-bdac-d5193d0f2aa32bB0kubectl.kubernetes.io/last-applied-configuration{"apiVersion":"v1","data":{"pass":"Y29uZmlkZW50aWFs"},"kind":"Secret","metadata":{"annotations":{},"name":"database-access","namespace":"team-green"}}zkubectl-client-side-applyUpdatevFieldsV1:{"f:data":{".":{},"f:pass":{}},"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}}},"f:type":{}}passconfidentialOpaque"
We're also required to store the plain and "decrypted" database password. For this we can copy the base64-encoded value from the ETCD output and run on our terminal:
xxxxxxxxxx➜ echo Y29uZmlkZW50aWFs | base64 -d > /opt/course/11/database-password
➜ cat /opt/course/11/database-passwordconfidential
Use context: kubectl config use-context restricted@infra-prod
You're asked to investigate a possible permission escape in Namespace restricted. The context authenticates as user restricted which has only limited permissions and shouldn't be able to read Secret values.
Try to find the password-key values of the Secrets secret1, secret2 and secret3 in Namespace restricted. Write the decoded plaintext values into files /opt/course/12/secret1, /opt/course/12/secret2 and /opt/course/12/secret3.
First we should explore the boundaries, we can try:
xxxxxxxxxx➜ k -n restricted get role,rolebinding,clusterrole,clusterrolebindingError from server (Forbidden): roles.rbac.authorization.k8s.io is forbidden: User "restricted" cannot list resource "roles" in API group "rbac.authorization.k8s.io" in the namespace "restricted"Error from server (Forbidden): rolebindings.rbac.authorization.k8s.io is forbidden: User "restricted" cannot list resource "rolebindings" in API group "rbac.authorization.k8s.io" in the namespace "restricted"Error from server (Forbidden): clusterroles.rbac.authorization.k8s.io is forbidden: User "restricted" cannot list resource "clusterroles" in API group "rbac.authorization.k8s.io" at the cluster scopeError from server (Forbidden): clusterrolebindings.rbac.authorization.k8s.io is forbidden: User "restricted" cannot list resource "clusterrolebindings" in API group "rbac.authorization.k8s.io" at the cluster scopeBut no permissions to view RBAC resources. So we try the obvious:
xxxxxxxxxx➜ k -n restricted get secretError from server (Forbidden): secrets is forbidden: User "restricted" cannot list resource "secrets" in API group "" in the namespace "restricted"
➜ k -n restricted get secret -o yamlapiVersion: v1items: []kind: Listmetadata: resourceVersion: "" selfLink: ""Error from server (Forbidden): secrets is forbidden: User "restricted" cannot list resource "secrets" in API group "" in the namespace "restricted"We're not allowed to get or list any Secrets. What can we see though?
xxxxxxxxxx➜ k -n restricted get allNAME READY STATUS RESTARTS AGEpod1-fd5d64b9c-pcx6q 1/1 Running 0 37spod2-6494f7699b-4hks5 1/1 Running 0 37spod3-748b48594-24s76 1/1 Running 0 37sError from server (Forbidden): replicationcontrollers is forbidden: User "restricted" cannot list resource "replicationcontrollers" in API group "" in the namespace "restricted"Error from server (Forbidden): services is forbidden: User "restricted" cannot list resource "services" in API group "" in the namespace "restricted"...There are some Pods, lets check these out regarding Secret access:
xxxxxxxxxxk -n restricted get pod -o yaml | grep -i secretThis output provides us with enough information to do:
xxxxxxxxxx➜ k -n restricted exec pod1-fd5d64b9c-pcx6q -- cat /etc/secret-volume/passwordyou-are
➜ echo you-are > /opt/course/12/secret1And for the second Secret:
xxxxxxxxxx➜ k -n restricted exec pod2-6494f7699b-4hks5 -- env | grep PASSPASSWORD=an-amazing
➜ echo an-amazing > /opt/course/12/secret2None of the Pods seem to mount secret3 though. Can we create or edit existing Pods to mount secret3?
xxxxxxxxxx➜ k -n restricted run test --image=nginxError from server (Forbidden): pods is forbidden: User "restricted" cannot create resource "pods" in API group "" in the namespace "restricted"
➜ k -n restricted delete pod pod1Error from server (Forbidden): pods "pod1" is forbidden: User "restricted" cannot delete resource "pods" in API group "" in the namespace "restricted"Doesn't look like it.
But the Pods seem to be able to access the Secrets, we can try to use a Pod's ServiceAccount to access the third Secret. We can actually see (like using k -n restricted get pod -o yaml | grep automountServiceAccountToken) that only Pod pod3-* has the ServiceAccount token mounted:
xxxxxxxxxx➜ k -n restricted exec -it pod3-748b48594-24s76 -- sh
/ # mount | grep serviceaccounttmpfs on /run/secrets/kubernetes.io/serviceaccount type tmpfs (ro,relatime)
/ # ls /run/secrets/kubernetes.io/serviceaccountca.crt namespace token
NOTE: You should have knowledge about ServiceAccounts and how they work with Pods like described in the docs
We can see all necessary information to contact the apiserver manually:
xxxxxxxxxx/ # curl https://kubernetes.default/api/v1/namespaces/restricted/secrets -H "Authorization: Bearer $(cat /run/secrets/kubernetes.io/serviceaccount/token)" -k... { "metadata": { "name": "secret3", "namespace": "restricted",... } ] }, "data": { "password": "cEVuRXRSYVRpT24tdEVzVGVSCg==" }, "type": "Opaque" }...Let's encode it and write it into the requested location:
xxxxxxxxxx➜ echo cEVuRXRSYVRpT24tdEVzVGVSCg== | base64 -dpEnEtRaTiOn-tEsTeR
➜ echo cEVuRXRSYVRpT24tdEVzVGVSCg== | base64 -d > /opt/course/12/secret3This will give us:
xxxxxxxxxx# /opt/course/12/secret1you-are
xxxxxxxxxx# /opt/course/12/secret2an-amazing
xxxxxxxxxx# /opt/course/12/secret3pEnEtRaTiOn-tEsTeR
We hacked all Secrets! It can be tricky to get RBAC right and secure.
NOTE: One thing to consider is that giving the permission to "list" Secrets, will also allow the user to read the Secret values like using
kubectl get secrets -o yamleven without the "get" permission set.
Use context: kubectl config use-context infra-prod
There is a metadata service available at http://192.168.100.21:32000 on which Nodes can reach sensitive data, like cloud credentials for initialisation. By default, all Pods in the cluster also have access to this endpoint. The DevSecOps team has asked you to restrict access to this metadata server.
In Namespace metadata-access:
Create a NetworkPolicy named metadata-deny which prevents egress to 192.168.100.21 for all Pods but still allows access to everything else
Create a NetworkPolicy named metadata-allow which allows Pods having label role: metadata-accessor to access endpoint 192.168.100.21
There are existing Pods in the target Namespace with which you can test your policies, but don't change their labels.
There was a famous hack at Shopify which was based on revealed information via metadata for nodes.
Check the Pods in the Namespace metadata-access and their labels:
xxxxxxxxxx➜ k -n metadata-access get pods --show-labelsNAME ... LABELSpod1-7d67b4ff9-xrcd7 ... app=pod1,pod-template-hash=7d67b4ff9pod2-7b6fc66944-2hc7n ... app=pod2,pod-template-hash=7b6fc66944pod3-7dc879bd59-hkgrr ... app=pod3,role=metadata-accessor,pod-template-hash=7dc879bd59There are three Pods in the Namespace and one of them has the label role=metadata-accessor.
Check access to the metadata server from the Pods:
xxxxxxxxxx➜ k exec -it -n metadata-access pod1-7d67b4ff9-xrcd7 -- curl http://192.168.100.21:32000metadata server
➜ k exec -it -n metadata-access pod2-7b6fc66944-2hc7n -- curl http://192.168.100.21:32000metadata server
➜ k exec -it -n metadata-access pod3-7dc879bd59-hkgrr -- curl http://192.168.100.21:32000metadata serverAll three are able to access the metadata server.
To restrict the access, we create a NetworkPolicy to deny access to the specific IP.
xxxxxxxxxxvim 13_metadata-deny.yamlxxxxxxxxxx# 13_metadata-deny.yamlapiVersionnetworking.k8s.io/v1kindNetworkPolicymetadata namemetadata-deny namespacemetadata-accessspec podSelector policyTypesEgress egresstoipBlock cidr0.0.0.0/0 except192.168.100.21/32xxxxxxxxxxk -f 13_metadata-deny.yaml apply
NOTE: You should know about general default-deny K8s NetworkPolcies.
Verify that access to the metadata server has been blocked, but other endpoints are still accessible:
xxxxxxxxxx➜ k exec -it -n metadata-access pod1-7d67b4ff9-xrcd7 -- curl http://192.168.100.21:32000curl: (28) Failed to connect to 192.168.100.21 port 32000: Operation timed outcommand terminated with exit code 28
➜ kubectl exec -it -n metadata-access pod1-7d67b4ff9-xrcd7 -- curl -I https://kubernetes.ioHTTP/2 200cache-control: public, max-age=0, must-revalidatecontent-type: text/html; charset=UTF-8date: Mon, 14 Sep 2020 15:39:39 GMTetag: "b46e429397e5f1fecf48c10a533f5cd8-ssl"strict-transport-security: max-age=31536000age: 13content-length: 22252server: Netlifyx-nf-request-id: 1d94a1d1-6bac-4a98-b065-346f661f1db1-393998290Similarly, verify for the other two Pods.
Now create another NetworkPolicy that allows access to the metadata server from Pods with label role=metadata-accessor.
xxxxxxxxxxvim 13_metadata-allow.yamlxxxxxxxxxx# 13_metadata-allow.yamlapiVersionnetworking.k8s.io/v1kindNetworkPolicymetadata namemetadata-allow namespacemetadata-accessspec podSelector matchLabels rolemetadata-accessor policyTypesEgress egresstoipBlock cidr192.168.100.21/32xxxxxxxxxxk -f 13_metadata-allow.yaml applyVerify that required Pod has access to metadata endpoint and others do not:
xxxxxxxxxx➜ k -n metadata-access exec pod3-7dc879bd59-hkgrr -- curl http://192.168.100.21:32000metadata server
➜ k -n metadata-access exec pod2-7b6fc66944-9ngzr -- curl http://192.168.100.21:32000^Ccommand terminated with exit code 130It only works for the Pod having the label. With this we implemented the required security restrictions.
If a Pod doesn't have a matching NetworkPolicy then all traffic is allowed from and to it. Once a Pod has a matching NP then the contained rules are additive. This means that for Pods having label metadata-accessor the rules will be combined to:
xxxxxxxxxx# merged policies into one for pods with label metadata-accessorspec podSelector policyTypesEgress egressto# first ruleipBlock# condition 1 cidr0.0.0.0/0 except192.168.100.21/32to# second ruleipBlock# condition 1 cidr192.168.100.21/32We can see that the merged NP contains two separate rules with one condition each. We could read it as:
xxxxxxxxxxAllow outgoing traffic if:(destination is 0.0.0.0/0 but not 192.168.100.21/32) OR (destination is 192.168.100.21/32)
Hence it allows Pods with label metadata-accessor to access everything.
Use context: kubectl config use-context workload-prod
There are Pods in Namespace team-yellow. A security investigation noticed that some processes running in these Pods are using the Syscall kill, which is forbidden by a Team Yellow internal policy.
Find the offending Pod(s) and remove these by reducing the replicas of the parent Deployment to 0.
Syscalls are used by processes running in Userspace to communicate with the Linux Kernel. There are many available syscalls: https://man7.org/linux/man-pages/man2/syscalls.2.html. It makes sense to restrict these for container processes and Docker/Containerd already restrict some by default, like the reboot Syscall. Restricting even more is possible for example using Seccomp or AppArmor.
But for this task we should simply find out which binary process executes a specific Syscall. Processes in containers are simply run on the same Linux operating system, but isolated. That's why we first check on which nodes the Pods are running:
xxxxxxxxxx➜ k -n team-yellow get pod -owideNAME ... NODE NOMINATED NODE ...collector1-7585cc58cb-n5rtd 1/1 ... cluster1-node1 <none> ...collector1-7585cc58cb-vdlp9 1/1 ... cluster1-node1 <none> ...collector2-8556679d96-z7g7c 1/1 ... cluster1-node1 <none> ...collector3-8b58fdc88-pjg24 1/1 ... cluster1-node1 <none> ...collector3-8b58fdc88-s9ltc 1/1 ... cluster1-node1 <none> ...All on cluster1-node1, hence we ssh into it and find the processes for the first Deployment collector1 .
xxxxxxxxxx➜ ssh cluster1-node1
➜ root@cluster1-node1:~# crictl pods --name collector1POD ID CREATED STATE NAME ...21aacb8f4ca8d 17 minutes ago Ready collector1-7585cc58cb-vdlp9 ...186631e40104d 17 minutes ago Ready collector1-7585cc58cb-n5rtd ...
➜ root@cluster1-node1:~# crictl ps --pod 21aacb8f4ca8dCONTAINER ID IMAGE CREATED ... POD ID9ea02422f8660 5d867958e04e1 12 minutes ago ... 21aacb8f4ca8d
➜ root@cluster1-node1:~# crictl inspect 9ea02422f8660 | grep args -A1 "args": [ "./collector1-process"Using crictl pods we first searched for the Pods of Deployment collector1, which has two replicas
We then took one pod-id to find it's containers using crictl ps
And finally we used crictl inspect to find the process name, which is collector1-process
We can find the process PIDs (two because there are two Pods):
xxxxxxxxxx➜ root@cluster1-node1:~# ps aux | grep collector1-processroot 35039 0.0 0.1 702208 1044 ? Ssl 13:37 0:00 ./collector1-processroot 35059 0.0 0.1 702208 1044 ? Ssl 13:37 0:00 ./collector1-processUsing the PIDs we can call strace to find Sycalls:
xxxxxxxxxx➜ root@cluster1-node1:~# strace -p 35039strace: Process 35039 attachedfutex(0x4d7e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0kill(666, SIGTERM) = -1 ESRCH (No such process)epoll_pwait(3, [], 128, 999, NULL, 1) = 0kill(666, SIGTERM) = -1 ESRCH (No such process)epoll_pwait(3, [], 128, 999, NULL, 1) = 0kill(666, SIGTERM) = -1 ESRCH (No such process)epoll_pwait(3, ^Cstrace: Process 35039 detached <detached ...>...First try and already a catch! We see it uses the forbidden Syscall by calling kill(666, SIGTERM).
Next let's check the Deployment collector2 processes:
xxxxxxxxxx➜ root@cluster1-node1:~# ps aux | grep collector2-processroot 35375 0.0 0.0 702216 604 ? Ssl 13:37 0:00 ./collector2-process
➜ root@cluster1-node1:~# strace -p 35375strace: Process 35375 attachedfutex(0x4d9e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0futex(0x4d9e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0futex(0x4d9e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0futex(0x4d9e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0...Looks alright. What about collector3:
xxxxxxxxxx➜ root@cluster1-node1:~# ps aux | grep collector3-processroot 35155 0.0 0.1 702472 1040 ? Ssl 13:37 0:00 ./collector3-processroot 35241 0.0 0.1 702472 1044 ? Ssl 13:37 0:00 ./collector3-process
➜ root@cluster1-node1:~# strace -p 35155strace: Process 35155 attachedfutex(0x4d9e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0futex(0x4d9e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0futex(0x4d9e68, FUTEX_WAIT_PRIVATE, 0, NULL) = 0epoll_pwait(3, [], 128, 999, NULL, 1) = 0epoll_pwait(3, [], 128, 999, NULL, 1) = 0...Also nothing about the forbidden Syscall. So we finalise the task:
xxxxxxxxxxk -n team-yellow scale deploy collector1 --replicas 0And the world is a bit safer again.
Use context: kubectl config use-context workload-prod
In Namespace team-pink there is an existing Nginx Ingress resources named secure which accepts two paths /app and /api which point to different ClusterIP Services.
From your main terminal you can connect to it using for example:
HTTP: curl -v http://secure-ingress.test:31080/app
HTTPS: curl -kv https://secure-ingress.test:31443/app
Right now it uses a default generated TLS certificate by the Nginx Ingress Controller.
You're asked to instead use the key and certificate provided at /opt/course/15/tls.key and /opt/course/15/tls.crt. As it's a self-signed certificate you need to use curl -k when connecting to it.
We can get the IP address of the Ingress and we see it's the same one to which secure-ingress.test is pointing to:
xxxxxxxxxx➜ k -n team-pink get ing secure NAME CLASS HOSTS ADDRESS PORTS AGEsecure <none> secure-ingress.test 192.168.100.12 80 7m11s
➜ ping secure-ingress.testPING cluster1-node1 (192.168.100.12) 56(84) bytes of data.64 bytes from cluster1-node1 (192.168.100.12): icmp_seq=1 ttl=64 time=0.316 msNow, let's try to access the paths /app and /api via HTTP:
xxxxxxxxxx➜ curl http://secure-ingress.test:31080/appThis is the backend APP!
➜ curl http://secure-ingress.test:31080/apiThis is the API Server!What about HTTPS?
xxxxxxxxxx➜ curl https://secure-ingress.test:31443/apicurl: (60) SSL certificate problem: unable to get local issuer certificateMore details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could notestablish a secure connection to it. To learn more about this situation andhow to fix it, please visit the web page mentioned above.
➜ curl -k https://secure-ingress.test:31443/apiThis is the API Server!HTTPS seems to be already working if we accept self-signed certificated using -k. But what kind of certificate is used by the server?
xxxxxxxxxx➜ curl -kv https://secure-ingress.test:31443/api...* Server certificate:* subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate* start date: Sep 28 12:28:35 2020 GMT* expire date: Sep 28 12:28:35 2021 GMT* issuer: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate* SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway....It seems to be "Kubernetes Ingress Controller Fake Certificate".
First, let us generate a Secret using the provided key and certificate:
xxxxxxxxxx➜ cd /opt/course/15
➜ :/opt/course/15$ lstls.crt tls.key
➜ :/opt/course/15$ k -n team-pink create secret tls tls-secret --key tls.key --cert tls.crtsecret/tls-secret createdNow, we configure the Ingress to make use of this Secret:
xxxxxxxxxx➜ k -n team-pink get ing secure -oyaml > 15_ing_bak.yaml
➜ k -n team-pink edit ing securexxxxxxxxxx# kubectl -n team-pink edit ing secureapiVersionnetworking.k8s.io/v1kindIngressmetadata annotations... generation1 namesecure namespaceteam-pink...spec tls# addhosts# addsecure-ingress.test # add secretNametls-secret # add ruleshostsecure-ingress.test http pathsbackend service namesecure-app port80 path/app pathTypeImplementationSpecificbackend service namesecure-api port80 path/api pathTypeImplementationSpecific...After adding the changes we check the Ingress resource again:
xxxxxxxxxx➜ k -n team-pink get ingNAME CLASS HOSTS ADDRESS PORTS AGEsecure <none> secure-ingress.test 192.168.100.12 80, 443 25mIt now actually lists port 443 for HTTPS. To verify:
xxxxxxxxxx➜ curl -k https://secure-ingress.test:31443/apiThis is the API Server!
➜ curl -kv https://secure-ingress.test:31443/api...* Server certificate:* subject: CN=secure-ingress.test; O=secure-ingress.test* start date: Sep 25 18:22:10 2020 GMT* expire date: Sep 20 18:22:10 2040 GMT* issuer: CN=secure-ingress.test; O=secure-ingress.test* SSL certificate verify result: self signed certificate (18), continuing anyway....We can see that the provided certificate is now being used by the Ingress for TLS termination.
Use context: kubectl config use-context workload-prod
There is a Deployment image-verify in Namespace team-blue which runs image registry.killer.sh:5000/image-verify:v1. DevSecOps has asked you to improve this image by:
Changing the base image to alpine:3.12
Not installing curl
Updating nginx to use the version constraint >=1.18.0
Running the main process as user myuser
Do not add any new lines to the Dockerfile, just edit existing ones. The file is located at /opt/course/16/image/Dockerfile.
Tag your version as v2. You can build, tag and push using:
xxxxxxxxxxcd /opt/course/16/imagepodman build -t registry.killer.sh:5000/image-verify:v2 .podman run registry.killer.sh:5000/image-verify:v2 # to test your changespodman push registry.killer.sh:5000/image-verify:v2Make the Deployment use your updated image tag v2.
We should have a look at the Docker Image at first:
xxxxxxxxxxcd /opt/course/16/image
cp Dockerfile Dockerfile.bak
vim Dockerfilexxxxxxxxxx# /opt/course/16/image/DockerfileFROM alpine:3.4RUN apk update && apk add vim curl nginx=1.10.3-r0RUN addgroup -S myuser && adduser -S myuser -G myuserCOPY ./run.sh run.shRUN ["chmod", "+x", "./run.sh"]USER rootENTRYPOINT ["/bin/sh", "./run.sh"]Very simple Dockerfile which seems to execute a script run.sh :
xxxxxxxxxx# /opt/course/16/image/run.shwhile true; do date; id; echo; sleep 1; doneSo it only outputs current date and credential information in a loop. We can see that output in the existing Deployment image-verify:
xxxxxxxxxx➜ k -n team-blue logs -f -l id=image-verifyFri Sep 25 20:59:12 UTC 2020uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)We see it's running as root.
Next we update the Dockerfile according to the requirements:
xxxxxxxxxx# /opt/course/16/image/Dockerfile
# changeFROM alpine:3.12
# changeRUN apk update && apk add vim nginx>=1.18.0
RUN addgroup -S myuser && adduser -S myuser -G myuserCOPY ./run.sh run.shRUN ["chmod", "+x", "./run.sh"]
# changeUSER myuser
ENTRYPOINT ["/bin/sh", "./run.sh"]Then we build the new image:
xxxxxxxxxx➜ :/opt/course/16/image$ podman build -t registry.killer.sh:5000/image-verify:v2 ....STEP 7/7: ENTRYPOINT ["/bin/sh", "./run.sh"]COMMIT registry.killer.sh:5000/image-verify:v2--> ceb8989101bSuccessfully tagged registry.killer.sh:5000/image-verify:v2ceb8989101bccd9f6b9c3b4c6c75f6c3561f19a5b784edd1f1a36fa0fb34a9dfWe can then test our changes by running the container locally:
xxxxxxxxxx➜ :/opt/course/16/image$ podman run registry.killer.sh:5000/image-verify:v2 Thu Sep 16 06:01:47 UTC 2021uid=101(myuser) gid=102(myuser) groups=102(myuser)
Thu Sep 16 06:01:48 UTC 2021uid=101(myuser) gid=102(myuser) groups=102(myuser)
Thu Sep 16 06:01:49 UTC 2021uid=101(myuser) gid=102(myuser) groups=102(myuser)Looking good, so we push:
xxxxxxxxxx➜ :/opt/course/16/image$ podman push registry.killer.sh:5000/image-verify:v2Getting image source signaturesCopying blob cd0853834d88 done Copying blob 5298d0709c3e skipped: already exists Copying blob e6688e911f15 done Copying blob dbc406096645 skipped: already exists Copying blob 98895ed393d9 done Copying config ceb8989101 done Writing manifest to image destinationStoring signaturesAnd we update the Deployment to use the new image:
xxxxxxxxxxk -n team-blue edit deploy image-verifyxxxxxxxxxx# kubectl -n team-blue edit deploy image-verifyapiVersionapps/v1kindDeploymentmetadata...spec... template... spec containersimageregistry.killer.sh5000/image-verifyv2 # changeAnd afterwards we can verify our changes by looking at the Pod logs:
xxxxxxxxxx➜ k -n team-blue logs -f -l id=image-verifyFri Sep 25 21:06:55 UTC 2020uid=101(myuser) gid=102(myuser) groups=102(myuser)Also to verify our changes even further:
xxxxxxxxxx➜ k -n team-blue exec image-verify-55fbcd4c9b-x2flc -- curlOCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"curl\": executable file not found in $PATH": unknowncommand terminated with exit code 126
➜ k -n team-blue exec image-verify-55fbcd4c9b-x2flc -- nginx -vnginx version: nginx/1.18.0Another task solved.
Use context: kubectl config use-context infra-prod
Audit Logging has been enabled in the cluster with an Audit Policy located at /etc/kubernetes/audit/policy.yaml on cluster2-controlplane1.
Change the configuration so that only one backup of the logs is stored.
Alter the Policy in a way that it only stores logs:
From Secret resources, level Metadata
From "system:nodes" userGroups, level RequestResponse
After you altered the Policy make sure to empty the log file so it only contains entries according to your changes, like using truncate -s 0 /etc/kubernetes/audit/logs/audit.log.
NOTE: You can use
jqto render json more readable.cat data.json | jq
First we check the apiserver configuration and change as requested:
xxxxxxxxxx➜ ssh cluster2-controlplane1
➜ root@cluster2-controlplane1:~# cp /etc/kubernetes/manifests/kube-apiserver.yaml ~/17_kube-apiserver.yaml # backup
➜ root@cluster2-controlplane1:~# vim /etc/kubernetes/manifests/kube-apiserver.yaml xxxxxxxxxx# /etc/kubernetes/manifests/kube-apiserver.yaml apiVersionv1kindPodmetadata creationTimestampnull labels componentkube-apiserver tiercontrol-plane namekube-apiserver namespacekube-systemspec containerscommandkube-apiserver--audit-policy-file=/etc/kubernetes/audit/policy.yaml--audit-log-path=/etc/kubernetes/audit/logs/audit.log--audit-log-maxsize=5--audit-log-maxbackup=1 # CHANGE--advertise-address=192.168.100.21--allow-privileged=true...
NOTE: You should know how to enable Audit Logging completely yourself as described in the docs. Feel free to try this in another cluster in this environment.
Now we look at the existing Policy:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# vim /etc/kubernetes/audit/policy.yamlxxxxxxxxxx# /etc/kubernetes/audit/policy.yamlapiVersionaudit.k8s.io/v1kindPolicyruleslevelMetadataWe can see that this simple Policy logs everything on Metadata level. So we change it to the requirements:
xxxxxxxxxx# /etc/kubernetes/audit/policy.yamlapiVersionaudit.k8s.io/v1kindPolicyrules
# log Secret resources audits, level MetadatalevelMetadata resourcesgroup"" resources"secrets"
# log node related audits, level RequestResponselevelRequestResponse userGroups"system:nodes"
# for everything else don't log anythinglevelNoneAfter saving the changes we have to restart the apiserver:
xxxxxxxxxx➜ root@cluster2-controlplane1:~# cd /etc/kubernetes/manifests/
➜ root@cluster2-controlplane1:/etc/kubernetes/manifests# mv kube-apiserver.yaml ..
➜ root@cluster2-controlplane1:/etc/kubernetes/manifests# watch crictl ps # wait for apiserver gone
➜ root@cluster2-controlplane1:/etc/kubernetes/manifests# truncate -s 0 /etc/kubernetes/audit/logs/audit.log
➜ root@cluster2-controlplane1:/etc/kubernetes/manifests# mv ../kube-apiserver.yaml .Once the apiserver is running again we can check the new logs and scroll through some entries:
xxxxxxxxxxcat audit.log | tail | jqxxxxxxxxxx{ "kind": "Event", "apiVersion": "audit.k8s.io/v1", "level": "Metadata", "auditID": "e598dc9e-fc8b-4213-aee3-0719499ab1bd", "stage": "RequestReceived", "requestURI": "...", "verb": "watch", "user": { "username": "system:serviceaccount:gatekeeper-system:gatekeeper-admin", "uid": "79870838-75a8-479b-ad42-4b7b75bd17a3", "groups": [ "system:serviceaccounts", "system:serviceaccounts:gatekeeper-system", "system:authenticated" ] }, "sourceIPs": [ "192.168.102.21" ], "userAgent": "manager/v0.0.0 (linux/amd64) kubernetes/$Format", "objectRef": { "resource": "secrets", "apiVersion": "v1" }, "requestReceivedTimestamp": "2020-09-27T20:01:36.238911Z", "stageTimestamp": "2020-09-27T20:01:36.238911Z", "annotations": { "authentication.k8s.io/legacy-token": "..." }}Above we logged a watch action by OPA Gatekeeper for Secrets, level Metadata.
xxxxxxxxxx{ "kind": "Event", "apiVersion": "audit.k8s.io/v1", "level": "RequestResponse", "auditID": "c90e53ed-b0cf-4cc4-889a-f1204dd39267", "stage": "ResponseComplete", "requestURI": "...", "verb": "list", "user": { "username": "system:node:cluster2-controlplane1", "groups": [ "system:nodes", "system:authenticated" ] }, "sourceIPs": [ "192.168.100.21" ], "userAgent": "kubelet/v1.19.1 (linux/amd64) kubernetes/206bcad", "objectRef": { "resource": "configmaps", "namespace": "kube-system", "name": "kube-proxy", "apiVersion": "v1" }, "responseStatus": { "metadata": {}, "code": 200 }, "responseObject": { "kind": "ConfigMapList", "apiVersion": "v1", "metadata": { "selfLink": "/api/v1/namespaces/kube-system/configmaps", "resourceVersion": "83409" }, "items": [ { "metadata": { "name": "kube-proxy", "namespace": "kube-system", "selfLink": "/api/v1/namespaces/kube-system/configmaps/kube-proxy", "uid": "0f1c3950-430a-4543-83e4-3f9c87a478b8", "resourceVersion": "232", "creationTimestamp": "2020-09-26T20:59:50Z", "labels": { "app": "kube-proxy" }, "annotations": { "kubeadm.kubernetes.io/component-config.hash": "..." }, "managedFields": [ { } ] }, } ] }, "requestReceivedTimestamp": "2020-09-27T20:01:36.223781Z", "stageTimestamp": "2020-09-27T20:01:36.225470Z", "annotations": { "authorization.k8s.io/decision": "allow", "authorization.k8s.io/reason": "" }}And in the one above we logged a list action by system:nodes for a ConfigMaps, level RequestResponse.
Because all JSON entries are written in a single line in the file we could also run some simple verifications on our Policy:
xxxxxxxxxx# shows Secret entriescat audit.log | grep '"resource":"secrets"' | wc -l
# confirms Secret entries are only of level Metadatacat audit.log | grep '"resource":"secrets"' | grep -v '"level":"Metadata"' | wc -l
# shows RequestResponse level entriescat audit.log | grep -v '"level":"RequestResponse"' | wc -l
# shows RequestResponse level entries are only for system:nodescat audit.log | grep '"level":"RequestResponse"' | grep -v "system:nodes" | wc -lLooks like our job is done.
Use context: kubectl config use-context infra-prod
Namespace security contains five Secrets of type Opaque which can be considered highly confidential. The latest Incident-Prevention-Investigation revealed that ServiceAccount p.auster had too broad access to the cluster for some time. This SA should've never had access to any Secrets in that Namespace.
Find out which Secrets in Namespace security this SA did access by looking at the Audit Logs under /opt/course/18/audit.log.
Change the password to any new string of only those Secrets that were accessed by this SA.
NOTE: You can use
jqto render json more readable.cat data.json | jq
First we look at the Secrets this is about:
xxxxxxxxxx➜ k -n security get secret | grep Opaquekubeadmin-token Opaque 1 37mmysql-admin Opaque 1 37mpostgres001 Opaque 1 37mpostgres002 Opaque 1 37mvault-token Opaque 1 37mNext we investigate the Audit Log file:
xxxxxxxxxx➜ cd /opt/course/18
➜ :/opt/course/18$ ls -lhtotal 7.1M-rw-r--r-- 1 k8s k8s 7.5M Sep 24 21:31 audit.log
➜ :/opt/course/18$ cat audit.log | wc -l4451Audit Logs can be huge and it's common to limit the amount by creating an Audit Policy and to transfer the data in systems like Elasticsearch. In this case we have a simple JSON export, but it already contains 4451 lines.
We should try to filter the file down to relevant information:
xxxxxxxxxx➜ :/opt/course/18$ cat audit.log | grep "p.auster" | wc -l28Not too bad, only 28 logs for ServiceAccount p.auster.
xxxxxxxxxx➜ :/opt/course/18$ cat audit.log | grep "p.auster" | grep Secret | wc -l2And only 2 logs related to Secrets...
xxxxxxxxxx➜ :/opt/course/18$ cat audit.log | grep "p.auster" | grep Secret | grep list | wc -l0
➜ :/opt/course/18$ cat audit.log | grep "p.auster" | grep Secret | grep get | wc -l2No list actions, which is good, but 2 get actions, so we check these out:
xxxxxxxxxxcat audit.log | grep "p.auster" | grep Secret | grep get | jqxxxxxxxxxx{ "kind": "Event", "apiVersion": "audit.k8s.io/v1", "level": "RequestResponse", "auditID": "74fd9e03-abea-4df1-b3d0-9cfeff9ad97a", "stage": "ResponseComplete", "requestURI": "/api/v1/namespaces/security/secrets/vault-token", "verb": "get", "user": { "username": "system:serviceaccount:security:p.auster", "uid": "29ecb107-c0e8-4f2d-816a-b16f4391999c", "groups": [ "system:serviceaccounts", "system:serviceaccounts:security", "system:authenticated" ] }, "userAgent": "curl/7.64.0", "objectRef": { "resource": "secrets", "namespace": "security", "name": "vault-token", "apiVersion": "v1" }, }{ "kind": "Event", "apiVersion": "audit.k8s.io/v1", "level": "RequestResponse", "auditID": "aed6caf9-5af0-4872-8f09-ad55974bb5e0", "stage": "ResponseComplete", "requestURI": "/api/v1/namespaces/security/secrets/mysql-admin", "verb": "get", "user": { "username": "system:serviceaccount:security:p.auster", "uid": "29ecb107-c0e8-4f2d-816a-b16f4391999c", "groups": [ "system:serviceaccounts", "system:serviceaccounts:security", "system:authenticated" ] }, "userAgent": "curl/7.64.0", "objectRef": { "resource": "secrets", "namespace": "security", "name": "mysql-admin", "apiVersion": "v1" },}There we see that Secrets vault-token and mysql-admin were accessed by p.auster. Hence we change the passwords for those.
xxxxxxxxxx➜ echo new-vault-pass | base64bmV3LXZhdWx0LXBhc3MK
➜ k -n security edit secret vault-token
➜ echo new-mysql-pass | base64bmV3LW15c3FsLXBhc3MK
➜ k -n security edit secret mysql-adminAudit Logs ftw.
By running cat audit.log | grep "p.auster" | grep Secret | grep password we can see that passwords are stored in the Audit Logs, because they store the complete content of Secrets. It's never a good idea to reveal passwords in logs. In this case it would probably be sufficient to only store Metadata level information of Secrets which can be controlled via a Audit Policy.
Use context: kubectl config use-context workload-prod
The Deployment immutable-deployment in Namespace team-purple should run immutable, it's created from file /opt/course/19/immutable-deployment.yaml. Even after a successful break-in, it shouldn't be possible for an attacker to modify the filesystem of the running container.
Modify the Deployment in a way that no processes inside the container can modify the local filesystem, only /tmp directory should be writeable. Don't modify the Docker image.
Save the updated YAML under /opt/course/19/immutable-deployment-new.yaml and update the running Deployment.
Processes in containers can write to the local filesystem by default. This increases the attack surface when a non-malicious process gets hijacked. Preventing applications to write to disk or only allowing to certain directories can mitigate the risk. If there is for example a bug in Nginx which allows an attacker to override any file inside the container, then this only works if the Nginx process itself can write to the filesystem in the first place.
Making the root filesystem readonly can be done in the Docker image itself or in a Pod declaration.
Let us first check the Deployment immutable-deployment in Namespace team-purple:
xxxxxxxxxx➜ k -n team-purple edit deploy -o yamlxxxxxxxxxx# kubectl -n team-purple edit deploy -o yamlapiVersionapps/v1kindDeploymentmetadata namespaceteam-purple nameimmutable-deployment labels appimmutable-deployment ...spec replicas1 selector matchLabels appimmutable-deployment template metadata labels appimmutable-deployment spec containersimagebusybox1.32.0 command'sh' '-c' 'tail -f /dev/null' imagePullPolicyIfNotPresent namebusybox restartPolicyAlways...The container has write access to the Root File System, as there are no restrictions defined for the Pods or containers by an existing SecurityContext. And based on the task we're not allowed to alter the Docker image.
So we modify the YAML manifest to include the required changes:
xxxxxxxxxxcp /opt/course/19/immutable-deployment.yaml /opt/course/19/immutable-deployment-new.yaml
vim /opt/course/19/immutable-deployment-new.yamlxxxxxxxxxx# /opt/course/19/immutable-deployment-new.yamlapiVersionapps/v1kindDeploymentmetadata namespaceteam-purple nameimmutable-deployment labels appimmutable-deploymentspec replicas1 selector matchLabels appimmutable-deployment template metadata labels appimmutable-deployment spec containersimagebusybox1.32.0 command'sh' '-c' 'tail -f /dev/null' imagePullPolicyIfNotPresent namebusybox securityContext# add readOnlyRootFilesystemtrue # add volumeMounts# addmountPath/tmp # add nametemp-vol # add volumes# addnametemp-vol # add emptyDir # add restartPolicyAlwaysSecurityContexts can be set on Pod or container level, here the latter was asked. Enforcing readOnlyRootFilesystem: true will render the root filesystem readonly. We can then allow some directories to be writable by using an emptyDir volume.
Once the changes are made, let us update the Deployment:
xxxxxxxxxx➜ k delete -f /opt/course/19/immutable-deployment-new.yamldeployment.apps "immutable-deployment" deleted
➜ k create -f /opt/course/19/immutable-deployment-new.yamldeployment.apps/immutable-deployment createdWe can verify if the required changes are propagated:
xxxxxxxxxx➜ k -n team-purple exec immutable-deployment-5b7ff8d464-j2nrj -- touch /abc.txttouch: /abc.txt: Read-only file systemcommand terminated with exit code 1
➜ k -n team-purple exec immutable-deployment-5b7ff8d464-j2nrj -- touch /var/abc.txttouch: /var/abc.txt: Read-only file systemcommand terminated with exit code 1
➜ k -n team-purple exec immutable-deployment-5b7ff8d464-j2nrj -- touch /etc/abc.txttouch: /etc/abc.txt: Read-only file systemcommand terminated with exit code 1
➜ k -n team-purple exec immutable-deployment-5b7ff8d464-j2nrj -- touch /tmp/abc.txt
➜ k -n team-purple exec immutable-deployment-5b7ff8d464-j2nrj -- ls /tmpabc.txtThe Deployment has been updated so that the container's file system is read-only, and the updated YAML has been placed under the required location. Sweet!
Use context: kubectl config use-context workload-stage
The cluster is running Kubernetes 1.29.5, update it to 1.30.1.
Use apt package manager and kubeadm for this.
Use ssh cluster3-controlplane1 and ssh cluster3-node1 to connect to the instances.
Let's have a look at the current versions:
xxxxxxxxxx➜ k get nodeNAME STATUS ROLES AGE VERSIONcluster3-controlplane1 Ready control-plane 173m v1.29.5cluster3-node1 Ready <none> 167m v1.29.5
First we should update the control plane components running on the master node, so we drain it:
xxxxxxxxxx➜ k drain cluster3-controlplane1 --ignore-daemonsetsnode/cluster3-controlplane1 cordonedWarning: ignoring DaemonSet-managed Pods: kube-system/kube-proxy-nct5v, kube-system/weave-net-qfh9lnode/cluster3-controlplane1 drainedNext we ssh into it and check versions:
xxxxxxxxxx➜ ssh cluster3-controlplane1
➜ root@cluster3-controlplane1:~# kubelet --versionKubernetes v1.29.5
➜ root@cluster3-controlplane1:~# kubeadm versionkubeadm version: &version.Info{Major:"1", Minor:"30", GitVersion:"v1.30.1", GitCommit:"6911225c3f747e1cd9d109c305436d08b668f086", GitTreeState:"clean", BuildDate:"2024-05-14T10:49:05Z", GoVersion:"go1.22.2", Compiler:"gc", Platform:"linux/amd64"}We see above that kubeadm is already installed in the required version. Else we would need to install it:
xxxxxxxxxx# not necessary because here kubeadm is already installed in correct versionapt-mark unhold kubeadmapt-mark hold kubectl kubeletapt install kubeadm=1.30.1-1.1apt-mark hold kubeadmCheck what kubeadm has available as an upgrade plan:
xxxxxxxxxx➜ root@cluster3-controlplane1:~# kubeadm upgrade plan[preflight] Running pre-flight checks.[upgrade/config] Reading configuration from the cluster...[upgrade/config] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'[upgrade] Running cluster health checks[upgrade] Fetching available versions to upgrade to[upgrade/versions] Cluster version: 1.29.5[upgrade/versions] kubeadm version: v1.30.1[upgrade/versions] Target version: v1.30.1[upgrade/versions] Latest version in the v1.29 series: v1.29.5
Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':COMPONENT NODE CURRENT TARGETkubelet cluster3-controlplane1 v1.29.5 v1.30.1kubelet cluster3-node1 v1.29.5 v1.30.1
Upgrade to the latest stable version:
COMPONENT NODE CURRENT TARGETkube-apiserver cluster3-controlplane1 v1.29.5 v1.30.1kube-controller-manager cluster3-controlplane1 v1.29.5 v1.30.1kube-scheduler cluster3-controlplane1 v1.29.5 v1.30.1kube-proxy 1.29.5 v1.30.1CoreDNS v1.11.1 v1.11.1etcd cluster3-controlplane1 3.5.12-0 3.5.12-0
You can now apply the upgrade by executing the following command:
kubeadm upgrade apply v1.30.1
_____________________________________________________________________
The table below shows the current state of component configs as understood by this version of kubeadm.Configs that have a "yes" mark in the "MANUAL UPGRADE REQUIRED" column require manual config upgrade orresetting to kubeadm defaults before a successful upgrade can be performed. The version to manuallyupgrade to is denoted in the "PREFERRED VERSION" column.
API GROUP CURRENT VERSION PREFERRED VERSION MANUAL UPGRADE REQUIREDkubeproxy.config.k8s.io v1alpha1 v1alpha1 nokubelet.config.k8s.io v1beta1 v1beta1 no_____________________________________________________________________And we apply to the required version:
xxxxxxxxxx➜ root@cluster3-controlplane1:~# kubeadm upgrade apply v1.30.1[preflight] Running pre-flight checks.[upgrade/config] Reading configuration from the cluster...[upgrade/config] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'[upgrade] Running cluster health checks[upgrade/version] You have chosen to change the cluster version to "v1.30.1"[upgrade/versions] Cluster version: v1.29.5[upgrade/versions] kubeadm version: v1.30.1[upgrade] Are you sure you want to proceed? [y/N]: y[upgrade/prepull] Pulling images required for setting up a Kubernetes cluster...[upgrade/staticpods] Waiting for the kubelet to restart the component[upgrade/staticpods] This can take up to 5m0s[apiclient] Found 1 Pods for label selector component=kube-scheduler[upgrade/staticpods] Component "kube-scheduler" upgraded successfully![upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace[kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config1488188576/config.yaml[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials[bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token[bootstrap-token] Configured RBAC rules to allow certificate rotation for all node client certificates in the cluster[addons] Applied essential addon: CoreDNS[addons] Applied essential addon: kube-proxy
[upgrade/successful] SUCCESS! Your cluster was upgraded to "v1.30.1". Enjoy!
[upgrade/kubelet] Now that your control plane is upgraded, please proceed with upgrading your kubelets if you haven't already done so.Next we can check if our required version was installed correctly:
xxxxxxxxxx➜ root@cluster3-controlplane1:~# kubeadm upgrade plan[preflight] Running pre-flight checks.[upgrade/config] Reading configuration from the cluster...[upgrade/config] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'[upgrade] Running cluster health checks[upgrade] Fetching available versions to upgrade to[upgrade/versions] Cluster version: 1.30.1[upgrade/versions] kubeadm version: v1.30.1[upgrade/versions] Target version: v1.30.1[upgrade/versions] Latest version in the v1.30 series: v1.30.1
Now we have to upgrade kubelet and kubectl:
xxxxxxxxxx➜ root@cluster3-controlplane1:~# apt updateHit:1 http://ppa.launchpad.net/rmescandon/yq/ubuntu focal InReleaseHit:2 http://us.archive.ubuntu.com/ubuntu bionic InRelease Hit:3 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.30/deb InRelease Hit:4 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.29/deb InReleaseReading package lists... DoneBuilding dependency tree Reading state information... Done2 packages can be upgraded. Run 'apt list --upgradable' to see them.
➜ root@cluster3-controlplane1:~# apt install kubelet=1.30.1-1.1 kubectl=1.30.1-1.1Reading package lists... DoneBuilding dependency tree Reading state information... DoneThe following packages will be upgraded: kubectl kubelet2 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.Need to get 28.9 MB of archives.After this operation, 10.2 MB disk space will be freed.Get:1 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.30/deb kubectl 1.30.1-1.1 [10.8 MB]Get:2 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.30/deb kubelet 1.30.1-1.1 [18.1 MB]Fetched 28.9 MB in 1s (36.8 MB/s)(Reading database ... 112589 files and directories currently installed.)Preparing to unpack .../kubectl_1.30.1-1.1_amd64.deb ...Unpacking kubectl (1.30.1-1.1) over (1.29.5-1.1) ...Preparing to unpack .../kubelet_1.30.1-1.1_amd64.deb ...Unpacking kubelet (1.30.1-1.1) over (1.29.5-1.1) ...Setting up kubectl (1.30.1-1.1) ...Setting up kubelet (1.30.1-1.1) ...
➜ root@cluster3-controlplane1:~# apt-mark hold kubelet kubectlkubelet set on hold.kubectl set on hold.
➜ root@cluster3-controlplane1:~# service kubelet restart
➜ root@cluster3-controlplane1:~# service kubelet status● kubelet.service - kubelet: The Kubernetes Node Agent Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset: enabled) Drop-In: /usr/lib/systemd/system/kubelet.service.d └─10-kubeadm.conf Active: active (running) since Sun 2024-05-26 15:59:10 UTC; 3s ago Docs: https://kubernetes.io/docs/ Main PID: 34143 (kubelet) Tasks: 10 (limit: 1066) Memory: 92.0M CGroup: /system.slice/kubelet.service └─34143 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/k>...
➜ root@cluster3-controlplane1:~# kubectl get nodeNAME STATUS ROLES AGE VERSIONcluster3-controlplane1 Ready,SchedulingDisabled control-plane 3h6m v1.30.1cluster3-node1 Ready <none> 179m v1.29.5Done, only uncordon missing:
xxxxxxxxxx➜ k uncordon cluster3-controlplane1node/cluster3-controlplane1 uncordoned
xxxxxxxxxx➜ k get nodeNAME STATUS ROLES AGE VERSIONcluster3-controlplane1 Ready control-plane 3h6m v1.30.0cluster3-node1 Ready <none> 3h v1.29.5Our data plane consist of one single worker node, so let's update it. First thing is we should drain it:
xxxxxxxxxx➜ k drain cluster3-node1 --ignore-daemonsetsnode/cluster3-node1 cordonedWarning: ignoring DaemonSet-managed Pods: kube-system/kube-proxy-zlnnj, kube-system/weave-net-q82dzevicting pod team-blue/pto-webform-557d7bc56b-w68zqevicting pod kube-system/coredns-76f75df574-z5gsqevicting pod kube-system/coredns-76f75df574-zzknhevicting pod team-blue/pto-webform-557d7bc56b-rg8rtevicting pod default/classification-bot-67644b99fd-jbq46pod/pto-webform-557d7bc56b-w68zq evictedpod/pto-webform-557d7bc56b-rg8rt evictedpod/classification-bot-67644b99fd-jbq46 evictedpod/coredns-76f75df574-zzknh evictedpod/coredns-76f75df574-z5gsq evictednode/cluster3-node1 drainedNext we ssh into it and upgrade kubeadm to the wanted version, or check if already done:
xxxxxxxxxx➜ ssh cluster3-node1
➜ root@cluster3-node1:~# apt updateHit:1 http://ppa.launchpad.net/rmescandon/yq/ubuntu focal InReleaseHit:3 http://us.archive.ubuntu.com/ubuntu bionic InRelease Hit:2 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.30/deb InReleaseHit:4 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.29/deb InReleaseReading package lists... DoneBuilding dependency tree Reading state information... Done2 packages can be upgraded. Run 'apt list --upgradable' to see them.
➜ root@cluster3-node1:~# apt-mark unhold kubeadmkubeadm was already not hold.
➜ root@cluster3-node1:~# apt-mark hold kubectl kubeletkubectl set on hold.kubelet set on hold.
➜ root@cluster3-node1:~# apt install kubeadm=1.30.1-1.1Reading package lists... DoneBuilding dependency tree Reading state information... Donekubeadm is already the newest version (1.30.1-1.1).0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded.
➜ root@cluster3-node1:~# apt-mark hold kubeadmkubeadm set on hold.
➜ root@cluster3-node1:~# kubeadm upgrade node[upgrade] Reading configuration from the cluster...[upgrade] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'[preflight] Running pre-flight checks[preflight] Skipping prepull. Not a control plane node.[upgrade] Skipping phase. Not a control plane node.[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config2503851970/config.yaml[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"[upgrade] The configuration for this node was successfully updated![upgrade] Now you should go ahead and upgrade the kubelet package using your package manager.Now we follow that kubeadm told us in the last line and upgrade kubelet (and kubectl):
xxxxxxxxxx➜ root@cluster3-node1:~# apt-mark unhold kubectl kubeletCanceled hold on kubectl.Canceled hold on kubelet.
➜ root@cluster3-node1:~# apt install kubelet=1.30.1-1.1 kubectl=1.30.1-1.1Reading package lists... DoneBuilding dependency tree Reading state information... DoneThe following packages will be upgraded: kubectl kubelet2 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.Need to get 28.9 MB of archives.After this operation, 10.2 MB disk space will be freed.Get:1 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.30/deb kubectl 1.30.1-1.1 [10.8 MB]Get:2 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.30/deb kubelet 1.30.1-1.1 [18.1 MB]Fetched 28.9 MB in 1s (43.0 MB/s)(Reading database ... 112589 files and directories currently installed.)Preparing to unpack .../kubectl_1.30.1-1.1_amd64.deb ...Unpacking kubectl (1.30.1-1.1) over (1.29.5-1.1) ...Preparing to unpack .../kubelet_1.30.1-1.1_amd64.deb ...Unpacking kubelet (1.30.1-1.1) over (1.29.5-1.1) ...Setting up kubectl (1.30.1-1.1) ...Setting up kubelet (1.30.1-1.1) ...
➜ root@cluster3-node1:~# service kubelet restart
➜ root@cluster3-node1:~# service kubelet status● kubelet.service - kubelet: The Kubernetes Node Agent Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset: enabled) Drop-In: /usr/lib/systemd/system/kubelet.service.d └─10-kubeadm.conf Active: active (running) since Sun 2024-05-26 16:01:36 UTC; 7s ago Docs: https://kubernetes.io/docs/ Main PID: 34514 (kubelet) Tasks: 9 (limit: 1066) Memory: 22.9M CGroup: /system.slice/kubelet.service └─34514 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/k>...Looking good, what does the node status say?
xxxxxxxxxx➜ k get nodeNAME STATUS ROLES AGE VERSIONcluster3-controlplane1 Ready control-plane 3h10m v1.30.1cluster3-node1 Ready,SchedulingDisabled <none> 3h4m v1.30.1Beautiful, let's make it schedulable again:
xxxxxxxxxx➜ k uncordon cluster3-node1node/cluster3-node1 uncordoned
➜ k get nodeNAME STATUS ROLES AGE VERSIONcluster3-controlplane1 Ready control-plane 3h10m v1.30.1cluster3-node1 Ready <none> 3h4m v1.30.1We're up to date.
(can be solved in any kubectl context)
The Vulnerability Scanner trivy is installed on your main terminal. Use it to scan the following images for known CVEs:
nginx:1.16.1-alpine
k8s.gcr.io/kube-apiserver:v1.18.0
k8s.gcr.io/kube-controller-manager:v1.18.0
docker.io/weaveworks/weave-kube:2.7.0
Write all images that don't contain the vulnerabilities CVE-2020-10878 or CVE-2020-1967 into /opt/course/21/good-images.
The tool trivy is very simple to use, it compares images against public databases.
xxxxxxxxxx➜ trivy nginx:1.16.1-alpine2020-10-09T20:59:39.198Z INFO Need to update DB2020-10-09T20:59:39.198Z INFO Downloading DB...18.81 MiB / 18.81 MiB [-------------------------------------2020-10-09T20:59:45.499Z INFO Detecting Alpine vulnerabilities...
nginx:1.16.1-alpine (alpine 3.10.4)===================================Total: 7 (UNKNOWN: 0, LOW: 0, MEDIUM: 7, HIGH: 0, CRITICAL: 0)
+---------------+------------------+----------+-------------------| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION+---------------+------------------+----------+-------------------| libcrypto1.1 | CVE-2020-1967 | MEDIUM | 1.1.1d-r2 ... To solve the task we can run:
xxxxxxxxxx➜ trivy nginx:1.16.1-alpine | grep -E 'CVE-2020-10878|CVE-2020-1967'| libcrypto1.1 | CVE-2020-1967 | MEDIUM | libssl1.1 | CVE-2020-1967 |
➜ trivy k8s.gcr.io/kube-apiserver:v1.18.0 | grep -E 'CVE-2020-10878|CVE-2020-1967'| perl-base | CVE-2020-10878 | HIGH
➜ trivy k8s.gcr.io/kube-controller-manager:v1.18.0 | grep -E 'CVE-2020-10878|CVE-2020-1967'| perl-base | CVE-2020-10878 | HIGH
➜ trivy docker.io/weaveworks/weave-kube:2.7.0 | grep -E 'CVE-2020-10878|CVE-2020-1967'➜The only image without the any of the two CVEs is docker.io/weaveworks/weave-kube:2.7.0, hence our answer will be:
xxxxxxxxxx# /opt/course/21/good-imagesdocker.io/weaveworks/weave-kube:2.7.0
(can be solved in any kubectl context)
The Release Engineering Team has shared some YAML manifests and Dockerfiles with you to review. The files are located under /opt/course/22/files.
As a container security expert, you are asked to perform a manual static analysis and find out possible security issues with respect to unwanted credential exposure. Running processes as root is of no concern in this task.
Write the filenames which have issues into /opt/course/22/security-issues.
NOTE: In the Dockerfile and YAML manifests, assume that the referred files, folders, secrets and volume mounts are present. Disregard syntax or logic errors.
We check location /opt/course/22/files and list the files.
xxxxxxxxxx➜ ls -la /opt/course/22/filestotal 48drwxr-xr-x 2 k8s k8s 4096 Sep 16 19:08 .drwxr-xr-x 3 k8s k8s 4096 Sep 16 19:08 ..-rw-r--r-- 1 k8s k8s 692 Sep 16 19:08 Dockerfile-go-rw-r--r-- 1 k8s k8s 897 Sep 16 19:08 Dockerfile-mysql-rw-r--r-- 1 k8s k8s 743 Sep 16 19:08 Dockerfile-py-rw-r--r-- 1 k8s k8s 341 Sep 16 19:08 deployment-nginx.yaml-rw-r--r-- 1 k8s k8s 705 Sep 16 19:08 deployment-redis.yaml-rw-r--r-- 1 k8s k8s 392 Sep 16 19:08 pod-nginx.yaml-rw-r--r-- 1 k8s k8s 228 Sep 16 19:08 pv-manual.yaml-rw-r--r-- 1 k8s k8s 188 Sep 16 19:08 pvc-manual.yaml-rw-r--r-- 1 k8s k8s 211 Sep 16 19:08 sc-local.yaml-rw-r--r-- 1 k8s k8s 902 Sep 16 19:08 statefulset-nginx.yamlWe have 3 Dockerfiles and 7 Kubernetes Resource YAML manifests. Next we should go over each to find security issues with the way credentials have been used.
NOTE: You should be comfortable with Docker Best Practices and the Kubernetes Configuration Best Practices.
While navigating through the files we might notice:
File Dockerfile-mysql might look innocent on first look. It copies a file secret-token over, uses it and deletes it afterwards. But because of the way Docker works, every RUN, COPY and ADD command creates a new layer and every layer is persistet in the image.
This means even if the file secret-token get's deleted in layer Z, it's still included with the image in layer X and Y. In this case it would be better to use for example variables passed to Docker.
xxxxxxxxxx# /opt/course/22/files/Dockerfile-mysqlFROM ubuntu
# Add MySQL configurationCOPY my.cnf /etc/mysql/conf.d/my.cnfCOPY mysqld_charset.cnf /etc/mysql/conf.d/mysqld_charset.cnf
RUN apt-get update && \ apt-get -yq install mysql-server-5.6 &&
# Add MySQL scriptsCOPY import_sql.sh /import_sql.shCOPY run.sh /run.sh
# Configure credentialsCOPY secret-token . # LAYER XRUN /etc/register.sh ./secret-token # LAYER YRUN rm ./secret-token # delete secret token again # LATER Z
EXPOSE 3306CMD "/run.sh"So we do:
xxxxxxxxxxecho Dockerfile-mysql >> /opt/course/22/security-issues
The file deployment-redis.yaml is fetching credentials from a Secret named mysecret and writes these into environment variables. So far so good, but in the command of the container it's echoing these which can be directly read by any user having access to the logs.
xxxxxxxxxx# /opt/course/22/files/deployment-redis.yamlapiVersionapps/v1kindDeploymentmetadata namenginx-deployment labels appnginxspec replicas3 selector matchLabels appnginx template metadata labels appnginx spec containersnamemycontainer imageredis command"/bin/sh" args"-c""echo $SECRET_USERNAME && echo $SECRET_PASSWORD && docker-entrypoint.sh" # NOT GOOD envnameSECRET_USERNAME valueFrom secretKeyRef namemysecret keyusernamenameSECRET_PASSWORD valueFrom secretKeyRef namemysecret keypasswordCredentials in logs is never a good idea, hence we do:
xxxxxxxxxxecho deployment-redis.yaml >> /opt/course/22/security-issues
In file statefulset-nginx.yaml, the password is directly exposed in the environment variable definition of the container.
xxxxxxxxxx# /opt/course/22/files/statefulset-nginx.yaml...apiVersionapps/v1kindStatefulSetmetadata namewebspec serviceName"nginx" replicas2 selector matchLabels appnginx template metadata labels appnginx spec containersnamenginx imagek8s.gcr.io/nginx-slim0.8 envnameUsername valueAdministratornamePassword valueMyDiReCtP@sSw0rd # NOT GOOD portscontainerPort80 nameweb..This should better be injected via a Secret. So we do:
xxxxxxxxxxecho statefulset-nginx.yaml >> /opt/course/22/security-issuesxxxxxxxxxx➜ cat /opt/course/22/security-issuesDockerfile-mysqldeployment-redis.yamlstatefulset-nginx.yaml
This is a preview of the full CKS Simulator course content.
The full course contains 22 questions and scenarios which cover all the CKS areas. The course also provides a browser terminal which is a very close replica of the original one. This is great to get used and comfortable before the real exam. After the test session (120 minutes), or if you stop it early, you'll get access to all questions and their detailed solutions. You'll have 36 hours cluster access in total which means even after the session, once you have the solutions, you can still play around.
The following preview will give you an idea of what the full course will provide. These preview questions are not part of the 22 in the full course but in addition to it. But the preview questions are part of the same CKS simulation environment which we setup for you, so with access to the full course you can solve these too.
The answers provided here assume that you did run the initial terminal setup suggestions as provided in the tips section, but especially:
xalias k=kubectl
export do="-o yaml --dry-run=client"
These questions can be solved in the test environment provided through the CKS Simulator
Use context: kubectl config use-context infra-prod
You have admin access to cluster2. There is also context gianna@infra-prod which authenticates as user gianna with the same cluster.
There are existing cluster-level RBAC resources in place to, among other things, ensure that user gianna can never read Secret contents cluster-wide. Confirm this is correct or restrict the existing RBAC resources to ensure this.
I addition, create more RBAC resources to allow user gianna to create Pods and Deployments in Namespaces security, restricted and internal. It's likely the user will receive these exact permissions as well for other Namespaces in the future.
We should probably first have a look at the existing RBAC resources for user gianna. We don't know the resource names but we know these are cluster-level so we can search for a ClusterRoleBinding:
xxxxxxxxxxk get clusterrolebinding -oyaml | grep gianna -A10 -B20From this we see the binding is also called gianna:
xxxxxxxxxxk edit clusterrolebinding giannaxxxxxxxxxx# kubectl edit clusterrolebinding giannaapiVersionrbac.authorization.k8s.io/v1kindClusterRoleBindingmetadata creationTimestamp"2020-09-26T13:57:58Z" namegianna resourceVersion"3049" selfLink/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/gianna uid72b64a3b-5958-4cf8-8078-e5be2c55b25droleRef apiGrouprbac.authorization.k8s.io kindClusterRole namegiannasubjectsapiGrouprbac.authorization.k8s.io kindUser namegiannaIt links user gianna to same named ClusterRole:
xxxxxxxxxxk edit clusterrole giannaxxxxxxxxxxapiVersionrbac.authorization.k8s.io/v1kindClusterRolemetadata creationTimestamp"2020-09-26T13:57:55Z" namegianna resourceVersion"3038" selfLink/apis/rbac.authorization.k8s.io/v1/clusterroles/gianna uidb713c1cf-87e5-4313-808e-1a51f392adc0rulesapiGroups"" resourcessecretsconfigmapspodsnamespaces verbslistAccording to the task the user should never be able to read Secrets content. They verb list might indicate on first look that this is correct. We can also check using K8s User Impersonation:
xxxxxxxxxx➜ k auth can-i list secrets --as giannayes
➜ k auth can-i get secrets --as giannanoBut let's have a closer look:
xxxxxxxxxx➜ k config use-context gianna@infra-prodSwitched to context "gianna@infra-prod".
➜ k -n security get secretsNAME TYPE DATA AGEdefault-token-gn455 kubernetes.io/service-account-token 3 20mkubeadmin-token Opaque 1 20mmysql-admin Opaque 1 20mpostgres001 Opaque 1 20mpostgres002 Opaque 1 20mvault-token Opaque 1 20m
➜ k -n security get secret kubeadmin-tokenError from server (Forbidden): secrets "kubeadmin-token" is forbidden: User "gianna" cannot get resource "secrets" in API group "" in the namespace "security"Still all expected, but being able to list resources also allows to specify the format:
xxxxxxxxxx➜ k -n security get secrets -oyaml | grep password password: ekhHYW5lQUVTaVVxCg== {"apiVersion":"v1","data":{"password":"ekhHYW5lQUVTaVVxCg=="},"kind":"Secret","metadata":{"annotations":{},"name":"kubeadmin-token","namespace":"security"},"type":"Opaque"} f:password: {} password: bWdFVlBSdEpEWHBFCg==...The user gianna is actually able to read Secret content. To prevent this we should remove the ability to list these:
xxxxxxxxxxk config use-context infra-prod # back to admin context
k edit clusterrole giannaxxxxxxxxxx# kubectl edit clusterrole giannaapiVersionrbac.authorization.k8s.io/v1kindClusterRolemetadata creationTimestamp"2020-09-26T13:57:55Z" namegianna resourceVersion"4496" selfLink/apis/rbac.authorization.k8s.io/v1/clusterroles/gianna uidb713c1cf-87e5-4313-808e-1a51f392adc0rulesapiGroups"" resources# - secrets # removeconfigmapspodsnamespaces verbslist
Let's talk a little about RBAC resources:
A ClusterRole|Role defines a set of permissions and where it is available, in the whole cluster or just a single Namespace.
A ClusterRoleBinding|RoleBinding connects a set of permissions with an account and defines where it is applied, in the whole cluster or just a single Namespace.
Because of this there are 4 different RBAC combinations and 3 valid ones:
Role + RoleBinding (available in single Namespace, applied in single Namespace)
ClusterRole + ClusterRoleBinding (available cluster-wide, applied cluster-wide)
ClusterRole + RoleBinding (available cluster-wide, applied in single Namespace)
Role + ClusterRoleBinding (NOT POSSIBLE: available in single Namespace, applied cluster-wide)
The user gianna should be able to create Pods and Deployments in three Namespaces. We can use number 1 or 3 from the list above. But because the task says: "The user might receive these exact permissions as well for other Namespaces in the future", we choose number 3 as it requires to only create one ClusterRole instead of three Roles.
xxxxxxxxxxk create clusterrole gianna-additional --verb=create --resource=pods --resource=deploymentsThis will create a ClusterRole like:
xxxxxxxxxx# kubectl create clusterrole gianna-additional --verb=create --resource=pods --resource=deploymentsapiVersionrbac.authorization.k8s.io/v1kindClusterRolemetadata creationTimestampnull namegianna-additionalrulesapiGroups"" resourcespods verbscreateapiGroupsapps resourcesdeployments verbscreateNext the three bindings:
xxxxxxxxxxk -n security create rolebinding gianna-additional \--clusterrole=gianna-additional --user=gianna
k -n restricted create rolebinding gianna-additional \--clusterrole=gianna-additional --user=gianna
k -n internal create rolebinding gianna-additional \--clusterrole=gianna-additional --user=giannaWhich will create RoleBindings like:
xxxxxxxxxx# k -n security create rolebinding gianna-additional --clusterrole=gianna-additional --user=giannaapiVersionrbac.authorization.k8s.io/v1kindRoleBindingmetadata creationTimestampnull namegianna-additional namespacesecurityroleRef apiGrouprbac.authorization.k8s.io kindClusterRole namegianna-additionalsubjectsapiGrouprbac.authorization.k8s.io kindUser namegiannaAnd we test:
xxxxxxxxxx➜ k -n default auth can-i create pods --as giannano
➜ k -n security auth can-i create pods --as giannayes
➜ k -n restricted auth can-i create pods --as giannayes
➜ k -n internal auth can-i create pods --as giannayesFeel free to verify this as well by actually creating Pods and Deployments as user gianna through context gianna@infra-prod.
Use context: kubectl config use-context infra-prod
There is an existing Open Policy Agent + Gatekeeper policy to enforce that all Namespaces need to have label security-level set. Extend the policy constraint and template so that all Namespaces also need to set label management-team. Any new Namespace creation without these two labels should be prevented.
Write the names of all existing Namespaces which violate the updated policy into /opt/course/p2/fix-namespaces.
We look at existing OPA constraints, these are implemeted using CRDs by Gatekeeper:
xxxxxxxxxx➜ k get crdNAME CREATED ATblacklistimages.constraints.gatekeeper.sh 2020-09-14T19:29:31Zconfigs.config.gatekeeper.sh 2020-09-14T19:29:04Zconstraintpodstatuses.status.gatekeeper.sh 2020-09-14T19:29:05Zconstrainttemplatepodstatuses.status.gatekeeper.sh 2020-09-14T19:29:05Zconstrainttemplates.templates.gatekeeper.sh 2020-09-14T19:29:05Zrequiredlabels.constraints.gatekeeper.sh 2020-09-14T19:29:31ZSo we can do:
xxxxxxxxxx➜ k get constraintNAME AGEblacklistimages.constraints.gatekeeper.sh/pod-trusted-images 10m
NAME AGErequiredlabels.constraints.gatekeeper.sh/namespace-mandatory-labels 10mAnd check violations for the namespace-mandatory-label one, which we can do in the resource status:
xxxxxxxxxx➜ k describe requiredlabels namespace-mandatory-labelsName: namespace-mandatory-labelsNamespace: Labels: <none>Annotations: <none>API Version: constraints.gatekeeper.sh/v1beta1Kind: RequiredLabels...Status:... Total Violations: 1 Violations: Enforcement Action: deny Kind: Namespace Message: you must provide labels: {"security-level"} Name: sidecar-injectorEvents: We see one violation for Namespace "sidecar-injector". Let's get an overview over all Namespaces:
xxxxxxxxxx➜ k get ns --show-labelsNAME STATUS AGE LABELSdefault Active 21m management-team=green,security-level=highgatekeeper-system Active 14m admission.gatekeeper.sh/ignore=no-self-managing,control-plane=controller-manager,gatekeeper.sh/system=yes,management-team=green,security-level=highjeffs-playground Active 14m security-level=highkube-node-lease Active 21m management-team=green,security-level=highkube-public Active 21m management-team=red,security-level=lowkube-system Active 21m management-team=green,security-level=highrestricted Active 14m management-team=blue,security-level=mediumsecurity Active 14m management-team=blue,security-level=mediumsidecar-injector Active 14m <none>When we try to create a Namespace without the required label we get an OPA error:
xxxxxxxxxx➜ k create ns testError from server ([denied by namespace-mandatory-labels] you must provide labels: {"security-level"}): error when creating "ns.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [denied by namespace-mandatory-labels] you must provide labels: {"security-level"}Next we edit the constraint to add another required label:
xxxxxxxxxxk edit requiredlabels namespace-mandatory-labelsxxxxxxxxxx# kubectl edit requiredlabels namespace-mandatory-labelsapiVersionconstraints.gatekeeper.sh/v1beta1kindRequiredLabelsmetadata annotations kubectl.kubernetes.io/last-applied-configuration {"apiVersion":"constraints.gatekeeper.sh/v1beta1","kind":"RequiredLabels","metadata":{"annotations":{},"name":"namespace-mandatory-labels"},"spec":{"match":{"kinds":[{"apiGroups":[""],"kinds":["Namespace"]}]},"parameters":{"labels":["security-level"]}}} creationTimestamp"2020-09-14T19:29:53Z" generation1 namenamespace-mandatory-labels resourceVersion"3081" selfLink/apis/constraints.gatekeeper.sh/v1beta1/requiredlabels/namespace-mandatory-labels uid2a51a291-e07f-4bab-b33c-9b8c90e5125bspec match kindsapiGroups"" kindsNamespace parameters labelssecurity-levelmanagement-team # addAs we can see the constraint is using kind: RequiredLabels as template, which is a CRD created by Gatekeeper. Let's apply the change and see what happens (give OPA a minute to apply the changes internally):
xxxxxxxxxx➜ k describe requiredlabels namespace-mandatory-labels... Violations: Enforcement Action: deny Kind: Namespace Message: you must provide labels: {"management-team"} Name: jeffs-playgroundAfter the changes we can see that now another Namespace jeffs-playground is in trouble. Because that one only specifies one required label. But what about the earlier violation of Namespace sidecar-injector?
xxxxxxxxxx➜ k get ns --show-labelsNAME STATUS AGE LABELSdefault Active 21m management-team=green,security-level=highgatekeeper-system Active 17m admission.gatekeeper.sh/ignore=no-self-managing,control-plane=controller-manager,gatekeeper.sh/system=yes,management-team=green,security-level=highjeffs-playground Active 17m security-level=highkube-node-lease Active 21m management-team=green,security-level=highkube-public Active 21m management-team=red,security-level=lowkube-system Active 21m management-team=green,security-level=highrestricted Active 17m management-team=blue,security-level=mediumsecurity Active 17m management-team=blue,security-level=mediumsidecar-injector Active 17m <none>Namespace sidecar-injector should also be in trouble, but it isn't any longer. This doesn't seem right, it means we could still create Namespaces without any labels just like using k create ns test.
So we check the template:
xxxxxxxxxx➜ k get constrainttemplatesNAME AGEblacklistimages 20mrequiredlabels 20m
➜ k edit constrainttemplates requiredlabelsxxxxxxxxxx# kubectl edit constrainttemplates requiredlabelsapiVersiontemplates.gatekeeper.sh/v1beta1kindConstraintTemplate...spec crd spec names kindRequiredLabels validation openAPIV3Schema properties labels itemsstring typearray targetsrego package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] { provided := {label | input.review.object.metadata.labels[label]} required := {label | label := input.parameters.labels[_]} missing := required - provided # count(missing) == 1 # WRONG count(missing) > 0 msg := sprintf("you must provide labels: %v", [missing]) } target: admission.k8s.gatekeeper.shIn the rego script we need to change count(missing) == 1 to count(missing) > 0 . If we don't do this then the policy only complains if there is one missing label, but there can be multiple missing ones.
After waiting a bit we check the constraint again:
xxxxxxxxxx➜ k describe requiredlabels namespace-mandatory-labels... Total Violations: 2 Violations: Enforcement Action: deny Kind: Namespace Message: you must provide labels: {"management-team"} Name: jeffs-playground Enforcement Action: deny Kind: Namespace Message: you must provide labels: {"security-level", "management-team"} Name: sidecar-injectorEvents: <none>This looks better. Finally we write the Namespace names with violations into the required location:
xxxxxxxxxx# /opt/course/p2/fix-namespacessidecar-injectorjeffs-playground
Use context: kubectl config use-context workload-stage
A security scan result shows that there is an unknown miner process running on one of the Nodes in cluster3. The report states that the process is listening on port 6666. Kill the process and delete the binary.
We have a look at existing Nodes:
xxxxxxxxxx➜ k get nodeNAME STATUS ROLES AGE VERSIONcluster3-controlplane1 Ready control-plane 109m v1.28.5cluster3-node1 Ready <none> 105m v1.28.5First we check the master:
xxxxxxxxxx➜ ssh cluster3-controlplane1
➜ root@cluster3-controlplane1:~# netstat -plnt | grep 6666
➜ root@cluster3-controlplane1:~# Doesn't look like any process listening on this port. So we check the worker:
xxxxxxxxxx➜ ssh cluster3-node1
➜ root@cluster3-node1:~# netstat -plnt | grep 6666tcp6 0 0 :::6666 :::* LISTEN 9591/system-atm There we go! We could also use lsof:
xxxxxxxxxx➜ root@cluster3-node1:~# lsof -i :6666COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAMEsystem-at 9591 root 3u IPv6 47760 0t0 TCP *:6666 (LISTEN)Before we kill the process we can check the magic /proc directory for the full process path:
xxxxxxxxxx➜ root@cluster3-node1:~# ls -lh /proc/9591/exelrwxrwxrwx 1 root root 0 Sep 26 16:10 /proc/9591/exe -> /bin/system-atmSo we finish it:
xxxxxxxxxx➜ root@cluster3-node1:~# kill -9 9591
➜ root@cluster3-node1:~# rm /bin/system-atmDone.
In this section we'll provide some tips on how to handle the CKS exam and browser terminal.
Pre-Knowledge
You should have your CKA knowledge up to date and be fast with kubectl, so we suggest to do:
Study all scenarios on https://killercoda.com/killer-shell-cka
Knowledge
Study all topics as proposed in the curriculum till you feel comfortable with all.
Study all scenarios on https://killercoda.com/killer-shell-cks
Read the free Sysdig Kubernetes Security Guide
Also a nice read (though based on outdated k8s version) is the Kubernetes Security book by Liz Rice
Check out the Cloud Native Security Whitepaper
Great repository with many tips and sources: walidshari
Approach
Do 1 or 2 test session with this CKS Simulator. Understand the solutions and maybe try out other ways to achieve the same thing.
Setup your aliases, be fast and breath kubectl
Content
Be comfortable with changing the kube-apiserver in a kubeadm setup
Be able to work with AdmissionControllers
Know how to create and use the ImagePolicyWebhook
Know how to use opensource tools Falco, Sysdig, Tracee, Trivy
Read the Curriculum
https://github.com/cncf/curriculum
Read the Handbook
https://docs.linuxfoundation.org/tc-docs/certification/lf-handbook2
Read the important tips
https://docs.linuxfoundation.org/tc-docs/certification/important-instructions-cks
Read the FAQ
https://docs.linuxfoundation.org/tc-docs/certification/faq-cka-ckad-cks
Get familiar with the Kubernetes documentation and be able to use the search. Allowed links are:
NOTE: Verify the list here
In the CKS exam you'll get access to as many clusters as you have questions, each will be solved in its own cluster. This is great because you cannot interfere with other tasks by breaking one. Every cluster will have one master and one worker node.
You'll be provided with a browser terminal which uses Ubuntu 20. The standard shells included with a minimal install of Ubuntu 20 will be available, including bash.
Laggin
There could be some lagging, definitely make sure you are using a good internet connection because your webcam and screen are uploading all the time.
Kubectl autocompletion and commands
Autocompletion is configured by default, as well as the k alias source and others:
kubectl with k alias and Bash autocompletion
yq and jqfor YAML/JSON processing
tmux for terminal multiplexing
curl and wget for testing web services
man and man pages for further documentation
Copy & Paste
There could be issues copying text (like pod names) from the left task information into the terminal. Some suggested to "hard" hit or long hold Cmd/Ctrl+C a few times to take action. Apart from that copy and paste should just work like in normal terminals.
Score
There are 15-20 questions in the exam. Your results will be automatically checked according to the handbook. If you don't agree with the results you can request a review by contacting the Linux Foundation Support.
Notepad & Skipping Questions
You have access to a simple notepad in the browser which can be used for storing any kind of plain text. It might makes sense to use this for saving skipped question numbers. This way it's possible to move some questions to the end.
Contexts
You'll receive access to various different clusters and resources in each. They provide you the exact command you need to run to connect to another cluster/context. But you should be comfortable working in different namespaces with kubectl.
Starting with PSI Bridge:
The exam will now be taken using the PSI Secure Browser, which can be downloaded using the newest versions of Microsoft Edge, Safari, Chrome, or Firefox
Multiple monitors will no longer be permitted
Use of personal bookmarks will no longer be permitted
The new ExamUI includes improved features such as:
A remote desktop configured with the tools and software needed to complete the tasks
A timer that displays the actual time remaining (in minutes) and provides an alert with 30, 15, or 5 minute remaining
The content panel remains the same (presented on the Left Hand Side of the ExamUI)
Read more here.
It should be considered to spend ~1 minute in the beginning to setup your terminal. In the real exam the vast majority of questions will be done from the main terminal. For few you might need to ssh into another machine. Just be aware that configurations to your shell will not be transferred in this case.
Alias
The alias k for kubectl will already be configured together with autocompletion. In case not you can configure it using this link.
Vim
The following settings will already be configured in your real exam environment in ~/.vimrc. But it can never hurt to be able to type these down:
set tabstop=2set expandtabset shiftwidth=2
The expandtab make sure to use spaces for tabs. Memorize these and just type them down. You can't have any written notes with commands on your desktop etc.
Fast dry-run output
xxxxxxxxxxexport do="--dry-run=client -o yaml"
This way you can just run k run pod1 --image=nginx $do. Short for "dry output", but use whatever name you like.
Fast pod delete
xxxxxxxxxxexport now="--force --grace-period 0"
This way you can run k delete pod1 $now and don't have to wait for ~30 seconds termination time.
Persist bash settings
You can store aliases and other setup in ~/.bashrc if you're planning on using different shells or tmux.
Alias Namespace
In addition you could define an alias like:
xxxxxxxxxxalias kn='kubectl config set-context --current --namespace 'Which allows you to define the default namespace of the current context. Then once you switch a context or namespace you can just run:
xxxxxxxxxxkn default # set default to defaultkn my-namespace # set default to my-namespaceBut only do this if you used it before and are comfortable doing so. Else you need to specify the namespace for every call, which is also fine:
xxxxxxxxxxk -n my-namespace get allk -n my-namespace get pod...
Use the history command to reuse already entered commands or use even faster history search through Ctrl r .
If a command takes some time to execute, like sometimes kubectl delete pod x. You can put a task in the background using Ctrl z and pull it back into foreground running command fg.
You can delete pods fast with:
xk delete pod x --grace-period 0 --force
k delete pod x $now # if export from above is configured
Be great with vim.
toggle vim line numbers
When in vim you can press Esc and type :set number or :set nonumber followed by Enter to toggle line numbers. This can be useful when finding syntax errors based on line - but can be bad when wanting to mark© by mouse. You can also just jump to a line number with Esc :22 + Enter.
copy&paste
Get used to copy/paste/cut with vim:
xxxxxxxxxxMark lines: Esc+V (then arrow keys)Copy marked lines: yCut marked lines: dPast lines: p or P
Indent multiple lines
To indent multiple lines press Esc and type :set shiftwidth=2. First mark multiple lines using Shift v and the up/down keys. Then to indent the marked lines press > or <. You can then press . to repeat the action.
By default tmux is installed and can be used to split your one terminal into multiple. But just do this if you know your shit, because scrolling is different and copy&pasting might be weird.
https://www.hamvocke.com/blog/a-quick-and-easy-guide-to-tmux