Understanding a Containers Attack Surface
We will go over the following:
- Analyze the application
- Base image
- Vulnerabilities
- Package dependencies
Analyze the application
Before getting started with a deployment in Kubernetes, we first must understand the requirements or dependencies necessary for your application to function correctly. After that, we will structure the Kubernetes objects around the application to have only the functionality it needs to do its job. This is also known as the principle of least privilege and is a beneficial tactic to harden your containers.
Let’s take a look at the simpleservice application.
Input
cd ~/simpleservice/
cat Dockerfile
Output
FROM python:2.7-onbuild
MAINTAINER Michael Hausenblas
ENV REFRESHED_AT 2017-04-24T13:50
CMD [ "python", "./simpleservice.py" ]
Now the Dockerfile doesn’t tell us much about the application. This is probably because the variables have been abstracted away. When we deploy the service, the application will probably want a port to listen on.
Let’s check the application code to learn more about it.
Input
cat simpleservice.py
Output
#!/usr/bin/env python
...
Now it is a reasonably large output, but the core components are displayed at the top of the python application.
##############################################################################
# The following enviroment variables can be overridden
# to simulate different behaviour of the simple service:
# By default simple service serves on 9876 but you can
# make it listen on a different port by setting the env
# variable `PORT0` (also: it listens on all network interfaces,
# i.e. 0.0.0.0).
PORT = os.getenv('PORT0', 9876)
# By default simple service reports this value in
# the /endpoint0 unless overridden by below env variable.
VERSION = os.getenv('SIMPLE_SERVICE_VERSION', "0.5.0")
# By default the `/health` endpoint returns a HTTP code 200
# immediately but you can define the range with the following
# env variables.
#
# Examples:
# HEALTH_MIN=1000 HEALTH_MAX=2000 ... delays between 1 sec and 3 sec
# HEALTH_MAX=500 ... delays up to 0.5 sec
HEALTH_MIN = os.getenv('HEALTH_MIN', 0) # in milliseconds
HEALTH_MAX = os.getenv('HEALTH_MAX', 0) # in milliseconds
These are the variables that can be altered.
PORT
VERSION
HEALTH_MIN
HEALTH_MAX
With that in mind, let's make sure our container is as up-to-date and secure as possible.
Base image
This simple service is very old, which means many vulnerabilities may be associated with the base image. In the fast-paced container-centric world, we should aim to keep our base images as fresh and up to date as possible. The speed of our ability to upgrade also protects us from lingering issues that hackers will learn to exploit over time.
This Dockerfile uses the following image as its base image.
FROM python:2.7-onbuild
and if we build utilizing this base image, we get the following:
Input
podman build . --tag=simple-python-2.7
podman images
Output
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/simple-python 2.7 481db3a2b9f4 33 seconds ago 711 MB
docker.io/library/python 2.7-onbuild 3f246dd60a17 3 years ago 706 MB
Let's test out our service!
Input
podman run -P simple-python-2.7
Output
... INFO This is simple service in version v0.5.0 listening on port 9876 [at line 142]
Okay, we pulled our base image and built a simple containerized service. But the python:2.7-onbuild service was last updated three years ago. Let's see if we can find something more relevant.
Vulnerabilities
Having vulnerabilities inside your container image is typical and small vulnerabilities are hard to avoid. What we do want to avoid being critical and exploitable vulnerabilities. This can be done by scanning the container image and using verified scanned images for Red Hat or other providers.
To learn more about vulnerabilities, check out the OWASP foundations detailed breakdown of vulnerabilities
We can use the python:3.9 image to build our service. It has been scanned for vulnerabilities, and we get only have to make a few small code changes. You can view the package at DockerHub
First, we'll need to change the Dockerfile to be more explicit with what is happening in the container. At the same time, we do want to abstract most commands away to Kubernetes. The core components needed to run locally should still be present.
Input
cat <<EOF > Dockerfile
FROM python:3.9
MAINTAINER Michael Hausenblas
ENV REFRESHED_AT 2017-04-24T13:50
WORKDIR /app
RUN pip3 install tornado
COPY simpleservice.py simpleservice.py
CMD [ "python", "simpleservice.py" ]
EOF
Package dependencies
There are a few packages necessary for this application to run. Not only to we need to be aware to the default packages in the python3.9
image, but we should ensure that our package dependencies are not vulnerable as well.
For example, the tornado package that we will have to install. we can check the dependencies associaated with it online with a few tools from Stack Overflow or google insights.
[Take a look through the website(https://deps.dev/pypi/tornado/6.1/dependencies)] and find some packages you use regularly.
Next, we have to change four lines of code to bring them up to Python 3 compatibility. We are going to replace except Exception, e:
with except getopt.GetoptError as e:
sed -i 's/ except Exception, e:/ except getopt.GetoptError as e:/' simpleservice.py
And let's repeat this process to ensure our application still runs!
podman build . -t python-3.9
podman images
Output
REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/python-3.9 latest 82ae542a657d 1 second ago 947 MB
docker.io/library/python 3.9 1372f931a98b 5 days ago 936 MB
Let's test out our service :)
Input
podman run -P python-3.9
Output
... INFO This is simple service in version v0.5.0 listening on port 9876 [at line 142]
Awesome!
This simple act updated our base image over years of lacking CVE data into a new stable version of python. This option may not always be available to you, depending on your applications. But it, the core concept of verifying your base image and keeping up to date on vulnerabilities is certainly an achievable task.