構建安全可靠、最小化的 Docker 鏡像

容器化部署愈來愈多的應用於企業的生產環境中,如何構建安全可靠、最小化Docker 鏡像也就愈來愈重要。本文將針對該問題,經過原理加實踐的方式,從頭到腳幫你擼一遍。文章比較長,主要經過五個部分對容器鏡像進行講解。分別是:html

  • 鏡像的構建
    講解了鏡像的手動構建與自動構建過程。
  • 鏡像的存儲與UnionFS聯合文件系統
    講解了鏡像的分層結構以及UnionFS聯合文件系統,以及鏡像層在UnionFS上的實現。
  • 最小化鏡像構建
    講解了爲何須要最小化鏡像,同時如何進行最小化構建。
  • 容器鏡像的加固
    容器鏡像加固的具體方式。
  • 容器鏡像的審查
    高質量的項目中容器鏡像也須要向代碼同樣進行審查。

讀者能夠根據各自狀況選擇性閱讀。原文發佈在個人我的站點: GitDiG.com. 原文鏈接:構建安全可靠、最小化的 Docker 鏡像.node

1. 構建鏡像

1.1 手動構建

手動構建 Docker 鏡像的流程圖,以下:git

如今依次按照流程採用命令行的方式手動構建一個簡單的Docker 鏡像。github

1.1.1 建立容器並增長文件

busybox做爲本次試驗的基礎鏡像,由於它足夠小,大小才 1.21MBgolang

$: docker run -it busybox:latest sh
/ # touch /newfile
/ # exit
複製代碼

經過以上的操做,咱們完成了流程圖的前三步。建立了一個新容器,並在該容器上建立了一個新問題。只是,咱們退出容器後,容器也不見了。固然容器不見了,並不表示容器不存在了,Docker 已經自動保存了該容器。若是在建立時,未顯示設置容器名稱,能夠經過如下方式查找該消失的容器docker

# 列出最近建立的容器
$: docker container ls -n 1
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
c028c091f964        busybox:latest      "sh"                13 minutes ago      Exited (0) 27 seconds ago                       upbeat_cohen

# 查詢容器的詳情
$: docker container inspect c028c091f964
...
複製代碼

1.1.2 提交變動生成鏡像

手動構建鏡像,很簡單。先找到發生變動的容器對象,對其變動進行提交。提交完成後,鏡像也就生成了。不過此時的鏡像只有一個自動生成的序列號惟一標識它。爲了方便鏡像的檢索,須要對鏡像進行命名以及標籤化處理。安全

命令行操做以下:bash

# 提交變動, 構建鏡像完成
$: docker commit -a JayL -m "add newfile" c028c091f964
sha256:01603f50694eb62e965e85cae2e2327240e4a68861bd0e98a4fb4ee27b403e6d

# 對鏡像進行命名, 原鏡像ID取前幾位就能夠了
$: docker image tag 01603f50694eb62e9 busybox:manual

# 驗證新鏡像
$: docker run busybox:manual ls -al newfile
-rw-r--r--    1 root     root             0 Jun 15 05:25 newfile
複製代碼

經過以上兩步過程就完成了Docker 鏡像手動建立。很是簡單是否是。可是也很是麻煩,必須先建立新容器在提交變動,生成鏡像。整個過程徹底能夠經過腳本化處理,這也是下節要說的,自動化構建Docker 鏡像。網絡

1.2 自動化構建

1.2.1 Dockerfile 構建

自動化構建Docker 鏡像,Docker公司提供的不是SHELL腳本的方式,而是經過定義一套獨立的語法來描述整個構建過程, 經過該語法編輯的文件,稱爲 Dockerfile。 自動化構建鏡像就是經過編寫Dockerfile文件構建的。app

一樣完成上面的工做,用Dockerfile寫出來就是:

FROM busybox:latest
RUN touch /newfile 複製代碼

至於更加詳細的Dockerfile語法,請參見官方指南

完成Dockerfile編寫後,經過命令觸發構建。整個過程,腳本化出來就是:

$: mkdir autobuild && cd autobuild
$: cat <<EOF > Dockerfile
FROM busybox:latest
RUN  touch /newfile
EOF
$: docker build -t busybox:autobuild .
複製代碼

2 鏡像的存儲

2.1 鏡像的組成

Docker 鏡像是由一組只讀的鏡像層Image Layer組成的。而Docker 容器則是在Docker 鏡像的基礎之上,增長了一層:容器層Container Layer。容器層Container Layer可讀寫的。若是對該容器層Container Layer進行commit提交操做,該層就變成了新的鏡像層Image Layer。新的Docker Image也就構建出來了。

