Use the Distroless
image to protect the container on Kubernetes
Containers have changed the way we look at technical infrastructure. This is a huge leap forward in the way we run applications. Together, container orchestration and cloud services provide us with a near-infinite scale of seamless scalability.
By definition, the container should contain the application and its runtime dependency . However, in reality, they contain far more than these. The standard container base image contains the package manager, shell
and other programs Linux
Although these are necessary to build the container image, they should not be part of the final image. For example, once you install the package, you no longer need to use package management tools such as apt
This not only makes your container full of unnecessary software packages and programs, but also provides cybercriminals with the opportunity to attack specific program vulnerabilities.
You should always understand what is in the container runtime, and you should precisely limit it to only the dependencies required by the application.
You should not install anything except those necessary. Some leading technology giants, such as Google, have years of experience in running containers in production and have already adopted this approach.
Google is now opening up this capability to the world Distroless
The goal of these images built by Google is to only contain your application and its dependencies, and they will not have all the features of the Linux
shell
.
This means that although you can run the container of the application as before, you cannot enter the container while the container is running. This is a major security improvement, because you have now closed the door shell
Distroless base image
Google provides a base image Distroless
for most popular programming languages and platforms.
The following basic images are officially released versions:
- gcr.io/distroless/static-debian10
- gcr.io/distroless/base-debian10
- gcr.io/distroless/java-debian10
- gcr.io/distroless/cc-debian10
- gcr.io/distroless/nodejs-debian10
The following basic images are still in the experimental stage and are not recommended for production environments:
- gcr.io/distroless/python2.7-debian10
- gcr.io/distroless/python3-debian10
- gcr.io/distroless/java/jetty-debian10
- gcr.io/distroless/dotnet
Build Distroless image
Google uses Bazel internally to build container images, but we can use Docker
to do the same thing. A controversial question about using the Distroless
image is: when we have a Distroless
image, how do we use Dockerfile
to build our application?
Usually, Dockerfile
with a standard OS base image, followed by the multiple steps required to create a proper runtime build. This includes the installation of packages, for which a package manager apt
or yum
There are two ways:
- First in
Docker
external build up your application, and then useDockerfile
in the ADD or COPY binary package to the container instructions. - Use multi-stage
Docker
build. This is a new feature of Docker 17.05 and later, which allows you to divide the build into different stages. The first stage can start from the standard OS base image, which can help you build applications; the second stage can simply get the built files from the first stage and useDistroless
as the base image.
In order to understand how it works, let's use a multi-stage build process for a hands-on exercise.
Necessary condition
You need to have the following:
- Docker version is greater than or equal to 17.05, used to build the image
- The optional
Kubernetes
cluster is used for the second part of the practice exercise. If you wantDocker
, you can use the equivalentdocker
command.
GitHub code warehouse
As a practical exercise, code warehouse to your GitHub account, then clone the GitHub code warehouse and use cd
enter the project directory.
The code warehouse contains a Python
application of Flask
. When you call the API, the application will respond to Hello World!
.
app.py
file is as follows:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
The Dockerfile contains two stages:
FROM python:2.7-slim AS build
ADD . /app
WORKDIR /app
RUN pip install --upgrade pip
RUN pip install -r ./requirements.txt
FROM gcr.io/distroless/python2.7
COPY --from=build /app /app
COPY --from=build /usr/local/lib/python2.7/site-packages /usr/local/lib/python2.7/site-packages
WORKDIR /app
ENV PYTHONPATH=/usr/local/lib/python2.7/site-packages
EXPOSE 5000
CMD ["app.py"]
Construction phase:
- Start from the base image of python:2.7-slim
- Copy the application to the /app directory
- Upgrade
pip
and install dependencies
Distroless stage:
- Start from the base image of gcr.io/distroless/python2.7
- Copy the application from the /app directory in the build phase to the /app directory in the current phase
- Copy python's
site-packages
from the build stage to thesite-packages
directory of the current stage - Set the working directory to /app, set the python PATH to the
site-packages
directory, and expose port 5000 - Use
CMD
instruction to runapp.py
Since the Disroless
image does not contain shell
CMD
instruction should be used at the end. If you don't do this, Docker will think it is a shell CMD and try to execute it like this, but this will not work.
Build the image:
$ docker build -t <your_docker_repo>/flask-hello-world-distroless .
Sending build context to Docker daemon 95.74kB
Step 1/12 : FROM python:2.7-slim AS build
---> eeb27ee6b893
Step 2/12 : ADD . /app
---> a01dc81df193
Step 3/12 : WORKDIR /app
---> Running in 48ccf6b990e4
Removing intermediate container 48ccf6b990e4
---> 2e5e335be678
Step 4/12 : RUN pip install --upgrade pip
---> Running in 583be3d0b8cc
Collecting pip
Downloading pip-20.1.1-py2.py3-none-any.whl (1.5 MB)
Installing collected packages: pip
Attempting uninstall: pip
Found existing installation: pip 20.0.2
Uninstalling pip-20.0.2:
Successfully uninstalled pip-20.0.2
Successfully installed pip-20.1.1
Removing intermediate container 583be3d0b8cc
...................................
Successfully installed Jinja2-2.11.2 MarkupSafe-0.23 click-7.1.2 flask-1.1.2 itsdangerous-0.24 werkzeug-1.0.1
Removing intermediate container c4d00b1abf4a
---> 01cbadcc531f
Step 6/12 : FROM gcr.io/distroless/python2.7
---> 796952c43cc4
Step 7/12 : COPY --from=build /app /app
---> 92657682cdcc
Step 8/12 : COPY --from=build /usr/local/lib/python2.7/site-packages /usr/local/lib/python2.7/site-packages
---> faafd06edeac
Step 9/12 : WORKDIR /app
---> Running in 0cf545aa0e62
Removing intermediate container 0cf545aa0e62
---> 4c4af4333209
Step 10/12 : ENV PYTHONPATH=/usr/local/lib/python2.7/site-packages
---> Running in 681ae3cd51cc
Removing intermediate container 681ae3cd51cc
---> 564f48eff90a
Step 11/12 : EXPOSE 5000
---> Running in 7ff5c073d568
Removing intermediate container 7ff5c073d568
---> ccc3d211d295
Step 12/12 : CMD ["app.py"]
---> Running in 2b2c2f111423
Removing intermediate container 2b2c2f111423
---> 76d13d2f61cd
Successfully built 76d13d2f61cd
Successfully tagged <your_docker_repo>/flask-hello-world-distroless:latest
Log in to DockerHub and push the image:
docker login
docker push <your_docker_repo>/flask-hello-world-distroless:latest
Log in to DockerHub (or your private mirror warehouse), you should see that the container image is available:
If you look at the compressed size, it is only 23.36 MB. If you use the slim
release as the base image, it will take up 56 MB.
You have reduced the container footprint by more than half. That's amazing!
Run containers in Kubernetes
To test whether the build works, let's run the container in the Kubernetes cluster. If you don't have Kubernetes, you can run equivalent Docker commands to do the same activities, because Kubectl and Docker commands are similar.
I created a file in the code kubernetes.yaml warehouse, which included the use of our built mirrored Deployment
and load balancing Service
.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-deployment
spec:
selector:
matchLabels:
app: flask
replicas: 2
template:
metadata:
labels:
app: flask
spec:
containers:
- name: flask
image: bharamicrosystems/flask-hello-world-distroless
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: flask-service
spec:
selector:
app: flask
ports:
- port: 80
targetPort: 5000
type: LoadBalancer
This is a very simple setup. The load balancer listens on port 80 and maps to the target port 5000. These Pods listen to the Flask application on the default port 5000.
application:
$ kubectl apply -f kubernetes.yaml
deployment.apps/flask-deployment created
service/flask-service created
Let's take a look at all the resources and see what we have created:
$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/flask-deployment-576496558b-hnbxt 1/1 Running 0 47s
pod/flask-deployment-576496558b-hszpq 1/1 Running 0 73s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/flask-service LoadBalancer 10.8.9.163 35.184.113.120 80:31357/TCP 86s
service/kubernetes ClusterIP 10.8.0.1 <none> 443/TCP 26m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/flask-deployment 2/2 2 2 88s
NAME DESIRED CURRENT READY AGE
replicaset.apps/flask-deployment-576496558b 2 2 2 89s
We see that there are two Pods
, one Deployment
LoadBalancer
service with an external IP, and one ReplicaSet
.
Let's access the application:
$ curl http://35.184.113.120
Hello World!
We got Hello World!
. This indicates that the Flask application is working properly.
Use Shell to access applications
As I described in the introduction, Disroless
is no shell
060dbc83eb63dc container, so it is impossible to enter the container. However, let's try to execute exec in the container:
$ kubectl exec -it flask-deployment-576496558b-hnbxt /bin/bash
OCI runtime exec failed: exec failed: container_linux.go:349: starting container process caused "exec: \"/bin/bash\": stat /bin/bash: no such file or directory": unknown
command terminated with exit code 126
We cannot connect to the container.
What about container logs? If we can't get the container log, we lose the way to debug the application.
Let's try to get the log:
$ kubectl logs flask-deployment-576496558b-hnbxt
* Running on http://0.0.0.0:5000/
* Restarting with reloader
10.128.0.4 - - [31/May/2020 13:40:27] "GET / HTTP/1.1" 200 -
10.128.0.3 - - [31/May/2020 13:42:01] "GET / HTTP/1.1" 200 -
So container logs can be obtained!
in conclusion
Using Distroless
as the base image is an exciting way to protect container security. Since the image is small and contains only the application and dependencies, it provides the smallest attack surface for the application. It improves the security of the application to a greater extent, so it is a good way to protect the security of the container.
thanks for reading! I hope you enjoy this article.
Original link
This article is translated from How to Harden Your Containers With Distroless Docker Images
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。