Dockerfile best practices

A collection of all Dockerfile best practices

這邊會分幾個大項目來做分類

權限

Rootless containers

很基本的,你不應該使用 Root 的 User 來操作 entrypoint。所以建議一定要設定 USER 來避免使用 Root。並且在程式主要讀寫的檔案位置,給予適當的權限

FROM alpine:3.12
# Create user and set ownership and permissions as required
RUN adduser -D myuser && chown -R myuser /myapp-data
# ... copy application files
USER myuser
ENTRYPOINT ["/myapp"]

如果 container 需要使用到 root 的話,也會建議使用 sudogosusu-exec 等工具。

Don't bind to a specific UID

不要替 non-root user 設定特定的 UID。設定特定的 UID 會需要調整所有綁定的權限,這可能會造成檔案讀取的錯誤。不要用 hardcore 的方式,需要有彈性的處理。

Make executables owned by root and not writable

建議在 container 中所有可執行的檔案都由 root 所擁有,即使是由 non-root user 來執行的檔案。另外也不應該具有可寫的權限。

...
WORKDIR $APP_HOME
COPY --chown=app:app app-files/ /app
USER app
ENTRYPOINT /app/my-app-entrypoint.sh

以此範例下,app user 不過需要擁有權,只需要執行權而已。所以 COPY ... 這段就完全不需要。

減少攻擊範圍

此主題主要目標是要盡量的減少 image 大小,避免包含到不必要的 packages 或者開放 port,來增加可被攻擊的範圍。

Multistage builds

使用 multistage build 會建立一個中間 image(或者 stage),其中包含了所有所需用來編譯或是生產你最終產物(例如:最後的執行檔)的工具。然後你再複製其最終產物到 final stage,其中不包含額外的開發相依套件、暫時檔等。
最後的 image 將只會留下所需的檔案,而不會留下暫存檔案、編譯時所需的相依套件。這樣可以大大減少可被攻擊的範圍,同時也能降低 image 的大小。

# This is the "builder" stage
FROM golang:1.15 as builder
WORKDIR /my-go-app
COPY app-src .
RUN GOOS=linux GOARCH=amd64 go build ./cmd/app-service

# This is the final stage, and we copy artifacts from "builder"
FROM gcr.io/distroless/static-debian10
COPY --from=builder /my-go-app/app-service /bin/app-service
ENTRYPOINT ["/bin/app-service"]

這邊有兩個範例提供參考

Distroless, from scratch

使用最小所需的 base image。理想上,我們從頭開始建立 image,但只有 100% 二進制檔案也依舊可以運作。
Google 所提供的 Distroless 會是個不錯的選擇。這主要被設計用來只包含最小的 library (例如:glibc, libssl, openssl) 來跑 Go、Python 和其他的框架

Use trusted base images

  • 應該要使用可信任來源 repositories 認證和官方的 image
  • 當使用客制的 image 時,確認其來源以及 Dockerfile。並且建立你自己的基礎 image。因為無法保證從 public registry 是從給予的 Dockerfile 建立來的,也不能保證是最新的。
  • 有時候在安全性和極簡主義下,官方 image 可能並不符合。

Update your images frequently

要保持你的基礎 image 頻繁的更新,新的安全漏洞會持續的被發現。所以要保持更新這些安全性的修正,但是並不是說需要得次都更新到最新的版本,因為可能會包含一些重大的更改,但需要定義下更新的策略:

  • 堅持使用穩定或長期支援的版本,這些版本會很快並經常提供安全性的修復
  • 提前計畫:準備好在你的基礎 image 達到其生命週期結束並停止更新之前刪除舊版本並更新
  • 定期重建你的 image,並使用類似的策略來更新 package,例如 npmgo mod

Exposed ports

任何開放的 port,都是一個可直接攻擊你系統的大門。所以要只開放你應用程式所需的 port,並且避免開放 SSH(22)。
請注意,即使 Dockerfile 有提供 EXPOSE。但是這指令只是用於說明、標示的用途,並不會自動允許其 EXPOSE 指定的 port 可以連線。

避免機密資料外流

Credentials and confidentiality

不要將任何的密碼或機密資料放到 Dockerfile 的指令(環境變數、args 或是 hardcode 到指令)
另外要特別注意,不要將其機密檔案複製到 image 中,即使後來被指令刪除也一樣。因為依舊可以在之前的 layer 中被讀取,它並沒有真正的刪除,只是對於最終的檔案系統被隱藏起來。

  • 如果應用程式支援從環境變數來設定配置,則會建議在執行時設定其密碼,或是使用 Docker secrets, Kubernetes secrets 提供其值到環境變數。
  • 使用設定檔並且建立掛載到 docker 中的設定檔,或是從 kubernetes secret 中掛載