$: mkdir layer && cd layer && touch newfile
$: cat <<EOF > Dockerfile
FROM scratch
ADD  newfile .
EOF
$: docker build -t layer .
複製代碼

如下官網提供的圖示能夠很清楚的看出鏡像與容器之間的聯繫與區別:

具體某個鏡像的組成Layer能夠經過以下命令進行查詢:

# 鏡像的構建層歷史
$: docker history busybox:autobuild
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
845cc5130d2c        17 minutes ago      /bin/sh -c touch /newfile                       0B
ef46e0caa533        4 days ago          /bin/sh -c #(nop) CMD ["sh"] 0B
<missing>           4 days ago          /bin/sh -c #(nop) ADD file:1067e5a... in / 1.21MB
複製代碼

不難看出,鏡像busybox:autobuild一共執行了從底往上的三次層構建。具體構建的指令能夠經過第三列的命令得出。<missing>的意思是:該層是在其它系統上構建的,在本地是不可用的。只須要忽略就好。

2.2 Union FileSystem

要了解 Docker 鏡像的存儲首先必須瞭解聯合文件系統 UnionFS (Union File System),所謂UnionFS就是把不一樣物理位置的目錄合併mount到同一個目錄中。UnionFS的具體實現有不少種:

  • 早期的UFS
  • AUFS
  • OverlayFS
    • overlay
    • overlay2

具體Docker宿主機上使用那種UnionFS文件系統驅動,能夠經過以下命令查詢:

$:  docker info | grep Storage
Storage Driver: overlay2
複製代碼

overlay2是一種更現代的聯合文件系統 UnionFS,它比overlay的早期版本在穩定與性能上都有很大提高。因此通常最新的Docker採用的存儲驅動使用的都是overlay2

爲了方便演示UnionFS文件系統,若是是MacOS系統,建議安裝Docker Machine開啓一臺新的虛擬機操做,排除由於Docker for MacOS運行在虛擬機上的各類環境干擾。具體Docker Machine的安裝請自行查閱相關文檔。

首先建立一臺新的Docker Machine:

# 建立
$: docker-machine create ufs
...
Docker is up and running!

# 登陸
$: docker-machine ssh ufs
... ok

# 查詢 overlay
$: cat /proc/filesystems | grep overlay
nodev	overlay
複製代碼

經過確認,這臺Docker Machine是支持UnionFS文件系統的,使用的是overlay存儲驅動。 既然UnionFS就是把不一樣物理位置的目錄合併mount到同一個目錄中.如今咱們經過命令行的方式實現一下Docker官網提供UnionFS的原理圖。

從圖中能夠看出,咱們須要提供兩個目錄,分別表明Container LayerImage Layer。目錄名稱,取圖示右部的名稱:

  • 目錄upper, 表明Container Layer
  • 目錄lower, 表明Image Layer

除了這兩個目錄之外,經過UnionFS掛載目錄還須要兩個目錄:

  • 目錄merged, 表明掛載目錄,即合併後的目錄
  • 目錄work, 必須爲空目錄,是overlay存儲驅動掛載所需的工做目錄。

經過命令行實現圖示中的文件夾結構:

# 建立一個測試目錄
$: mkdir demo && cd demo

# 建立子目錄與文件
$: mkdir upper lower merged work
$: touch lower/file1 lower/file2 lower/file3
$: touch upper/file2 upper/file4

# 經過文件內容區分如下file2
$: echo lower > lower/file2
$: echo upper > upper/file2

# 未掛載
$: ls merged
複製代碼

迄今爲止,一切都是常規文件系統操做。如今經過mount命令進行UnionFS文件系統的目錄掛載.

# 目錄合併掛載到merged
$: sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged

# 掛載完成後
$: ls merged
file1 file2 file3 file4

# file2 使用的是頂層 upper 的file2 文件
$: cat merged/file2
upper
複製代碼

下面再分別經過文件的增刪改加深對UnionFS文件系統的理解:

  • 新增文件
# 新增文件
$: touch merges/file5
$: ls merged/
file1  file2  file3  file4  file5
# 新增文件寫在頂層的 upper 文件夾
$: ls upper/
file2  file4  file5
$: ls lower/
file1  file2  file3
複製代碼
  • 修改文件
# 修改文件 CoW 技術
$: echo mod > merged/file1
$: ls upper/
file1 file2  file4  file5
$: cat upper/file1
mod
$: cat lower/file1
複製代碼
  • 刪除文件
# 刪除文件
$: rm merged/file1
$: ls -al upper | grep file1
c---------    1 root     root        0,   0 Jun 17 10:41 file1
$: ls -al lower | grep file1
-rw-r--r--    1 docker   staff            0 Jun 17 10:15 file1
複製代碼

