Kubernetes/Kubernetes Workshop/Step 8
Step 8: K8s Manageability and Security
Kubernetes is a new execution environment for code, in that sense quite similar to a new operating system albeit with familiar roots. It is powerful, flexible and complex. Like other software it is becoming more mature with each new version. A new version typically also addresses vulnerabilities that can be used by an attacker to abuse the system.
As k8s became more successful guidelines came out on how to use and configure k8s for a safe experience. So far we have not followed or looked at these guidelines, roughly the equivalent of running a Linux system off a not too old CD/DVD as root.
Typical guidelines:
- Container images should be updated
- Containers should run as a normal user, not as root
- Containers should run in a separate namespace, not in the default shared namespace
- Container registries that can be used to get images from should be limited
- Kubernetes itself is kept updated
- The underlying OS that runs kubernetes is managed and updated
Hands-on: Container image updates
Over time the software installed in the container will become outdated. Even the base images that we have used so far can be out of date when we download them.
We have been using base images from dockerhub in a rather careless way, without much scrutiny as to the origin or patchlevel. That is ok for learning and exploration but not for a service in production. In addition we have been using quite basic images that are under the control of certain trustworthy organizations such as Ubuntu and Debian, which provides some additional confidence. But there are many (millions) of docker images on dockerhub and most contain vulnerabilities, many of them high severity (see http://dance.csc.ncsu.edu/papers/codaspy17.pdf). Since using an available image is often the quickest way to get an application up and running, it is tempting to do so, but quite possibly a bad idea for a production system. In addition attackers have started to infiltrate docker registries - see this news article for a recent example.
If you do use any external images, be sure to exercise some caution as to their purpose and the data that you store in these systems.
Let’s perform a quick check on the images we have been using:
Dockerfile:
FROM ubuntu
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get upgrade -
/security/namespace$ kubectl get cronjobs
No resources found in default namespace.
/security/namespace$ kubectl get cronjobs --namespace=cronpywpchksumbot-dev
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
cronpywpchksumbot */5 * * * * False 0 <none> 85s
By reducing the /etc/apt/sources.list file to only security sources we get
The following packages will be upgraded:
gcc-10-base libgcc-s1 libgnutls30 libseccomp2 libstdc++6 perl-base
Including OS updates in the Dockerfile and rebuilding and redeploying of application images in accordance with standards set by your security team makes sense.
Hands-on: Run the container as non-root
Containers run by default as root. All applications that we have run so far have been running as root. Let’s check.
Dockerfile:
FROM ubuntu
RUN apt-get update && apt-get install -y ca-certificates python3
ADD printuid.py /
CMD ["/printuid.py"]
printuid.py:
#!/usr/bin/python3
import os
print(os.getuid())
- docker build --tag printuid .
- docker run printuid
Should give you a “0” as output, that means you are running as root.
Now run it on minikube
Quick sidebar: We have been running images on minikube by uploading them to the dockerhub and pulling them down again. But minikube can use your local images as well. The problem is that it uses a different local docker installation that keeps its own images. In order to built for that docker we have to switch:
- eval $(minikube docker-env)
- To undo this: eval $(minikube docker-env --unset)
- docker images # to check on images present in minikube
Rebuild the image and retag so the minikube can access the image locally. Remember that In the past we have avoided that action by uploading the image to the dockerhub and pulling it from there. But this way we can save some cycles and work entirely locally.
- docker build --tag printuid .
- docker images
Now run the the image as a job and check the output with:
- Kubectl apply -f job.yaml
- kubectl get pods
- kubectl logs <pod in question>
Note the image is just specified as “printuid” and the imagePullPolicy is “Never”.
job.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: printuid
spec:
template:
spec:
containers:
- name: printuid
image: printuid:latest
imagePullPolicy: Never
restartPolicy: Never
Ok, we are running as root here as well. The user can be specified in the Dockerfile such as:
FROM ubuntu
RUN apt-get update && apt-get install -y ca-certificates python3
ADD printuid.py /
USER nobody
CMD ["/printuid.py"]
Rebuild the docker file and rerun to check. Sample Output (redacted for length):
/k8s/security/root$ docker build --tag printuid .
Sending build context to Docker daemon 4.096kB
Step 1/5 : FROM ubuntu
---> adafef2e596e
…
Successfully built 6438746fda15
Successfully tagged printuid:latest
/k8s/security/root$ docker run printuid
65534
/k8s/security/root$ kubectl apply -f job.yaml
job.batch/printuid created
/k8s/security/root$ kubectl get pods
NAME READY STATUS RESTARTS AGE
printuid-djbgd 0/1 Completed 0 6s
/k8s/security/root$ kubectl logs printuid-djbgd
65534
Now let’s adapt the simpleapache image to run as non-root. Give it a try to see what needs to change in order to work before looking at the Dockerfile below. Dockerfile:
FROM ubuntu
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y apache2
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_RUN_DIR /var/run
ENV APACHE_PID_FILE /var/run/apache2.pid
RUN echo 'Hello Apache in Docker' > /var/www/html/index.html
RUN sed -r -i 's?isten 80?isten 8080?' /etc/apache2/ports.conf
RUN sed -r -i 's?:80?:8080?' /etc/apache2/sites-available/000-default.conf
RUN chown -R nobody /var/log/apache2
RUN chown -R nobody /var/run
RUN chown -R nobody /var/lock
EXPOSE 8080
CMD ["/usr/sbin/apachectl", "-D FOREGROUND"]
To return to the local docker:
- eval $(minikube docker-env --unset)
Hands-on: Namespaces
Namespaces are used to logically partition a kubernetes cluster, for example into environments for development, staging and production or for different teams or applications. So far all of our containers have been running in the same namespace called default. Using a namespace will provide separation between these environments, minimizing the possibility of name collisions and enhancing manageability of the cluster.
Namespaces are a kubernetes construct and need to be created and then added in the deployment files. Let’s run our initial example in two separate namespaces for used for development and production.
cronpywpchksumbot-dev.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: cronpywpchksumbot-dev
cronpywpchksumbot-prod.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: cronpywpchksumbot-prod
cronpywpchksumbotdeployment1.yaml:
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: cronpywpchksumbot
namespace: cronpywpchksumbot-dev
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: pywpchksumbot
image: <userid>/pywpchksumbot
imagePullPolicy: IfNotPresent
restartPolicy: OnFailure
cronpywpchksumbotdeployment2.yaml:
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: cronpywpchksumbot
namespace: cronpywpchksumbot-prod
spec:
schedule: "*/15 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: pywpchksumbot
image: <userid>/pywpchksumbot
imagePullPolicy: IfNotPresent
restartPolicy: OnFailure
You now have to add the --namespace=<name of namespace> to kubectl to get information on the cronjob. Alternatively use the --all-namespaces option to get info on all namespaces. Sample output:
/security/namespace$ kubectl get cronjobs
No resources found in default namespace.
/security/namespace$ kubectl get cronjobs --namespace=cronpywpchksumbot-dev
NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE
cronpywpchksumbot */5 * * * * False 0 <none> 85s
Note that using namespaces in that way is not enforced by kubernetes, but is simply a convention that developers and SRE/devops have to follow.
There are many more management and security topics in kubernetes that we can take a look at:
- White-list registries: can be done through the Open Policy Agent
- Kubernetes updates quarterly to a new version and maintains the past 4 versions with security updates
- OS updates