應該要讓應用程式可以在 runtime 時可以客製化的 injecting 值到特定的密碼。

ADD vs COPY

ADDCOPY 基本上提供相同的功能,但是 COPY 更為明確。
建議盡量使用 COPY,除非有額外需要使用 ADD,像是需要從 URL 來新增檔案或是從壓縮檔(因為 ADD 會自動解壓縮)。 但是一般情況下,也會比較常使用 RUN 來代替 ADD 做這些操作。

Build context and dockerignore

這是使用 docker 執行建構時的典型操作,使用預設 Dockerfile 和當前目錄中的上下文:

docker build -t app .

注意!

使用 . 這個參數是架構的內容,這會是相當危險的。因為可能會複製了機密資料或是任何非必要的檔案到 image。

會建議使用一個子目錄,將所需的東西放在裡面。
此外也建議可以建立一個 .dockerignore 來明確指定哪些檔案或目錄不要放到 image 中。

將你的建構內容放到你自己的目錄,並使用 .dockerignore 來減少各種放入非必要檔案的可能

其他

Layer sanity

要記得指令的順序在 Dockerfile 相當的重要。自從 RUNCOPYADD和其他指令將會建立一個新的 image layer,結合多個指令再一起可以減少數個 layers

原本:

FROM ubuntu
RUN apt-get install -y wget
RUN wget https://…/downloadedfile.tar
RUN tar xvzf downloadedfile.tar
RUN rm downloadedfile.tar
RUN apt-get remove wget

優化後:

FROM ubuntu
RUN apt-get install wget && \
     wget https://…/downloadedfile.tar && \
     tar xvzf downloadedfile.tar && \
     rm downloadedfile.tar && \
     apt-get remove wget

並且將較不會頻繁更改的指令,放置更前面,便於 cache

原本:

FROM ubuntu
COPY source/* .
RUN apt-get install nodejs
ENTRYPOINT ["/usr/bin/node", "/main.js"]

優化後:

FROM ubuntu
RUN apt-get install nodejs
COPY source/* .
ENTRYPOINT ["/usr/bin/node", "/main.js"]

Metadata labels

Label 有助於 image 的維運和管理,像是應用程式的版本、網站的連結、維護者的聯絡方式等

您可以查看 OCI 圖像規範中的預定義註釋,這些註釋棄用了之前的 Label 架構標準草案

Linting

一些工具例如 Haskell Dockerfile Linter(hadolint) 可以偵測 Dockerfile 中不好的實踐,甚至是 RUN 指令中的操作。

Locally scan images during development

Image scanning 是另一個你再啟動你的 container 之前偵測潛在問題的方式。可以按照 image scanning best practices,你應該要在每個 stage 的 image 生命週期中去 scanning。

Image 建立之後

此前,我們都專注在 image 建構的過程。在此將專注在建構之後,啟動的部分

Image Lifecycle

Docker port socket and TCP protection

確認 /var/run/docker.sock 有正確的權限,而且如果 docker 有透過 TCP 開放(完全不建議),請確認有適當的保護

Sign images and verify signatures

在最佳實踐中,很重要的一塊就是使用 docker content trust、Docker notary、Harbor notary 或是類似的工具對你的 image 做數位簽證在 runtime 時驗證
在每個 runtime 時啟用簽名驗證是不同的。例如:在 Docker 中可以設定環境變數的方式來啟用 export DOCKER_CONTENT_TRUST=1

Tag mutability

tag 是很容易做變動的,可以參考這篇 Attack of the mutant tags

Run as non root

雖然在 Dockerfile 中我們可以設定 non-root user,但是其執行環境(docker run 或 Kubernetes),有最終的決定權。所以要避免其執行環境使用的到 root user。

Include health / liveness checks

盡量在 Dockerfile 中包含 HEALTHCHECK 指令。如果是在 Kubernetes 的話,會建議設定 livenessProbe,因為 docker 的 HEALTHCHECK 將不會被執行

Drop capabilities

在運行中,您可以使用 Docker 中的 --cap-drop 或 Kubernetes 中的 securityContext.capabilities.drop應用程序功能限制為所需的最小集合。 這樣,萬一您的容器遭到破壞,攻擊者可採取的行動範圍就會受到限制。

References

Did you find this article valuable?

Support 攻城獅 by becoming a sponsor. Any amount is appreciated!