實際操做完成以上過程,相信你對於UnionFS文件系統有了更加直觀的感覺。你可能會問, Docker Image的底層鏡像是由一組Layer組成的,多個底層目錄在UnionFS中如何掛載?其實很簡單,只須要經過:分隔便可。

# 多層目錄: lower1 / lower2 / lower3
$: sudo mount -t overlay overlay -olowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
複製代碼

掛載完成後,lower1 / lower2 / lower3之間的層疊順序又是怎樣,讀者能夠自行測試一下。

最後,咱們查詢一下系統的掛載列表,

mount | grep overlay
overlay on /home/docker/demo/merged type overlay (rw,relatime,lowerdir=lower,upperdir=upper,workdir=work)
複製代碼

從現有輸出可知目前咱們docker-machine中僅掛載了一個overlay目錄。

2.3 鏡像的存儲

如今咱們在這臺新的docker-machine上構建一個1.2中所描述的Docker鏡像: busybox:autobuild

$: mkdir autobuild && cd autobuild
$: cat <<EOF > Dockerfile
FROM busybox:latest
RUN  touch /newfile
EOF
$: docker build -t busybox:autobuild .

# 完成構建後,如今系統中有兩個docker image
$: docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             autobuild           2e32da74b3ad        4 seconds ago       1.22MB
busybox             latest              e4db68de4ff2        2 days ago          1.22MB
複製代碼

構建完成後,咱們直接看一下docker-machine上的文件系統的掛載狀況:

# docker 無容器運行
$: mount
...
/dev/sda1 on /mnt/sda1/var/lib/docker type ext4 (rw,relatime,data=ordered)

# docker 運行容器時
# 從新開啓新會話,運行一個容器實例 `docker run -it busybox:autobuild sh`
$: mount
...
/dev/sda1 on /mnt/sda1/var/lib/docker type ext4 (rw,relatime,data=ordered)
overlay on /mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/merged type overlay (rw,relatime,lowerdir=/mnt/sda1/var/lib/docker/overlay2/l/KLGL6INSJ2UBLMAUP5B4IORUTG:/mnt/sda1/var/lib/docker/overlay2/l/BGIT3WQZVII4Z2THF35I6T5V5O:/mnt/sda1/var/lib/docker/overlay2/l/6GZ2NT4UQT6EQK3IT4IGMBXU4T,upperdir=/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/diff,workdir=/mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/work)
shm on /mnt/sda1/var/lib/docker/containers/e50f19c5bde3fe53cde3729de92f75b74323f7ebb506b0635eb76dd5b81e080a/mounts/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)
nsfs on /var/run/docker/netns/3c464f8003e8 type nsfs (rw)
複製代碼

對比輸出,可以很明顯的看到,暫僅關注 overlay 掛載狀況。得出:

  • 掛載後的目錄是: /mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/merged
  • 容器Layer是: /mnt/sda1/var/lib/docker/overlay2/a54541dd24971b9491a54b43cdf51f4ef9c87c1cd29748bb3fe64dedafd91b56/diff
  • 鏡像Layer是: /mnt/sda1/var/lib/docker/overlay2/l/KLGL6INSJ2UBLMAUP5B4IORUTG /mnt/sda1/var/lib/docker/overlay2/l/BGIT3WQZVII4Z2THF35I6T5V5O /mnt/sda1/var/lib/docker/overlay2/l/6GZ2NT4UQT6EQK3IT4IGMBXU4T

其中鏡像Layer使用的是軟鏈接。一樣的信息,咱們能夠通docker inspect查詢出來。

$: docker inspect <container-id> -f '{{.GraphDriver.Data.MergedDir}}'
$: docker inspect <container-id> -f '{{.GraphDriver.Data.UpperDir}}'
$: docker inspect <container-id> -f '{{.GraphDriver.Data.LowerDir}}'
複製代碼

輸出的路徑就是具體Docker鏡像的存儲位置。

3. 最小化 Docker 鏡像

3.1 爲何要最小化 Docker 鏡像

最小化 Docker 鏡像的緣由可總結出如下幾條:

  • 省錢,減小網絡傳輸流量,節省鏡像存儲空間
  • 省時,加速鏡像部署時間
  • 安全,有限功能下降被攻擊的可能性
  • 環保,垃圾都分類了,浪費資源可恥

3.2 如何構建最小化 Docker 鏡像

1.31.4 中所討論的鏡像的組成原理與存儲, 最小化 Docker 鏡像的主要途徑總結下來也就兩條:

  • 縮減鏡像的Layer大小
  • 減小鏡像的Layer層數

