# 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 的話，也會建議使用 `sudo`、`gosu` 或 `su-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"]
```

這邊有兩個範例提供參考
- [NodeJS application example](https://github.com/Coffee-WIP/coffeewip-website/blob/master/Dockerfile)
- [efficient Python with Django multi-stage build](https://blog.ploetzli.ch/2020/efficient-multi-stage-build-django-docker/)

## Distroless, from scratch

使用最小所需的 base image。理想上，我們從頭開始建立 image，但只有 100% 二進制檔案也依舊可以運作。  
Google 所提供的 [Distroless](https://github.com/GoogleContainerTools/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，例如 `npm` 或 `go 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

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

## Build context and dockerignore

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

```
docker build -t app .
```

### 注意！

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

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

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

# 其他

## Layer sanity

要記得指令的順序在 Dockerfile 相當的重要。自從 `RUN`、`COPY`、`ADD`和其他指令將會建立一個新的 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 圖像規範中的預定義註釋](https://github.com/opencontainers/image-spec/blob/master/annotations.md)，這些註釋棄用了之前的 [Label 架構標準草案](http://label-schema.org/rc1/)。

## Linting

一些工具例如 [Haskell Dockerfile Linter(hadolint)](https://github.com/hadolint/hadolint) 可以偵測 Dockerfile 中不好的實踐，甚至是 `RUN` 指令中的操作。

## Locally scan images during development

Image scanning 是另一個你再啟動你的 container 之前偵測潛在問題的方式。可以按照 [image scanning best practices](https://sysdig.com/blog/image-scanning-best-practices/)，你應該要在每個 stage 的 image 生命週期中去 scanning。

# Image 建立之後

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

![Image Lifecycle](https://sysdig.com/wp-content/uploads/Dockerfile-best-practices-06-container-image-lifecycle.png)

## Docker port socket and TCP protection

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

## Sign images and verify signatures

在最佳實踐中，很重要的一塊就是使用 [docker content trust](https://docs.docker.com/engine/security/trust/)、Docker notary、Harbor notary 或是類似的工具對你的 **image 做數位簽證** 和**在 runtime 時驗證**。  
在每個 runtime 時啟用簽名驗證是不同的。例如：在 Docker 中可以設定環境變數的方式來啟用 `export DOCKER_CONTENT_TRUST=1` 

## Tag mutability

tag 是很容易做變動的，可以參考這篇 [Attack of the mutant tags](https://sysdig.com/blog/toctou-tag-mutability/)

## 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
- https://sysdig.com/blog/dockerfile-best-practices/
