讓你寫出更安全的 Dockerfile

讓你寫出更安全的 Dockerfile

自從進入大容器時代後,Docker、K8s 已經逐漸成為開發、測試及部署時不可或缺的工具,如果突然叫我不要用 Docker,那我可能什麼都做不了,但也因為這樣,跟容器有關的攻擊越來越普遍,因此容器的安全性也越來越重要

而想要從零開始建出一個容器,第一步就是要寫 Dockerfile 把你的應用包裝成 Docker image。關於怎麼產生出盡量小的 image 已經很多人寫過了,所以今天想要跟大家分享的是想要寫出一個安全的 Dockerfile,有哪些該注意的地方。

使用 stable 或 LTS 的 base image

很多人在寫 Dockerfile 並不會特別指定 base image 的版本(就懶啊,我懂 XD),譬如說想要包一個 Node API server,就直接寫 FROM node 或是 FROM node:latest

# BadFROM node

WORKDIR /appCOPY . .

RUN npm installRUN npm run test

bad.Dockerfile hosted with by GitHub

但這樣可能會在哪次 build image 時就意外從 Node 14 升到 Node 16,導致部分功能直接壞掉。而且最新版本的 Node 可能有一些不為人知的 bug,需要有一些勇者去幫忙踩坑,所以除非是自己的 Side Project 想要玩玩看最新的 feature,否則直接把最新版本的 Node 用在 production 並不是個好作法

比較好的方式是先看看 Node 的 LTS(Long-Term Support) 版本是多少,像我在寫文章的當下是 v14.17.5,那就選擇 node:14 或是 node:14.17 作為 base image。

# GoodFROM node:14

WORKDIR /appCOPY . .

RUN npm installRUN npm run test

這樣做一來是可以把版號固定在 Node 14、確保不會有大變動,二來是 LTS 的版本都會不斷推出 security fix,因此如果哪天 Node 更新到 v14.17.10,那些 security fix 也會在部署時被加進的 API server,我們只要坐等更新就好了

安裝套件時要指定版本

這點跟上面提到的不要用 latest image 有些類似,不管你是用 apt-get install 安裝 CLI 工具、用 npm install 裝函式庫、還是用 curl/wget 把東西下載回來編譯,都要盡量確保每次下載到的東西是一樣的

譬如在用 apt-get 安裝 nginx 時就可以透過 apt-get install nginx=1.14.0 來下載指定版本(有點麻煩對吧,我也覺得XD),而 npm、pip 這類的語言套件管理工具則是看官方推薦什麼方法,像 npm 就是用 package-lock.json 來鎖定套件的版本、pip 的話則是先跑 pip freeze > requirements.txt 把套件的版本凍起來,等要安裝時再跑 pip install -r requirements.txt 把原本的套件裝回來。

# GoodFROM ubuntu:20.04

RUN apt-get updateRUN apt-get install nginx=1.14.0 python=2.7.15 nodejs=12.18.2

雖然把套件版本的鎖定之後可以省下很多麻煩,但也不能一直鎖在那都不更新,所以記得偶爾去檢查一下版本是不是太舊了,如果太舊再手動把版號升上去就好了~

只 COPY 需要的東西

平常在寫 Dockerfile 時,有些人為了一時方便,會直接用 COPY . /app 把整個專案資料夾複製到 container 的 /app 資料夾內。這樣不用動太多腦筋,在 container 裡面也可以直接存取到所有檔案,但這樣的做法可以說是糟透了。

FROM node:14WORKDIR /app

# Very BADCOPY . .

RUN npm installRUN npm run testCMD [“node”, “index.js”]

首先是這樣會讓 image 變得很肥(連 node_modules 都進去了能不肥嗎XD),而且一不小心就會把 .envrc 這類敏感資料一起放進去,如果哪天這個 image 被駭客拿到,裡面的 AWS 憑證、資料庫密碼等等超機密資料就會直接外洩出去,哪天突然被刪庫也是有可能的

為了避免這種事情發生,在 build image 時應該只把需要的東西複製進去,譬如說你馬上就要跑 npm install,這才把 package.json 跟 package-lock.json 放進去,而程式碼也是把真的會跑到的那些放進去就好。

FROM node:14WORKDIR /app

# GoodCOPY package.json package-lock.json ./RUN npm install

# GoodCOPY src/ index.js ./RUN npm run testCMD [“node”, “index.js”]

而且這樣還有另外一個好處,就是如果你改了 src 裡面的程式碼,但沒有安裝新的 package(開發時大部分都是這樣吧~),因為 Docker 會自動做 cache,所以就不需要重新跑一次 npm install,會直接從第 10 行的 npm run test 開始跑,因此可以大幅縮減需要等待的時間

用 multi-stage build 捨棄不需要的檔案