先從簡單的減小鏡像Layer的層數開始。

3.3 減小鏡像的 Layer 層數

3.3.1 組合命令

在定義Dockerfile的時候,每一條指令都會對應一個新的鏡像層。經過docker history命令就能夠查詢出具體Docker 鏡像構建的層以及每層使用的指令。爲了減小鏡像的層數,在實際構建鏡像時,經過使用&&鏈接命令的執行過程,將多個命令定義到一個構建指令中執行。如:

FROM debian:stable

WORKDIR /var/www 
RUN apt-get update && \ apt-get -y --no-install-recommends install curl \ ca-certificates && \ apt-get purge -y curl \ ca-certificates && \ apt-get autoremove -y && \ apt-get clean 複製代碼

3.3.2 壓縮鏡像層

除了經過將多命令經過&&鏈接到一個構建指令外,在Docker鏡像的構建過程當中,還能夠經過--squash的方式,開啓鏡像層的壓縮功能,將多個變化的鏡像層,壓縮成一個新的鏡像層。

具體命令就以下:

$: docker build --squash -t <image> .
複製代碼

3.4 縮減鏡像的 Layer 大小

3.4.1 選擇基礎鏡像

縮減Layer的大小須要從頭開始,即選擇什麼樣的基礎鏡像做爲初始鏡像。通常狀況下,你們都會從如下三個基礎鏡像開始。

  • 鏡像 scratch(空鏡像), 大小 0B
  • 鏡像 busybox(空鏡像 + busybox), 大小 1.4MB
  • 鏡像 alpine (空鏡像 + busybox + apk), 大小 3.98MB

鏡像 busybox 經過busybox程序提供一些基礎的Linux系統操做命令,鏡像 alpine則是在次基礎上提供了apk包管理命令,方便安裝各種工具及依賴包。普遍使用的鏡像基本都是鏡像 alpine鏡像 busybox更適合一些快速的實驗場景。而鏡像 scratch空鏡像,由於不提供任何輔助工具,對於不依賴任何第三方庫的程序是合適的。由於鏡像 scratch空鏡像自己不提供任何container OS,因此程序是運行在Docker Host即宿主機上的,只是利用了Docker技術提供的隔離技術而已。

細心的讀者可能會發現,在MacOS上編譯的程序,採用鏡像 scratch空鏡像時,容器運行會報錯:。那是由於,Docker for Mac是運行在Linux虛擬機上的緣故。因此不能夠直接構建MacOS格式的可執行程序在Docker for Mac上採用空鏡像的方式運行。

3.4.2 多階段構建鏡像

多階段構建 Multi-Stage BuildDocker 17.05 版本開始引入的新特性。經過將原先僅一個階段構建的鏡像查分紅多個階段。之因此多階段構建鏡像可以縮減鏡像的大小,是由於發佈程序在編譯期相關的依賴包以及臨時文件並非最終發佈鏡像所須要的。經過劃分不一樣的階段,構建不一樣的鏡像,最終鏡像則取決於咱們真正須要發佈的實體是什麼。

FROM golang:1.11-alpine3.7 AS builder

WORKDIR /app COPY main.go . RUN go build -o server . 
FROM alpine:3.7

WORKDIR /app COPY --from=builder /app . 
CMD ["./server"] 複製代碼

如上的Dockerfile就是多階段構建,在builder階段使用的基礎鏡像是golang:1.11-alpine3.7,顯然是由於編譯期的須要,對於發佈真正的server程序是徹底不必的。經過多階段構建鏡像的方式就能夠僅僅打包須要的實體構成鏡像。

除了多階段構建之外,若是你還想忽略鏡像中一些冗餘文件,還能夠經過.dockerignore的方式在文件中定義出來。功能和.gitignore相似。

4. 加固 Docker 鏡像

最小化Docker 鏡像的構建完成了,可是,咱們的工做卻仍未結束。咱們還須要對鏡像進行加固處理。

4.1 鏡像內容可尋址標識符(CAIID)

鏡像內容可尋址標識符(Content addressable image identifiers), 能夠對來源基礎鏡像內容進行校驗,確保沒有被第三方篡改。具體的操做方式,就是在構建本身鏡像的同時,對基礎鏡像內容進行內容的sha256摘要值進行設置,防止在不知情的狀況下被篡改。

首先,得出具體鏡像的正確sha256摘要值.

# 經過命令查詢出具體鏡像的sha256摘要
$: docker inspect busybox:autobuild -f "{{.RepoDigests}}"
sha256:9b63a0eaaed5e677bb1e1b29c1a97268e6c9e6fee98b48badf0f168ae72a51dc
複製代碼

