Pod Security Policies(PSP)
Reference:
- k8s Pod Security Policy官方文档
- aws eks 官方文档
- PodSecurityPolicy is Dead, Long Live…?
- https://docs.bitnami.com/kubernetes/faq/configuration/understand-pod-security-policies/
Removed feature
PodSecurityPolicy was deprecated in Kubernetesv1.21, and removed from Kubernetes inv1.25.
UsePod Security Admissioninstead.
A Pod Security Policy is a cluster-level resource that controls security sensitive aspects of the pod specification.
AppArmor
kubernetes CKS 3.2 AppArmor限制容器对资源访问
Manage AppArmor profiles and Pods using these







Apiserver Crash
Crash that Apiserver and check them logs
kube-apiserver.yaml Location: /etc/kubernetes/manifests/kube-apiserver.yaml
Log locations to check:
/var/log/pods/var/log/containerscrictl ps+crictl logsdocker ps+docker logs(in case when Docker is used)- kubelet logs:
/var/log/syslogorjournalctl
1 | # smart people use a backup |
Give the Apiserver some time to restart itself, like 1-2 minutes. If it doesn’t restart by itself you can also force it with:
- 1:
mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/kube-apiserver.yaml - 2: wait till container is removed with
watch crictl ps - 3:
mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/kube-apiserver.yaml
Configure a wrong argument
The idea here is to misconfigure the Apiserver in different ways, then check possible log locations for errors.
You should be very comfortable with situations where the Apiserver is not coming back up.
Configure the Apiserver manifest with a new argument --this-is-very-wrong .
Check if the Pod comes back up and what logs this causes.
Fix the Apiserver again.
1 | # always make a backup ! |
Apiserver is not coming back, we messed up!
1 | # check pod logs |
Now undo the change and continue
1 | # smart people use a backup |
Misconfigure ETCD connection
Change the existing Apiserver manifest argument to: --etcd-servers=this-is-very-wrong .
Check what the logs say, without using anything in /var .
Fix the Apiserver again.
1 | # always make a backup ! |
Apiserver is not coming back, we messed up!
1 | # 1) if we would check the /var directory |
Now undo the change and continue
1 | # smart people use a backup |
Invalid Apiserver Manifest YAML
Change the Apiserver manifest and add invalid YAML, something like this:
1 | apiVersionTHIS IS VERY ::::: WRONG v1 |
Check what the logs say, and fix again.
Fix the Apiserver again.
Apiserver is not coming back, we messed up!
1 | # seems like the kubelet can't even create the apiserver pod/container |
Apiserver Misconfigured
https://killercoda.com/killer-shell-cks/scenario/apiserver-misconfigured
Make sure to have solved the previous Scenario Apiserver Crash.
The Apiserver is not coming up, the manifest is misconfigured in 3 places. Fix it.
Issues
For your changes to apply you might have to:
move the kube-apiserver.yaml out of the manifests directory
wait for apiserver container to be gone (watch crictl ps )
move the manifest back in and wait for apiserver coming back up
Some users report that they need to restart the kubelet (service kubelet restart ) but in theory this shouldn’t be necessary.
Solution 1
The kubelet cannot even create the Pod/Container. Check the kubelet logs in syslog for issues.
1 | cat /var/log/syslog | grep kube-apiserver |
There is wrong YAML in the manifest at metadata;
1 | vim /etc/kubernetes/manifests/kube-apiserver.yaml |
Solution 2
After fixing the wrong YAML there still seems to be an issue with a wrong parameter.
1 | # Check logs in `/var/log/pods`. |
Solution 3
After fixing the wrong parameter, the pod/container might be up, but gets restarted.
Check container logs or /var/log/pods, where we should find:
Error while dialing dial tcp 127.0.0.1:23000: connect:connection refused
Check the container logs: the ETCD connection seems to be wrong. Set the correct port on which ETCD is running (check the ETCD manifest)
1 | vim /etc/kubernetes/manifests/kube-apiserver.yaml |
Apiserver NodeRestriction
Verify the issue
The Kubelet on node01 shouldn’t be able to set Node labels
- starting with
node-restriction.kubernetes.io/* - on other Nodes than itself (不能label其他node,只能label本node:
node01)
Verify this is not restricted atm by performing the following actions as the Kubelet from node01 :
- add label
killercoda/one=123to Nodecontrolplane - add label
node-restriction.kubernetes.io/one=123to Nodenode01
Tip
We can contact the Apiserver as the Kubelet by using the Kubelet kubeconfig
1 | export KUBECONFIG=/etc/kubernetes/kubelet.conf |
Solution
1 | ssh node01 |
Enable the NodeRestriction Admission Controller
1 | node01 $ exit |
1 | spec: |
1 | ssh node01 |
Notice that existing restricted labels won’t be removed once the NodeRestriction is enabled.
ImagePolicyWebhook
Complete the ImagePolicyWebhook setup
Complete the ImagePolicyWebhook setup
An ImagePolicyWebhook setup has been half finished, complete it:
- Make sure
admission_config.jsonpoints to correct kubeconfig - Set the
allowTTLto100 - All Pod creation should be prevented if the external service is not reachable
- The external service will be reachable under
https://localhost:1234in the future. It doesn’t exist yet so it - shouldn’t be able to create any Pods till then - Register the correct admission plugin in the apiserver
Solution
1
2
3
4
5
6
7# find admission_config.json path in kube-apiserver.yaml
vim /etc/kubernetes/manifests/kube-apiserver.yaml # Find value of --admission-control-config-file
# or
cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep admission-control-config-file
- --admission-control-config-file=/etc/kubernetes/policywebhook/admission_config.json
vim /etc/kubernetes/policywebhook/admission_config.json
The /etc/kubernetes/policywebhook/admission_config.json should look like this:
1 | { |
1 | vim /etc/kubernetes/policywebhook/kubeconf |
The /etc/kubernetes/policywebhook/kubeconf should contain the correct server:
1 | apiVersion: v1 |
5 The apiserver needs to be configured with the ImagePolicyWebhook admission plugin:
1 | vim /etc/kubernetes/manifests/kube-apiserver.yaml |
1 | spec: |
Luckily the --admission-control-config-file argument seems already to be configured.
Test your Solution
Wait till apiserver container restarted:
watch crictl ps
To test your solution you can simply try to create a Pod:
k run pod --image=nginx
It should throw you an error like:
1 | Error from server (Forbidden): pods "pod" is forbidden: Post "https://localhost:1234/?timeout=30s": dial tcp 127.0.0.1:1234: connect: connection refused |
Once that service would be implemented and if it would allow the Pod, the Pod could be created.
kube-bench
cis-benchmarks-kube-bench-fix-controlplane
- Api Server:
/etc/kubernetes/manifests/kube-apiserver.yaml - Controller Manager pod specification file
/etc/kubernetes/manifests/kube-controller-manager.yaml - Kubelet:
/var/lib/kubelet/config.yaml - etcd:
/etc/kubernetes/manifests/etcd.yaml
Apiserver should be more conform to CIS
Use kube-bench to ensure 1.2.20 has status PASS.
Solution
Check for results
1 | # see all |
Fix the /etc/kubernetes/manifests/kube-apiserver.yaml
1 | ... |
Now wait for container to be restarted: watch crictl ps
ControllerManager should be more conform to CIS
Use kube-bench to ensure 1.3.2 has status PASS.
1 | # 推荐这种方法,输出比较少,好看关键信息 |
Fix the /etc/kubernetes/manifests/kube-controller-manager.yaml
1 | ... |
Now wait for container to be restarted: watch crictl ps
PKI directory should be more conform to CIS
1 | # 推荐这种方法,输出比较少,好看关键信息 |
Fix the /etc/kubernetes/pki/
1 | chgrp root /etc/kubernetes/pki/ |
Trivy
Image Vulnerability Scanning Trivy
Use trivy to scan images for known vulnerabilities
Using trivy :
Scan images in Namespaces applications and infra for the vulnerabilities CVE-2021-28831 and CVE-2016-9841 .
Scale those Deployments containing any of these down to 0 .
Solution:
First we check the applications Namespace.
1 | # find images |
Next we check the infra Namespace.
1 | # find images |
Falco
https://killercoda.com/killer-shell-cks/scenario/falco-change-rule
Investigate a Falco Rule
Falco has been installed on Node controlplane and it runs as a service.
It’s configured to log to syslog, and this is where the verification for this scenario also looks.
Cause the rule “shell in a container” to log by:
- creating a new Pod image
nginx:alpine kubectl exec pod -- shinto it- check the Falco logs contain a related output
Tip
service falco status
cat /var/log/syslog | grep falco
Solution
k run pod --image=nginx:alpine
k exec -it pod -- sh
exit
cat /var/log/syslog | grep falco | grep shell
Change a Falco Rule
Change the Falco output of rule “Terminal shell in container” to:
- include
NEW SHELL!!!at the very beginning - include
user_id=%user.uidat any position - include
repo=%container.image.repositoryat any position
Cause syslog output again by creating a shell in that Pod.
Verify the syslogs contain the new data.
Tip
https://falco.org/docs/rules/supported-fields
1 | cd /etc/falco/ |
Solution
1 | cd /etc/falco/ |
1 | - rule: Terminal shell in container |
1 | service falco restart |
NetworkPolicy - namespace selector
There are existing Pods in Namespace space1 and space2 .
We need a new NetworkPolicy named np that restricts all Pods in Namespace space1 to only have outgoing traffic to Pods in Namespace space2 . Incoming traffic not affected.
We also need a new NetworkPolicy named np that restricts all Pods in Namespace space2 to only have incoming traffic from Pods in Namespace space1 . Outgoing traffic not affected.
The NetworkPolicies should still allow outgoing DNS traffic on port 53 TCP and UDP.
Tip
For learning you can check the NetworkPolicy Editor
The namespaceSelector from NPs works with Namespace labels, so first we check existing labels for Namespaces
Solution
1 | # 获取 namespace的label |
1 | # 1.yaml |
1 | # 2.yaml |
1 | k apply -f 1.yaml |
NetworkPolicy - Metadata Protection
https://killercoda.com/killer-shell-cks/scenario/networkpolicy-metadata-protection
Cloud providers can have Metadata Servers which expose critical information, for example GCP or AWS.
For this task we assume that there is a Metadata Server at 1.1.1.1 .
You can test connection to that IP using nc -v 1.1.1.1 53 .
Create a NetworkPolicy named metadata-serverIn Namespace default which restricts all egress traffic to that IP.
The NetworkPolicy should only affect Pods with label trust=nope.
1 | apiVersion: networking.k8s.io/v1 |
RBAC - user
https://killercoda.com/killer-shell-cks/scenario/rbac-user-permissions
There is existing Namespace applications.
- User
smokeshould be allowed tocreateanddeletePods, Deployments and StatefulSets in Namespaceapplications. - User
smokeshould haveviewpermissions (like the permissions of the default ClusterRole namedview) in all Namespaces but not inkube-system. - User
smokeshould be allowed to retrieve available Secret names in Namespaceapplications. Just the Secret names, no data. - Verify everything using
kubectl auth can-i.
Solution
1) RBAC for Namespace applications
1 | k -n applications create role smoke --verb create,delete --resource pods,deployments,sts |
2) view permission in all Namespaces but not kube-system
As of now it’s not possible to create deny-RBAC in K8s
So we allow for all other Namespaces
1 | k get ns # get all namespaces |
3) just list Secret names, no content
This is NOT POSSIBLE using plain K8s RBAC. You might think of doing this:
1 | # NOT POSSIBLE: assigning "list" also allows user to read secret values |
Having the list verb you can simply run kubectl get secrets -oyaml and see all content. Dangerous misconfiguration!
Verify
1 | # applications |
RBAC - ServiceAccount
There are existing Namespaces ns1 and ns2
Create ServiceAccount pipeline in both Namespaces.
- These SAs should be allowed to
viewalmost everything in the whole cluster. You can use the default ClusterRoleviewfor this - These SAs should be allowed to
createanddeleteDeploymentsin their Namespace.
Verify everything using kubectl auth can-i
Solution
1 | # create Namespaces |
Secret - ETCD Encryption
https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/encrypt-data/
https://killercoda.com/killer-shell-cks/scenario/secret-etcd-encryption
Enable ETCD Encryption
Create an EncryptionConfiguration file at /etc/kubernetes/etcd/ec.yaml and make ETCD use it.
- One provider should be of type
aesgcmwith passwordthis-is-very-sec. All new secrets should be encrypted using this one. - One provider should be the
identityone to still be able to read existing unencrypted secrets.
Solution
Generate EncryptionConfiguration:
1 | mkdir -p /etc/kubernetes/etcd |
1 | apiVersion: apiserver.config.k8s.io/v1 |
-
Add a new volume and volumeMount in
/etc/kubernetes/manifests/kube-apiserver.yaml, so that the container can access the file. -
Pass the new file as argument:
--encryption-provider-config=/etc/kubernetes/etcd/ec.yaml
1 | spec: |
Wait till apiserver was restarted: watch crictl ps
Encrypt existing Secrets
Encrypt all existing Secrets in Namespace one using the new provider
Encrypt all existing Secrets in Namespace two using the new provider
Encrypt all existing Secrets in Namespace three using the new provider
Tip
Recreate Secrets so they are encrypted through the new encryption settings.
Solution
1 | kubectl -n one get secrets -o json | kubectl replace -f - |
To check you can do for example:
1 | 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/one/s1 |
The output should be encrypted and prefixed with k8s:enc:aesgcm:v1:key1.
Secret - Read and Decode
Read and decode the Secrets in Namespace one
- Get the Secrets of type Opaque that have been created in Namespace one .
- Create a new file called /opt/ks/one and store the base64-decoded values in that file. Each value needs to be stored on a new line.
Solution
1 | k get secrets -n one |
Your file /opt/ks/one should look like this:
1 | secret |
Privilege Escalation Containers
Set Privilege Escalation for Deployment
There is a Deployment named logger which constantly outputs the NoNewPrivs flag.
Let the Pods of that Deployment run with Privilege Escalation disabled.
The logs should show the field change.
Solution
Check them logs k logs -f deploy/logger
Edit the Deployment and set the allowPrivilegeEscalation field:
k edit deployments.apps logger
1 | ... |
Privileged Containers
https://killercoda.com/killer-shell-cks/scenario/privileged-containers
Create a privileged Pod
- Create a Pod named
primeimagenginx:alpine. - The container should run as
privileged.
Install iptables (apk add iptables ) inside the Pod.
Test the capabilities using iptables -L .
Solution
Generate Pod yaml
k run prime --image=nginx:alpine -oyaml --dry-run=client --command -- sh -c 'sleep 1d' > pod.yaml
Set the privileged :
1 | apiVersion: v1 |
Now exec into the Pod and run apk add iptables .
k exec prime -- apk add iptables
You’ll see that iptables -L needs capabilities to run which it here gets through privileged.
k exec prime -- iptables -L
Create a privileged StatefulSet
There is an existing StatefulSet yaml at /application/sts.yaml .
It should run as privileged but it seems like it cannot be applied.
Fix it and create the StatefulSet.
Solution
Edit the yaml to set privileged in the container section:
1 | apiVersion: apps/v1 |
Container Hardening
Harden a given Docker Container
There is a Dockerfile at /root/image/Dockerfile .
It’s a simple container which tries to make a curl call to an imaginary api with a secret token, the call will 404 , but that’s okay.
- Use specific version
20.04for the base image - Remove layer caching issues with
apt-get - Remove the hardcoded secret value
2e064aad-3a90–4cde-ad86–16fad1f8943e. The secret value should be passed into the container during runtime as env variableTOKEN - Make it impossible to
docker exec,podman execorkubectl execinto the container usingbash
You can build the image using
1 | cd /root/image |
Solution
1 | # vim /root/image/Dockerfile |
1 | # 修改为 |
1 | cd /root/image |
Container Image Footprint User
Run the default Dockerfile
There is a given Dockerfile under /opt/ks/Dockerfile .
Using Docker:
- Build an image named
base-imagefrom the Dockerfile. - Run a container named
c1from that image. - Check under which user the sleep process is running inside the container
Solution
1 | # Build and run |
Run container as user
Modify the Dockerfile /opt/ks/Dockerfile to run processes as user appuser
Update the image base-image with your change
Build a new container c2 from that image
Solution
Add the USER docker command:
1 | FROM alpine:3.12.3 |
1 | # Build and run: |
RuntimeClass - gVisor
https://killercoda.com/killer-shell-cks/scenario/sandbox-gvisor
Install and configure gVisor
You should install gVisor on the node node01 and make containerd use it.
There is install script /root/gvisor-install.sh which should setup everything, execute it on node node01 .
Solution
1 | scp gvisor-install.sh node01:/root |
Create RuntimeClass and Pod to use gVisor
Now that gVisor should be configured, create a new RuntimeClass for it.
Then create a new Pod named sec using image nginx:1.21.5-alpine .
Verify your setup by running dmesg in the Pod.
Tip
The handler for the gVisor RuntimeClass is runsc.
Solution
1 | # Back to cantroplane |
First we create the RuntimeClass
1 | apiVersion: node.k8s.io/v1 |
And the Pod that uses it
1 | apiVersion: v1 |
1 | k apply -f rc.yaml |
Verify
1 | k exec sec -- dmesg | grep -i gvisor |
CertificateSigningRequests sign manually
https://killercoda.com/killer-shell-cks/scenario/certificate-signing-requests-sign-manually
Create KEY and CSR
The idea here is to create a new “user” that can communicate with K8s.
For this now:
- Create a new KEY at /root/60099.key for user named 60099@internal.users
- Create a CSR at /root/60099.csr for the KEY
Explanation
Users in K8s are managed via CRTs and the CN/CommonName field in them. The cluster CA needs to sign these CRTs.
This can be achieved with the following procedure:
- Create a KEY (Private Key) file
- Create a CSR (CertificateSigningRequest) file for that KEY
- Create a CRT (Certificate) by signing the CSR. Done using the CA (Certificate Authority) of the cluster
Tip
1 | openssl genrsa -out XXX 2048 |
Solution
1 | openssl genrsa -out 60099.key 2048 |
Manual signing
Manually sign the CSR with the K8s CA file to generate the CRT at /root/60099.crt .
Create a new context for kubectl named 60099@internal.users which uses this CRT to connect to K8s.
Tip 1
1 | openssl x509 -req -in XXX -CA XXX -CAkey XXX -CAcreateserial -out XXX -days 500 |
Tip 2
1 | find /etc/kubernetes/pki | grep ca |
Solution 1
1 | openssl x509 -req -in 60099.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out 60099.crt -days 500 |
Solution 2
1 | k config set-credentials 60099@internal.users --client-key=60099.key --client-certificate=60099.crt |
CertificateSigningRequests sign via API
https://killercoda.com/killer-shell-cks/scenario/certificate-signing-requests-sign-k8s
Create KEY and CSR
The idea here is to create a new “user” that can communicate with K8s.
For this now:
- Create a new KEY at /root/60099.key for user named 60099@internal.users
- Create a CSR at /root/60099.csr for the KEY
Tip
1 | openssl genrsa -out XXX 2048 |
Solution
1 | openssl genrsa -out 60099.key 2048 |
Signing via API
Create a K8s CertificateSigningRequest resource named 60099@internal.users and which sends the /root/60099.csr to the API.
Let the K8s API sign the CertificateSigningRequest.
Download the CRT file to /root/60099.crt .
Create a new context for kubectl named 60099@internal.users which uses this CRT to connect to K8s.
CertificateSigningRequest template
1 | apiVersion: certificates.k8s.io/v1 |
Solution
Convert the CSR file into base64
cat 60099.csr | base64 -w 0
Copy it into the YAML
1 | apiVersion: certificates.k8s.io/v1 |
Create and approve
1 | k -f csr.yaml create |
Use the CRT
1 | k config set-credentials 60099@internal.users --client-key=60099.key --client-certificate=60099.crt |
Image Use Digest
Use an image digest instead of tag
Image tags can be overwritten, digests not.
Create a Pod named crazy-pod which uses the image digest nginx@sha256:eb05700fe7baa6890b74278e39b66b2ed1326831f9ec3ed4bdc6361a4ac2f333 .
Solution
1 | # Simply use the image@sha256:digest as image: |
Switch deployment from using tag to digest
Convert the existing Deployment crazy-deployment to use the image digest of the current tag instead of the tag.
Solution
1 | # get digest |
securityContext - Immutability Readonly Filesystem
https://killercoda.com/killer-shell-cks/scenario/immutability-readonly-fs
Create a Pod with read-only filesystem
Create a Pod named pod-ro in Namespace sun of image busybox:1.32.0 .
Make sure the container keeps running, like using sleep 1d .
The container root filesystem should be read-only.
Solution
1 | # Generate Pod yaml |
Set the readOnlyRootFilesystem :
1 | apiVersion: v1 |
1 | k apply -f pod.yaml |
Fix existing Nginx Deployment to work with read-only filesystem
The Deployment web4.0 in Namespace moon doesn’t seem to work with readOnlyRootFilesystem .
Add an emptyDir volume to fix this.
Solution
1 | Check the logs to find the location that needs to be writable |
Edit the Deployment, add a new emptyDir volume
1 | ... |
Static Manual Analysis K8s
Analyse K8s Pod YAML
Perform a manual static analysis on files /root/apps/app1-* considering security.
Move the less secure file to /root/insecure
1 | # app1-30b5eba5.yaml |
Tip
Enforcing a read-only root filesystem can make containers more secure.
Solution
1 | cd /root/apps |
Analyse K8s Deployment YAML
Perform a manual static analysis on files /root/apps/app2-* considering security.
Move the less secure file to /root/insecure
1 | # app2-b917e60e.yaml |
Tip
Check the securityContext settings, just because there are some doesn’t mean they do something good or at all.
Solution
File app2-b917e60e.yaml has some securityContext settings, but they don’t drop any capabilities and even allow allowPrivilegeEscalation.
1 | mv /root/apps/app2-b917e60e.yaml /root/insecure |
Analyse K8s StatefulSet YAML
Perform a manual static analysis on files /root/apps/app3-* considering security.
Move the less secure file to /root/insecure
1 | # app3-819f4686.yaml |
Tip
If you face large files, search for settings like securityContext .
Solution
We see usage of privileged: true .
1 | cat /root/apps/app3-819f4686.yaml | grep securityContext -A 3 |