這跟上一點有點類似,簡單來說就是不要留任何不需要的東西在 image 裡面(沒用的東西都給我滾),即便那是 build image 過程中產生的東西也是一樣

譬如說原本 Go API server 的 Dockerfile 可能長這樣,因為要在 build image 時編譯出執行檔,所以第 5 行的 COPY *.go 是一定要的,沒有他們就無法進行編譯。

FROM golang:1.17

WORKDIR /app

COPY *.go go.mod go.sum ./RUN go build -o main

CMD [“./main”]

但說真的一旦編譯出執行檔之後,那些 Go 程式碼就用不到了,所以應該來個爽快的過河拆橋,用 multi-stage build 把編譯完的執行檔保留下來就好,程式碼什麼的就直接拜拜

像下面這個這個 Dockerfile 經過 multi-stage build 後 image 裡面就只有 /app/main 這個編譯好的執行檔,沒有任何程式碼以及 Go 的編譯器,非常的存粹。

# compile stageFROM golang:1.17 as compile-envWORKDIR /appCOPY *.go go.mod go.sum ./RUN go build -o main

# final stageFROM gcr.io/distroless/baseCOPY –from=compile-env /app/main /appCMD [“/app/main”]

那這樣有什麼好處呢?除了 image 可以變小很多之外,即便 image 被駭客拿到了,程式碼也不會外流出去(有很多攻擊都是拿到程式碼後從裡面找到漏洞),因此只保留執行檔可以提高安全性

除此之外,因為環境越複雜就越可能有沒發現的漏洞,而把 base image 從原本的 golang:1.17 換成 Google 提供的 distroless image 剛好可以大幅減少環境的複雜度(distroless 幾乎沒裝什麼東西,連 shell 都沒有),也就可以提高安全性

不要把敏感資料 hardcode 在 Dockerfile 裡面

我想這已經是常識等級的安全知識了,因為直接把敏感資料用 ENV 寫在 Dockerfile 裡會讓駭客輕易拿到(只要拿到 image 就可以了),所以絕對不要想不開把資料庫或任何的帳號密碼寫在裡面,ENV 頂多用來設定時區或是 NODE_ENV 這種被看光也不會出事的變數就好,不然哪天資料被偷走真的會哭出來。

FROM node:14

ENV TZ=Asia/TaipeiENV NODE_ENV=production

# Very BadENV PG_HOST=test.postgresql.comENV PG_USER=thisIsmyUserNameENV PG_PASS=mySecretPa55w0rdENV PG_DBNAME=projectName

COPY . .RUN npm installCMD [“node”, “index.js”]

如果說 ENV 不能放敏感資料,那這些資料究竟要怎麼被加進環境變數呢?

答案就是在 docker run 時加上 –env 或是 –env-file 把環境變數塞進去;如果是用 docker-compose 的話,則是把那些資料寫進 docker-compose.yml 的 environment 裡面,這樣 container 啟動時就會讀到這些變數,而且即便 image 被偷走也不用擔心資料外洩

弱點掃描

可以做的事情都做了之後,最後就是要來用工具來做弱點掃描了。因為做弱描的工具還滿多的,這邊就介紹已經被 Docker 加進 CLI 的 Snyk,他可以把你的環境、安裝的套件丟到他們資料庫去做搜尋,看有哪些潛在的危險

譬如說我手邊有幾個多年前用 Node.js 寫的 express server,Dockerfile 長這樣(看到 9.2.0 就知道真的是多年前XD,現在都已經 Node 16 了)。

FROM node:9.2.0

COPY index.js package.json /app/WORKDIR /appRUN npm install && npm cache clean –force

CMD node index.js

先用 docker build . -t app 把 image 建出來後,接著就下 docker scan app 對他做掃描。因為我用的是古早古早以前的 node:9.2.0,所以光 Base Image 的部分掃出來就有 1039 個漏洞,而且其中 99 是屬於 critical 等級的,嚇都嚇死

除了告訴你有多少漏洞之外,他還會把每個漏洞給列出來(有興趣可以去讀一下那些漏洞的報告,其實都不長),並且告訴你那些漏洞分別在哪些版本修掉了

如果懶得看那些漏洞的話,也可以直接滑到最底下看他給你的建議,譬如這邊他就建議把 base image 升級到 node:16.7.0,若是不一定要 full image 的話,那 node:16-bullseye-slim 也是不錯的選項,因為安裝的東西更少,所以漏洞自然也更少。

Debian bullseye 是今年八月剛發佈的版本,可能比較不穩定

總結

今天介紹了一些在寫 Dockerfile 時的注意事項,雖然很多都是小地方,但畢竟魔鬼藏在細節裡,想要讓你的 Docker image 更安全,那就連這些小細節都不能放過。