再在Dockerfile定義時,設置基礎鏡像的sha256摘要值

FROM busybox@sha256:9b63a0eaaed5e677bb1e1b29c1a97268e6c9e6fee98b48badf0f168ae72a51dc
...
複製代碼

注意:鏡像內容可尋址標識符的獲取必須通過一次 push 或者 pull 操做,即在鏡像註冊服務上發佈後,才能夠經過以上 inspect 命令查詢出結果。若是僅僅是本地的鏡像,沒法經過 inpect 命令獲取。固然僅僅是本地使用的鏡像,鏡像內容可尋址標識符也是不必的。

4.2 用戶權限

容器一旦建立出來,其默認使用的用戶是能夠在鏡像中進行設置的。經過設置必要的鏡像默認用戶,能夠限制其在容器中的執行權限。在某種程度上也就進行提高了鏡像的安全級別。不過,這須要根據具體的業務發佈狀況進行設置,常規狀況下,基礎鏡像都仍是root用戶做爲默認用戶 。

安全原則:構建鏡像自己是爲了特定的應用定製的,默認狀況下應該儘量的下降用戶權限。

4.3 SUID與SGID問題

除了鏡像自己設置必要的默認用戶之外,在鏡像中,還會存在一類程序,即便是經過普通用戶執行,但在運行時會以更高級別的權限執行。就是系統針對可執行文件與目錄提供的SUID與SGID特殊權限。

經過對可執行文件設置SUID或SGID屬性,本來執行命令的用戶會切換成爲命令的全部者或是所屬組的權限進行執行。也就是提高了執行命令的權限。

在實際的鏡像構建中,應該儘量的避免此類權限提高形成的可能的漏洞。建議鏡像構建時,掃描鏡像內是否存在此類執行文件,若是存在儘量的刪除。刪除命令可參考:

# 鏡像構建過程當中增長對特殊權限可執行文件的掃描並刪除
RUN for i in $(find / -type f \( -perm +6000 -o -perm +2000 \)); \ do chmod ug-s $i; done 複製代碼

5. 審查 Docker 鏡像

正如Code Review同樣,代碼審查能夠大大提高企業項目的質量。容器鏡像一樣做爲開發人員或是運維人員的產出物,對其進行審查也是必要的。

雖然咱們能夠經過docker命令結合文件系統瀏覽的方式進行容器鏡像的審查,但其過程須要人工參與,很難作到自動化,更別提將鏡像審查集成到CI過程當中了。但一個好的工具能夠幫咱們作到這點。

推薦一個很是棒的開源項目dive,具體安裝請參考其項目頁。它不但能夠方便咱們查詢具體鏡像層的詳細信息,還能夠做爲CI持續集成過程當中的鏡像審查之用。使用它能夠大大提高咱們審查鏡像的速度,而且能夠將這個過程作成自動化。

該項目的具體動態操做圖示以下:

dive

若是做爲鏡像審查以後,能夠進行以下命令操做:

$: CI=true dive <image-id>
Fetching image... (this can take a while with large images)
Parsing image...
Analyzing image...
  efficiency: 95.0863 %
  wastedBytes: 671109 bytes (671 kB)
  userWastedPercent: 8.2274 %
Run CI Validations...
  Using default CI config
  PASS: highestUserWastedPercent
  SKIP: highestWastedBytes: rule disabled
  PASS: lowestEfficiency
複製代碼

從輸出信息能夠獲得不少有用的信息,集成的CI過程也就很是容易了。 dive自己就提供了.dive-ci做爲項目的CI配置:

rules:
  # If the efficiency is measured below X%, mark as failed.
  # Expressed as a percentage between 0-1.
 lowestEfficiency: 0.95

  # If the amount of wasted space is at least X or larger than X, mark as failed.
  # Expressed in B, KB, MB, and GB.
 highestWastedBytes: 20MB

  # If the amount of wasted space makes up for X% or more of the image, mark as failed.
  # Note: the base image layer is NOT included in the total image size.
  # Expressed as a percentage between 0-1; fails if the threshold is met or crossed.
 highestUserWastedPercent: 0.20
複製代碼

集成到CI中,增長如下命令便可:

$: CI=true dive <image-id> 
複製代碼

鏡像審查和代碼審查相似,是一件開始抵制,開始後就欲罷不能的事。這件事宜早不宜遲。對於企業與我的而言均百利而無一害。

以前發佈文章如何對Docker Image進行審查,出自此節。

6. 參考資源

相關文章
相關標籤/搜索