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 Admission
instead.
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/containers
crictl ps
+crictl logs
docker ps
+docker logs
(in case when Docker is used)- kubelet logs:
/var/log/syslog
orjournalctl
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=123
to Nodecontrolplane
- add label
node-restriction.kubernetes.io/one=123
to 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.json
points to correct kubeconfig - Set the
allowTTL
to100
- All Pod creation should be prevented if the external service is not reachable
- The external service will be reachable under
https://localhost:1234
in 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 -- sh
into 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.uid
at any position - include
repo=%container.image.repository
at 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-server
In 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
smoke
should be allowed tocreate
anddelete
Pods, Deployments and StatefulSets in Namespaceapplications
. - User
smoke
should haveview
permissions (like the permissions of the default ClusterRole namedview
) in all Namespaces but not inkube-system
. - User
smoke
should 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
view
almost everything in the whole cluster. You can use the default ClusterRoleview
for this - These SAs should be allowed to
create
anddelete
Deployments
in 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
aesgcm
with passwordthis-is-very-sec
. All new secrets should be encrypted using this one. - One provider should be the
identity
one 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
prime
imagenginx: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.04
for 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 exec
orkubectl exec
into 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-image
from the Dockerfile. - Run a container named
c1
from 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 |