乐趣区

关于docker:如何使用-Distroless-让你的容器更加安全

应用 Distroless 镜像来爱护 Kubernetes 上的容器。

容器扭转了咱们对待技术基础设施的形式。这是咱们运行应用程序形式的一次微小飞跃。容器编排和云服务一起为咱们提供了一种近乎有限规模的无缝扩大能力。

依据定义,容器应该蕴含 应用程序 及其 运行时依赖项。然而,在事实中,它们蕴含的远不止这些。规范容器根底映像蕴含规范 Linux 发行版中能够找到的包管理器、shell 和其余程序。

尽管这些都是构建容器镜像所必须的,但它们不应该成为最终镜像的一部分。例如,一旦你把包装置好了,就不再须要在容器中应用 apt 等包管理工具了。

这不仅使你的容器里充斥了不必要的软件包和程序,而且还为网络罪犯提供了攻打特定程序破绽的机会。

你应该始终理解容器运行时中存在什么,并且应该准确地限度其只蕴含应用程序所需的依赖项。

除了那些必要的,你不应该装置任何货色。一些当先的科技巨头,如谷歌,有多年在生产中运行容器的教训,曾经采纳了这种办法。

谷歌当初通过提供 Distroless 镜像向全世界凋谢这种能力。谷歌构建的这些镜像的指标是只蕴含你的应用程序及其依赖项,同时它们将没有惯例 Linux 发行版的所有个性,包含 shell

这意味着尽管能够想以前一样运行应用程序的容器,但不能在容器运行的时候进入容器内。这是一个重大的平安改良,因为你当初曾经为黑客通过 shell 进入你的容器关上了大门。

Distroless 根底镜像

谷歌为大多数风行的编程语言和平台提供了 Distroless 的根底镜像。

以下根底镜像是正式公布的版本:

  • Bazel 来构建容器映像,然而咱们能够应用 Docker 来做同样的事件。对于应用 Distroless 镜像的一个有争议的问题是:当咱们有一个 Distroless 镜像时,咱们如何应用 Dockerfile 来构建咱们的应用程序呢?

    通常,Dockerfile 以一个规范的 OS 根底镜像开始,而后是创立适当的运行时构建所需执行的多个步骤。这包含包的装置,为此须要像 aptyum 这样的包管理器。

    有两种办法:

    1. 先在 Docker 内部构建好你的应用程序,而后应用 Dockerfile 中的 ADD 或 COPY 指令将二进制包复制到容器中。
    2. 应用多阶段 Docker 构建。这是 Docker 17.05 及当前版本的一个新个性,它容许你将构建分为不同的阶段。第一阶段能够从规范的 OS 根底镜像开始,能够帮忙你构建应用程序;第二阶段能够简略地从第一阶段获取构建的文件并应用 Distroless 作为根底镜像。

    为了了解它是如何工作的,让咱们应用多阶段构建流程进行一个实际操作练习。

    必要条件

    你须要具备以下内容:

    • Docker 版本大于等于 17.05,用于构建镜像
    • 可选的 Kubernetes 集群用于实际练习的第二局部。如果你想在 Docker 中运行你的容器,你能够应用等价的 docker 命令。

    GitHub 代码仓

    作为实际练习,将 此代码仓 Fork 到你的 GitHub 帐号下,而后克隆 GitHub 代码仓并应用 cd 进入到我的项目目录下。

    该代码仓蕴含一个 PythonFlask 应用程序,当你调用 API 时,该应用程序会响应 Hello World!

    app.py 文件如下所示:

    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)

    Dockerfile 蕴含两个阶段:

    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"]

    构建阶段:

    • 从 python:2.7-slim 的根底镜像开始
    • 将应用程序复制到 /app 目录下
    • 降级 pip 并装置依赖

    Distroless 阶段:

    • 从 gcr.io/distroless/python2.7 的根底镜像开始
    • 将应用程序从构建阶段的 /app 目录复制到以后阶段的 /app 目录
    • 将 python 的 site-packages 从构建阶段复制到以后阶段的 site-packages 目录
    • 设置工作目录到 /app,将 python PATH 设置为 site-packages 目录,并裸露 5000 端口
    • 应用 CMD 指令运行 app.py

    因为 Disroless 镜像不蕴含 shell,所以应该在最初应用 CMD 指令。如果不这样做,Docker 将认为它是一个 shell CMD,并试图这样执行它,但这是不工作的。

    构建镜像:

    $ 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

    登录到 DockerHub 并推送镜像:

    docker login
    docker push <your_docker_repo>/flask-hello-world-distroless:latest

    登录到 DockerHub(或者你的公有镜像仓),你应该会看到容器镜像能够应用:

    如果你看一下压缩后的大小,它只有 23.36 MB。如果你应用 slim 发行版作为根底镜像,它将占用 56 MB。

    你曾经缩小了超过一半的容器占用空间。That’s amazing!

    在 Kubernetes 中运行容器

    为了测试构建是否无效,让咱们在 Kubernetes 集群中运行容器。如果你没有 Kubernetes,你能够运行等价的 Docker 命令来做雷同的流动,因为 Kubectl 和 Docker 命令是类似的。

    我在代码仓中创立了一个 kubernetes.yaml 文件,该文件蕴含应用咱们构建的镜像的 Deployment 和 负载平衡的 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

    这是一个非常简单的设置。负载均衡器监听端口 80 并映射到指标端口 5000。这些 Pods 在默认的 5000 端口上监听 Flask 应用程序。

    利用:

    $ kubectl apply -f kubernetes.yaml
    deployment.apps/flask-deployment created
    service/flask-service 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

    咱们看到存在两个 Pods、一个 Deployment、一个带有内部 IP 的 LoadBalancer 服务和一个 ReplicaSet

    让咱们拜访应用程序:

    $ curl http://35.184.113.120
    Hello World!

    咱们失去了 Hello World!。这表明 Flask 应用程序在失常工作。

    应用 Shell 对应用程序进行拜访

    正如我在引言中所形容的,Disroless 容器中没有 shell,因而不可能进入到容器内。然而,让咱们试着在容器中执行 exec:

    $ 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

    咱们无奈连贯到容器上。

    容器日志呢?如果拿不到容器日志,咱们就失去了调试应用程序的办法。

    让咱们试着去拿日志:

    $ 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 -

    所以容器日志是能够被获取到的!

    论断

    应用 Distroless 作为根底镜像是一种令人兴奋的爱护容器平安的形式。因为镜像小并且仅蕴含应用程序和依赖项,因而它为应用程序提供了最小的攻击面。它在更大程度上进步了应用程序的安全性,所以它是爱护容器平安的好办法。

    谢谢浏览!我心愿你喜爱这篇文章。

    原文链接

    本文翻译自 How to Harden Your Containers With Distroless Docker Images

退出移动版