如何使用 Distroless 讓你的容器更加安全

使用 Distroless 鏡像來保護 Kubernetes 上的容器。python

容器改變了咱們看待技術基礎設施的方式。這是咱們運行應用程序方式的一次巨大飛躍。容器編排和雲服務一塊兒爲咱們提供了一種近乎無限規模的無縫擴展能力。linux

根據定義,容器應該包含 應用程序 及其 運行時依賴項。然而,在現實中,它們包含的遠不止這些。標準容器基礎映像包含標準 Linux 發行版中能夠找到的包管理器、shell 和其餘程序。git

雖然這些都是構建容器鏡像所必需的,但它們不該該成爲最終鏡像的一部分。例如,一旦你把包安裝好了,就再也不須要在容器中使用 apt 等包管理工具了。github

這不只使你的容器裏充滿了沒必要要的軟件包和程序,並且還爲網絡罪犯提供了攻擊特定程序漏洞的機會。docker

你應該始終了解容器運行時中存在什麼,而且應該精確地限制其只包含應用程序所需的依賴項。shell

除了那些必要的,你不該該安裝任何東西。一些領先的科技巨頭,如谷歌,有多年在生產中運行容器的經驗,已經採用了這種方法。編程

谷歌如今經過提供 Distroless 鏡像向全世界開放這種能力。谷歌構建的這些鏡像的目標是隻包含你的應用程序及其依賴項,同時它們將沒有常規 Linux 發行版的全部特性,包括 shellflask

這意味着雖然能夠想之前同樣運行應用程序的容器,但不能在容器運行的時候進入容器內。這是一個重大的安全改進,由於你如今已經爲黑客經過 shell 進入你的容器關上了大門。api

Distroless 基礎鏡像

谷歌爲大多數流行的編程語言和平臺提供了 Distroless 的基礎鏡像。安全

如下基礎鏡像是正式發佈的版本:

  • Bazel 來構建容器映像,可是咱們可使用 Docker 來作一樣的事情。關於使用 Distroless 鏡像的一個有爭議的問題是:當咱們有一個 Distroless 鏡像時,咱們如何使用 Dockerfile 來構建咱們的應用程序呢?

    一般,Dockerfile 以一個標準的 OS 基礎鏡像開始,而後是建立適當的運行時構建所需執行的多個步驟。這包括包的安裝,爲此須要像 aptyum 這樣的包管理器。

    有兩種方法:

    1. 先在 Docker 外部構建好你的應用程序,而後使用 Dockerfile 中的 ADDCOPY 指令將二進制包複製到容器中。
    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

相關文章
相關標籤/搜索