幾個小技巧,讓你寫出更安全的 Dockerfile

Larry Lu
Starbugs Weekly 星巴哥技術專欄
9 min readSep 4, 2021

--

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

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

這篇文主要著重在 Dockerfile 的安全性,所以不會介紹基本語法,若是還沒有寫過 Dockerfile 的話可以先讀完我之前寫的「一步一步帶你 dockerize 你的應用」,再回來繼續往下會比較看得懂哦~

使用 stable 或 LTS 的 base image

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

但這樣可能會在哪次 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

這樣做一來是可以把版號固定在 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 把原本的套件裝回來

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

只 COPY 需要的東西

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

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

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

而且這樣還有另外一個好處,就是如果你改了 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 是一定要的,沒有他們就無法進行編譯

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

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

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

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

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

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

如果說 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 了)

先用 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 更安全,那就連這些小細節都不能放過

最後順便宣傳一下,如果對其他資安的東西有興趣,我今年參加鐵人賽的主題是「從以卵擊石到堅若磐石之 Web API 安全性全攻略」,而除了我之外,我們 Starbugs 團隊的其他人也有報名 Web 跟 DevOps 相關的主題,快趁現在到 StarBugs 團隊頁面 訂閱一下,才不會錯過我們寫的文章哦~

延伸閱讀

--

--

Larry Lu
Starbugs Weekly 星巴哥技術專欄

我是 Larry 盧承億,傳說中的 0.1 倍工程師。我熱愛技術、喜歡與人分享,專長是 JS 跟 Go,平常會寫寫技術文章還有參加各種技術活動,歡迎大家來找我聊聊~