隨着Docker技術近年來的高速發展和廣泛使用,衆多互聯網公司陸續採用Docker技術作爲核心業務的Paas層支撐。那麼勢必在Docker技術下會給安全技術體系帶來新的挑戰。此文章總結了VIPKID安全團隊基於Docker技術的安全實踐,希望同各位安全小夥伴一起探討學習。
Docker架構簡介
Docker包括三個基本概念:
鏡像(Image)
容器(Container)
倉庫(Repository)
理解了這三個概念,就理解了Docker的整個生命週期。
鏡像
我們都知道,操作系統分爲內核和用戶空間。對於Linux而言,內核啓動後,會掛載root文件系統爲其提供用戶空間支持。而Docker鏡像(Image),就相當於是一個root文件系統。比如官方鏡像Ubuntu 18.04就包含了完整的一套Ubuntu 18.04最小系統的root文件系統。Docker鏡像是一個特殊的文件系統,除了提供容器運行時所需的程序、庫、資源、配置等文件外,還包含了一些爲運行時準備的一些配置參數(如匿名卷、環境變量、用戶等)。鏡像不包含任何動態數據,其內容在構建之後也不會被改變。
因爲鏡像包含操作系統完整的root文件系統,其體積往往是龐大的,因此在Docker設計時,就充分利用[Union FS]的技術,將其設計爲分層存儲的架構。所以嚴格來說,鏡像並非是像一個ISO那樣的打包文件,鏡像只是一個虛擬的概念,其實際體現並非由一個文件組成,而是由一組文件系統組成,或者說,由多層文件系統聯合組成。嘗試下docker pull ubuntu:
鏡像構建時,會一層層構建,前一層是後一層的基礎。每一層構建完就不會再發生改變,後一層上的任何改變只發生在自己這一層。比如,刪除前一層文件的操作,實際不是真的刪除前一層的文件,而是僅在當前層標記爲該文件已刪除。在最終容器運行的時候,雖然不會看到這個文件,但是實際上該文件會一直跟隨鏡像。因此,在構建鏡像的時候,需要額外小心,每一層儘量只包含該層需要添加的東西,任何額外的東西應該在該層構建結束前清理掉。
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
例如如上Dockerfile中每一個指令都會建立一層,RUN也不例外。每一個RUN的行爲,就和剛纔我們手工建立鏡像的過程一樣:新建立一層,在其上執行這些命令,執行結束後,commit這一層的修改,構成新的鏡像。而上面的這種寫法,創建了7層鏡像。
總結:鏡像就是打包的、分層的文件系統。
容器
鏡像(Image)和容器(Container)的關係,就像是面向對象程序設計中的類和實例一樣,鏡像是靜態的定義,容器是鏡像運行時的實體。容器可以被創建、啓動、停止、刪除、暫停等。
容器的實質是進程,但與直接在宿主執行的進程不同,容器進程運行於屬於自己的獨立的『命名空間』,因此容器可以擁有自己的root文件系統、自己的網絡配置、自己的進程空間,甚至自己的用戶ID空間。容器內的進程是運行在一個隔離的環境裏,使用起來,就好像是在一個獨立於宿主的系統下操作一樣。這種特性使得容器封裝的應用比直接在宿主運行更加安全。
總結:容器就是供鏡像運行的進程,具有獨立的命名空間(namespaces)。
倉庫
鏡像構建完成後,可以很容易的在當前宿主機上運行,但是,如果需要在其它服務器上使用這個鏡像,我們就需要一個集中的存儲、分發鏡像的服務,Docker Registry就是這樣的服務。
一個Docker Registry中可以包含多個倉庫(Repository);每個倉庫可以包含多個標籤(Tag);每個標籤對應一個鏡像。
通常,一個倉庫會包含同一個軟件不同版本的鏡像,而標籤就常用於對應該軟件的各個版本。我們可以通過<倉庫名>:<標籤>的格式來指定具體是這個軟件哪個版本的鏡像。如果不給出標籤,將以latest作爲默認標籤。
以『Ubuntu鏡像』爲例,Ubuntu是倉庫的名字,其內包含有不同的版本標籤,如:16.04,18.04。我們可以通過ubuntu:16.04,或者ubuntu:18.04來具體指定所需哪個版本的鏡像。如果忽略了標籤,比如ubuntu,那將視爲ubuntu:latest。
倉庫名經常以兩段式路徑形式出現,比如jwilder/nginx-proxy,前者往往意味着Docker Registry多用戶環境下的用戶名,後者則往往是對應的軟件名。但這並非絕對,取決於所使用的具體Docker Registry的軟件或服務。
DockerRegistry公開服務:例如Docker Hub\Quay.io\Google Container Registry。
私有Docker Registry:比如Harbor和Sonatype Nexus。
Docker架構
Docker的文件系統是分層的,它的rootfs在mount之後會轉爲只讀模式。Docker在它上面添加一個新的文件系統,來達成它的只讀。
事實上,從下圖中,我們能看到多個只讀的文件系統,Docker中把他們稱爲 層(layer)。
image是隻讀的,container部分則是可寫的。
如果用戶想要修改底層只讀層上的文件,這個文件就會被先拷貝到上層,修改後駐留在上層,並屏蔽原有的下層文件。
最後一部分是容器(container), 容器是可讀寫的。
在系統啓動時,Docker會首先一層一層的加載image,直到最先面的base image。而這些image都是隻讀的。
最後,在最上層添加可讀寫的一個容器, 這裏存放着諸如unique id,網絡配置之類的信息。
既然是可讀寫的,就會有狀態。容器共有兩種狀態:running和exited。
用戶也可以用Docker commit命令(不推薦使用)將一個容器壓制爲image,供後續使用。
Docker生態
建於Docker之上的Kubernetes可以構建一個容器的調度服務,其目的是讓用戶透過Kubernetes集羣來進行雲端容器集羣的管理,而無需用戶進行復雜的設置工作。系統會自動選取合適的工作節點來執行具體的容器集羣調度處理工作。其核心概念是Container Pod。一個Pod由一組工作於同一物理工作節點的容器構成。這些組容器擁有相同的網絡命名空間、IP以及存儲配額,也可以根據實際情況對每一個Pod進行端口映射。此外,Kubernetes工作節點會由主系統進行管理,節點包含了能夠運行Docker容器所用到的服務。
kubelet是在每個Node節點上運行的主要“節點代理”。它可以通過以下方式向apiserver進行註冊:主機名(hostname);覆蓋主機名的參數;某雲服務商的特定邏輯。
節點(Node):一個節點是一個運行Kubernetes中的主機。
容器組(Pod):一個Pod對應於由若干容器組成的一個容器組,同個組內的容器共享一個存儲卷(volume)。
容器組生命週期(pos-states):包含所有容器狀態,包括容器組狀態類型,容器組生命週期,事件,重啓策略,以及replication controllers。
Replication Controllers:主要負責指定數量的Pod在同一時間一起運行。並且在任何時候都有指定數量的Pod副本,在此基礎上提供一些高級特性,比如滾動升級和彈性伸縮。
服務(Service):一個Kubernetes服務是容器組邏輯的高級抽象,同時也對外提供訪問容器組的策略。
卷(Volume):一個卷就是一個目錄,容器對其有訪問權限。
標籤(Label):標籤是用來連接一組對象的,比如容器組。標籤可以被用來組織和選擇子對象。
接口權限(accessing_the_api):端口,IP 地址和代理的防火牆規則。
Web界面(ux):用戶可以通過 web 界面操作 Kubernetes。
命令行操作(cli):kubectl命令。
Docker文件存儲驅動
Docker的overlay存儲驅動利用OverlayFS(堆疊文件系統)的一些特徵來構建以及管理鏡像和容器的磁盤結構。
Docker 1.12後推出的overlay2在inode的利用方面比Ovelay更有效,overlay2要求內核版本大於等於4.0。
Overlay
由較早的kernel以及Docker版本使用。
如下圖所示,Overlay在主機上用到2個目錄,這2個目錄被看成是Overlay的層。upperdir爲容器層、lowerdir爲鏡像層使用聯合掛載技術將它們掛載在同一目錄(merged)下,提供統一視圖。
當容器層和鏡像層擁有相同的文件時,容器層的文件可見,隱藏了鏡像層相同的文件。然後由容器掛載目錄(merged)提供了統一視圖。
Overlay只使用2層,意味着多層鏡像不會被實現爲多個OverlayFS層。每個鏡像被實現爲自己的目錄,這個目錄在路徑/var/lib/docker/overlay下。硬鏈接被用來索引和共享底層的數據,節省了空間。
當創建一個容器時,Overlay驅動連接代表鏡像層頂層的目錄(只讀)和一個代表容器層的新目錄(讀寫):
root@chenximing-MS-7823:/home/chenximing# tree /var/lib/docker/overlay/
/var/lib/docker/overlay/
一開始Overlay路徑下爲空,用docker pull命令下載一個由5層鏡像層組成的Docker鏡像:
chenximing@chenximing-MS-7823:~$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
c62795f78da9: Pull complete
d4fceeeb758e: Pull complete
5c9125a401ae: Pull complete
0062f774e994: Pull complete
6b33fd031fac: Pull complete
Digest: sha256:c2bbf50d276508d73dd865cda7b4ee9b5243f2648647d21e3a471dd3cc4209a0
Status: Downloaded newer image for ubuntu:latests
此時,在路徑/var/lib/docker/overlay/下,每個鏡像層都有一個對應的目錄,包含了該層鏡像的內容,通過tree命令發現,每個鏡像層下只包含一個root目錄(因爲Docker 1.10開始,使用基於內容的尋址,因此目錄名和鏡像層的ID不一致)。
root@chenximing-MS-7823:/home/chenximing# tree -L 2 /var/lib/docker/overlay/
/var/lib/docker/overlay/
├── 3279a41fd358cdda798d99cc2da0425b4836a489083ae9db4aedc834f426915a
│ └── root
├── 33629fe3999e692ecbeb340f855a2d05ddf4e173ea915041be217e1c9cc8a48f
│ └── root
├── 6ab876fcc7ad584dd1eebae0d9304abb22561012f6adb87828f57906d799c33b
│ └── root
├── 77806a4b8257cd5508a1131d4d47c2d4b4d51703a1cc0dd8daddf0e86a68d492
│ └── root
└── ed271f2159e3c9de18ede980e4e2ecb0ef39b96927d446ba93deab0b6ff1a8e3
└── root
每一層都包含了“該層獨有的文件”以及“和其低層共享的數據的硬連接”。如ls命令,每一層都使用一個硬鏈接來指向實際ls命令的inode號(405241):
root@chenximing-MS-7823:/home/chenximing# ls -i /var/lib/docker/overlay/3279a41fd358cdda798d99cc2da0425b4836a489083ae9db4aedc834f426915a/root/bin/ls 405241 /var/lib/docker/overlay/3279a41fd358cdda798d99cc2da0425b4836a489083ae9db4aedc834f426915a/root/bin/ls root@chenximing-MS-7823:/home/chenximing# ls -i /var/lib/docker/overlay/33629fe3999e692ecbeb340f855a2d05ddf4e173ea915041be217e1c9cc8a48f/root/bin/ls 405241 /var/lib/docker/overlay/33629fe3999e692ecbeb340f855a2d05ddf4e173ea915041be217e1c9cc8a48f/root/bin/ls
使用剛剛拉取的Ubuntu鏡像創建一個容器(docker run --name test -d -it ubuntu:latest /bin/bash),用docker ps命令查看:
root@chenximing-MS-7823:/home/chenximing# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8963ffb422fa ubuntu "/bin/bash" 32 minutes ago Up 52 seconds container_1
創建容器時,實際上是在已有的鏡像層上創建了一層容器層,容器層在路徑/var/lib/docker/overlay下也存在對應的目錄:
root@chenximing-MS-7823:/home/chenximing# ls /var/lib/docker/overlay/
3279a41fd358cdda798d99cc2da0425b4836a489083ae9db4aedc834f426915a
33629fe3999e692ecbeb340f855a2d05ddf4e173ea915041be217e1c9cc8a48f
6ab876fcc7ad584dd1eebae0d9304abb22561012f6adb87828f57906d799c33b
6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d //創建容器後新增的目錄
6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d-init //創建容器後新增的目錄
77806a4b8257cd5508a1131d4d47c2d4b4d51703a1cc0dd8daddf0e86a68d492
ed271f2159e3c9de18ede980e4e2ecb0ef39b96927d446ba93deab0b6ff1a8e3
“6abc47……”和“6abc47…..-init”爲創建容器後新增的目錄。查看這兩個目錄的內容:
root@chenximing-MS-7823:/home/chenximing# tree -L 3 /var/lib/docker/overlay/6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d*/
/var/lib/docker/overlay/6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d/
├── lower-id
├── merged
├── upper
│ ├── dev
│ │ └── console
│ ├── etc
│ │ ├── hostname
│ │ ├── hosts
│ │ ├── mtab -> /proc/mounts
│ │ └── resolv.conf
│ └── root
└── work
└── work
/var/lib/docker/overlay/6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d-init/
├── lower-id
├── merged
├── upper
│ ├── dev
│ │ └── console
│ └── etc
│ ├── hostname
│ ├── hosts
│ ├── mtab -> /proc/mounts
│ └── resolv.conf
└── work
└── work
“6abc47……”爲讀寫層,“6abc47…..-init”爲初始層。初始層中大多是初始化容器環境時,與容器相關的環境信息,如容器主機名,主機host信息以及域名服務文件等。所有對容器做出的改變都記錄在讀寫層。
文件lower-id用來索引該容器使用的鏡像層,upper目錄包含了容器層的內容,每當啓動一個容器時,會將lower-id指向的鏡像層目錄以及upper目錄聯合掛載到merged目錄,因此,容器內的視角就是merged目錄下的內容。而work目錄則是用來完成如copy-on_write的操作。
看看容器層使用到了哪一層鏡像層:
root@chenximing-MS-7823:/home/chenximing# cat /var/lib/docker/overlay/6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d/lower-id
ed271f2159e3c9de18ede980e4e2ecb0ef39b96927d446ba93deab0b6ff1a8e3
root@chenximing-MS-7823:/home/chenximing# cat /var/lib/docker/overlay/6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d-init/lower-id
ed271f2159e3c9de18ede980e4e2ecb0ef39b96927d446ba93deab0b6ff1a8e3
在剛纔創建的容器中創建一個文件:
root@8963ffb422fa:/# touch file
root@8963ffb422fa:/# echo "hello world" > file
root@8963ffb422fa:/# ls
bin dev file lib media opt root sbin sys usr
boot etc home lib64 mnt proc run srv tmp var
此時再觀察“6abc47……”目錄(讀寫層):
root@chenximing-MS-7823:/home/chenximing# tree -L 3 /var/lib/docker/overlay/6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d/
/var/lib/docker/overlay/6abc47fcf668b6820a2a9a8b0d0c6150f9df61ab5a323783630fac3fd282144d/
├── lower-id
├── merged
├── upper
│ ├── dev
│ │ └── console
│ ├── etc
│ │ ├── hostname
│ │ ├── hosts
│ │ ├── mtab -> /proc/mounts
│ │ └── resolv.conf
│ ├── file
│ └── root
└── work
└── work
發現upper目錄下多出了一個file文件,就是剛纔在容器中創建的文件。
overlay2
和Overlay爲了實現“兩個目錄反映多層鏡像”而使用硬鏈接不同,overlay2驅動天生支持多層(最多128層)。
因此,overlay2在使用Docker層相關的命令時,能提供更好的性能(如:docker build、docker commit)。而且overlay2消耗的inode節點更少。
docker pull ubuntu拉取完一個5層的Ubuntu鏡像後,/var/lib/docker/overlay2下可以看到幾個目錄:
“I”目錄包含一些符號鏈接作爲縮短的層標識符,這些縮短的標識符用來避免掛載時超出頁面大小的限制,每個標示符對應不同層級的diff目錄。
最底層包含“link”文件(不包含lower文件,因爲是最底層),在上面的結果中“c7fae….”爲最底層。這個文件記錄着作爲標識符的更短的符號鏈接的名字、最底層還有一個“diff”目錄(包含實際內容)。
[[email protected] lijianxin1]#cat /var/lib/docker/overlay2/c7fae7e34df9196dacbb4a23f858758d14737d9a0e4168056126c7c540d8f6c8/link
NAQRQEJKM4Z7EIVPNX3QXGW75U
[[email protected] lijianxin1]#ls /var/lib/docker/overlay2/c7fae7e34df9196dacbb4a23f858758d14737d9a0e4168056126c7c540d8f6c8/diff/
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
從第二層開始,每層鏡像層包含“lower”文件,根據這個文件可以索引構建出整個鏡像的層次結構。“diff”文件(層的內容)。還包含“merged”和“work”目錄,用途和Overlay一樣。
[[email protected] lijianxin1]# cat /var/lib/docker/overlay2/596c50d705a1fccc2cd6785bdb9fcfd48c5ec72d64e566047184f432747c3eba/link
DLZI7ZAHMEL5DPBJPBN2AF7X2Q
[[email protected] lijianxin1]# ls /var/lib/docker/overlay2/596c50d705a1fccc2cd6785bdb9fcfd48c5ec72d64e566047184f432747c3eba/diff/
run
[[email protected] lijianxin1]# cat /var/lib/docker/overlay2/596c50d705a1fccc2cd6785bdb9fcfd48c5ec72d64e566047184f432747c3eba/lower
l/IUJFL55XIAQBZV67VRVSWBYVHO:l/NJIGUMOAH4B4VYQUPR3WMO2LRP:l/NAQRQEJKM4Z7EIVPNX3QXGW75U
再來看看容器層,容器層的文件構成和鏡像層類似(這點和Overlay不同),使用剛剛拉取的Ubuntu鏡像創建一個容器(docker run --name test -d ubuntu:latest),/var/lib/docker/overlay2目錄下新增2個目錄:
查看容器層通過lower目錄索引的鏡像層明細:
[[email protected] lijianxin1]# cat /var/lib/docker/overlay2/2297016015b9a9b359a54b63fccb294977043e502f6063fa25d48590868862fb/lower
l/URSCIF77IGWZBPBQJI4Y4GV5T4:l/DLZI7ZAHMEL5DPBJPBN2AF7X2Q:l/IUJFL55XIAQBZV67VRVSWBYVHO:l/NJIGUMOAH4B4VYQUPR3WMO2LRP:l/NAQRQEJKM4Z7EIVPNX3QXGW75U
對於讀,考慮下列3種場景:
讀的文件不在容器層:如果讀的文件不在容器層,則從鏡像層進行讀
讀的文件只存在在容器層:直接從容器層讀
讀的文件在容器層和鏡像層:讀容器層中的文件,因爲容器層隱藏了鏡像層同名的文件
對於寫,考慮下列場景:
寫的文件不在容器層,在鏡像層:由於文件不在容器層,因此overlay/overlay2存儲驅動使用copy_up操作從鏡像層拷貝文件到容器層,然後將寫入的內容寫入到文件新的拷貝中
刪除文件和目錄:刪除鏡像層的文件,會在容器層創建一個whiteout文件來隱藏它;刪除鏡像層的目錄,會創建opaque目錄,它和whiteout文件有相同的效果
重命名目錄:對一個目錄調用rename僅僅在資源和目的地路徑都在頂層時才被允許,否則返回EXDEV(場景較少,未做深入研究)
總結:
Docker鏡像存儲存在分層的概念,在不同的版本存儲驅動overlay與overlay2中,除了高層級鏡像向底層級鏡像link文件方式不同外,原理是相同的,每個層級有專屬這個層級的文件系統和文件。最後merged目錄將鏡像層與容器層文件取並集後形成視圖。
Docker安全中,webshell等文件檢測機制就是通過找到容器文件存儲驅動,通過manifest找到容器與宿主機文件驅動的對應關係,再進行文件hash檢測與特徵碼識別即可。
Docker鏡像與層級的概念
Docker鏡像與層級詳解
因爲鏡像包含操作系統完整的root文件系統,其體積往往是龐大的,因此在Docker設計時,就充分利用[Union FS]的技術,將其設計爲分層存儲的架構。所以嚴格來說,鏡像並非是像一個ISO那樣的打包文件,鏡像只是一個虛擬的概念,其實際體現並非由一個文件組成,而是由一組文件系統組成,或者說,由多層文件系統聯合組成。
最終最高層的鏡像層成爲只讀的init layer層,其與讀寫權限的容器層以及mount的宿主機文件路徑,共同成爲容器的文件系統。
嘗試下docker pull ubuntu:
可以發現一個Ubuntu鏡像存在多個sha256的digestid,用來標示不同層級的layerid(塗層),我們可以通過docker inspect ubuntu查看每個鏡像層級的digest sha256的值:
[[email protected] lijianxin1]# docker inspect ubuntu
除了基礎鏡像存儲驅動層,還有容器掛載的目錄(Volumes)以及Dockerfile中指定的WorkDir(工作目錄):
Docker鏡像掃描的實現
之前講到Docker的存儲驅動overlay2,Docker會在/var/lib/docker/image目錄下給使用的存儲驅動創建一個目錄:/var/lib/docker/overlay2。
tree -L 2 overlay2/
overlay2/
├── distribution
│ ├── diffid-by-digest
│ └── v2metadata-by-diffid
├── imagedb
│ ├── content
│ └── metadata
├── layerdb
│ ├── mounts
│ ├── sha256
│ └── tmp
└── repositories.json
這裏的關鍵地方是imagedb和layerdb目錄,看這個目錄名字,很明顯就是專門用來存儲元數據的地方,那爲什麼區分image和layer呢?因爲在Docker中,image是由多個layer組合而成的,換句話就是layer是一個共享的層,可能有多個image會指向某個layer。
通過docker image ls找到鏡像id,我的是1e4467b07108。
我們打印/var/lib/docker/image/overlay2/imagedb/content/sha256這個目錄:
[[email protected] sha256]# ll |grep 1e4467b07108
-rw------- 1 root root 3408 8月 10 15:14 1e4467b07108685c38297025797890f0492c4ec509212e2e4b4822d367fe6bc8
第一行的1e4467b07108685c38297025797890f0492c4ec509212e2e4b4822d367fe6bc8正是記錄我們Ubuntu鏡像元數據的文件,接下來cat一下這個文件,得到一個長長的json:
由於篇幅原因,我只展示最關鍵的一部分,也就是rootfs。可以看到rootfs的diff_ids是一個包含了3個元素的數組,其實這3個元素正是組成ubuntu鏡像的3個layerID,從上往下看,就是底層到頂層,也就是說
是image的最底層。既然得到了組成這個image的所有layerID,那麼我們就可以帶着這些layerID去尋找overlay中對應的文件目錄了。
我們返回到上一層的layerdb中,先打印一下這個目錄:
[[email protected] layerdb]# ll
總用量 12
drwxr-xr-x 3 root root 4096 8月 10 15:47 mounts
drwxr-xr-x 6 root root 4096 8月 10 15:14 sha256
drwxr-xr-x 2 root root 4096 8月 10 15:14 tmp
在這裏我們只管mounts和sha256兩個目錄,再打印一下sha256目錄:
/var/lib/docker/image/overlay2/layerdb存的只是元數據,那麼真實的rootfs到底存在哪裏呢?其中cache-id就是我們關鍵所在了。我們打印一下如下目錄:
沒錯,這個id就是對應/var/lib/docker/overlay2/c7fae7e34df9196dacbb4a23f858758d14737d9a0e4168056126c7c540d8f6c8。
通過以上技術手段,那我們能夠獲取一個鏡像(image)所有layerid對應的overlay2存儲驅動的層級,這個層級中diff目錄包含了實際容器層使用的文件系統。
[[email protected] layerdb]# cd /var/lib/docker/overlay2/c7fae7e34df9196dacbb4a23f858758d14737d9a0e4168056126c7c540d8f6c8
[[email protected] c7fae7e34df9196dacbb4a23f858758d14737d9a0e4168056126c7c540d8f6c8]# ls
diff link
[[email protected] c7fae7e34df9196dacbb4a23f858758d14737d9a0e4168056126c7c540d8f6c8]# cd diff/
[[email protected] diff]# ;s
bash: 未預期的符號 `;' 附近有語法錯誤
[[email protected] diff]# ls
bin boot dev etc home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp usr var
1、鏡像系統版本,安裝的軟件包版本記錄等
centos:etc/os-release,usr/lib/os-release
[[email protected] diff]# more usr/lib/os-release
NAME="Ubuntu"
VERSION="20.04 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal
2、探測鏡像已安裝的軟件包
上一步已經探測到了操作系統,自然可以知道系統的軟件管理包是RPM還是DPKG。
debian, ubuntu : dpkg
centos, rhel, fedora, amzn, ol, oracle : rpm
CentOS系統的軟件管理包是RPM,而Debain系統的軟件管理是DPKG。下面是不同軟件管理包的目錄:
rpm:var/lib/rpm/Packages
dpkg:var/lib/dpkg/status
apk:lib/apk/db/installed
比如Debian系統,從文件/var/lib/dpkg/status文件則可以探測到當前系統安裝了哪些版本的軟件。
[[email protected] diff]# more var/lib/dpkg/status
Package: adduser
Status: install ok installed
Priority: important
Section: admin
Installed-Size: 624
Maintainer: Ubuntu Core Developers <[email protected]>
Architecture: all
Multi-Arch: foreign
Version: 3.118ubuntu2
Depends: passwd, debconf (>= 0.5) | debconf-2.0
Suggests: liblocale-gettext-perl, perl, ecryptfs-utils (>= 67-1)
Conffiles:
/etc/deluser.conf 773fb95e98a27947de4a95abb3d3f2a2
Description: add and remove users and groups
This package includes the 'adduser' and 'deluser' commands for creating
and removing users.
.
- 'adduser' creates new users and groups and adds existing users to
existing groups;
- 'deluser' removes users and groups and removes users from a given
group.
.
Adding users with 'adduser' is much easier than adding them manually.
Adduser will choose appropriate UID and GID values, create a home
directory, copy skeletal user configuration, and automate setting
initial values for the user's password, real name and so on.
.
Deluser can back up and remove users' home directories
and mail spool or all the files they own on the system.
.
A custom script can be executed after each of the commands.
Original-Maintainer: Debian Adduser Developers <[email protected]>
Package: apt
Status: install ok installed
Priority: important
Section: admin
Installed-Size: 4182
Maintainer: Ubuntu Developers <[email protected]>
Architecture: amd64
Version: 2.0.2ubuntu0.1
Replaces: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~)
Provides: apt-transport-https (= 2.0.2ubuntu0.1)
Depends: adduser, gpgv | gpgv2 | gpgv1, libapt-pkg6.0 (>= 2.0.2ubuntu0.1), ubuntu-keyring, libc6 (>= 2.15), libgcc-s1 (>= 3.0), libgnutls30 (>= 3.6.12), libseccomp2 (>= 2.4.2), libstdc++6 (>= 9), libsystemd0
Recommends: ca-certificates
Suggests: apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), gnupg | gnupg2 | gnupg1, powermgmt-base
Breaks: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~), aptitude (<< 0.8.10)
Conffiles:
/etc/apt/apt.conf.d/01-vendor-ubuntu c69ce53f5f0755e5ac4441702e820505
/etc/apt/apt.conf.d/01autoremove ab6540f7278a05a4b7f9e58afcaa5f46
/etc/cron.daily/apt-compat 49e9b2cfa17849700d4db735d04244f3
/etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a
/etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3
Description: commandline package manager
This package provides commandline tools for searching and
managing as well as querying information about packages
as a low-level access to all features of the libapt-pkg library.
現在Horbor已經集成Clair鏡像安全掃描組件:https://blog.csdn.net/arnolan/article/details/102666831
總結:鏡像使用的Union FS具備分層結構(Layer),分爲鏡像層+容器讀寫層+掛載Volume,而這些層級在宿主機是依靠上一章節講述的Overlay技術進行驅動的,那麼這些層級在宿主機是有1對1映射的目錄的。然後不同類型的OS軟件安裝包是有固定目錄的,獲取到DPKG或是RPM包安裝信息,再去匹配該版本軟件包CVE漏洞即可。
Docker Namespaces與Cgroup概念
Namespaces
命名空間(namespaces)是Linux爲我們提供的用於分離進程樹、網絡接口、掛載點以及進程間通信等資源的方法。在日常使用Linux或者macOS時,我們並沒有運行多個完全分離的服務器的需要,但是如果我們在服務器上啓動了多個服務,這些服務其實會相互影響的,每一個服務都能看到其他服務的進程,也可以訪問宿主機器上的任意文件,這是很多時候我們都不願意看到的,我們更希望運行在同一臺機器上的不同服務能做到完全隔離,就像運行在多臺不同的機器上一樣。Docker就通過Linux的namespaces對不同的容器實現了隔離。
Linux的命名空間機制提供了以下七種不同的命名空間,其中包括:
CLONE_NEWCGROUP:Docker未使用。
CLONE_NEWIPC:IPC namespaces,用於隔離進程間通訊所需的資源。
CLONE_NEWNET:Network namespaces,爲進程提供了一個完全獨立的網絡協議棧的視圖。
CLONE_NEWNS:mount namespaces,每個進程都存在於一個mount namespaces裏面,mount namespaces爲進程提供了一個文件層次視圖。
CLONE_NEWPID:PID namespaces,Linux通過命名空間管理進程號,同一個進程,在不同的命名空間進程號不同。
CLONE_NEWUSER:user namespaces,用於隔離用戶。
CLONE_NEWUTS:UTS namespaces,用於隔離主機名。
通過這七個選項我們能在創建新的進程時設置新進程應該在哪些資源上與宿主機器進行隔離。
進程Namespaces
我們在當前的Linux操作系統下運行一個新的Docker容器,並通過exec進入其內部的bash並打印其中的全部進程,我們會得到以下的結果:
[[email protected] lijianxin1]# docker run --name mytest -d -it ubuntu:latest
[[email protected] lijianxin1]# docker exec -it 15e93faf323c /bin/bash
root@15e93faf323c:/#
root@15e93faf323c:/#
root@15e93faf323c:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 08:20 pts/0 00:00:00 /bin/bash
root 10 0 0 08:21 pts/1 00:00:00 /bin/bash
root 18 10 0 08:22 pts/1 00:00:00 ps -ef
在新的容器內部執行ps命令打印出了非常乾淨的進程列表,只有包含當前ps -ef在內的三個進程,在宿主機器上的幾十個進程都已經消失不見了。
當前的Docker容器成功將容器內的進程與宿主機器中的進程隔離,如果我們在宿主機器上打印當前的全部進程時,會得到下面三條與Docker相關的結果:
[[email protected] lijianxin1]# ps -ef|grep docker
root 10229 11636 0 16:20 ? 00:00:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/15e93faf323c5bfbc0897f09f0f3fd371d149198d96bf16ca487d971fe584db2 -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd -runtime-root /var/run/docker/runtime-runc
root 11092 6474 0 16:23 pts/0 00:00:00 grep --color=auto docker
root 11625 1 0 8月05 ? 00:13:20 /usr/bin/dockerd
root 11636 11625 0 8月05 ? 00:46:11 docker-containerd --config /var/run/docker/containerd/containerd.toml
而當我們在宿主機中,使用pstree -p查看進程樹:
[[email protected] lijianxin1]# pstree -p
發現Docker中bash進程PID爲10271:
├─dockerd(11625)─┬─docker-containe(11636)─┬─docker-containe(10229)─┬─bash(10271)
│ │ │ ├─{docker-containe}(10232)
│ │ │ ├─{docker-containe}(10233)
│ │ │ ├─{docker-containe}(10234)
│ │ │ ├─{docker-containe}(10235)
│ │ │ ├─{docker-containe}(10240)
│ │ │ ├─{docker-containe}(10245)
│ │ │ ├─{docker-containe}(10249)
│ │ │ └─{docker-containe}(10568)
│ │ ├─{docker-containe}(11637)
通過lsof查看10271進程關聯的文件:
[[email protected] lijianxin1]# lsof -p 10271
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 10271 root cwd DIR 0,38 4096 51640972 /
bash 10271 root rtd DIR 0,38 4096 51640972 /
bash 10271 root txt REG 253,1 1183448 1573083 /usr/bin/bash
bash 10271 root mem REG 253,1 1573866 /usr/lib/x86_64-linux-gnu/libnss_files-2.31.so (stat: No such file or directory)
bash 10271 root mem REG 253,1 1573801 /usr/lib/x86_64-linux-gnu/libc-2.31.so (stat: No such file or directory)
bash 10271 root mem REG 253,1 1573812 /usr/lib/x86_64-linux-gnu/libdl-2.31.so (stat: No such file or directory)
bash 10271 root mem REG 253,1 1573921 /usr/lib/x86_64-linux-gnu/libtinfo.so.6.2 (stat: No such file or directory)
bash 10271 root mem REG 253,1 1573779 /usr/lib/x86_64-linux-gnu/ld-2.31.so (stat: No such file or directory)
bash 10271 root 0u CHR 136,0 0t0 3 /dev/pts/0
bash 10271 root 1u CHR 136,0 0t0 3 /dev/pts/0
bash 10271 root 2u CHR 136,0 0t0 3 /dev/pts/0
bash 10271 root 255u CHR 136,0 0t0 3 /dev/pts/0
由此可見在當前的宿主機器上,可能就存在由上述的不同進程構成的進程樹:
這就是在使用clone創建新進程時傳入CLONE_NEWPID實現的,也就是使用Linux的命名空間實現進程的隔離,Docker容器內部的任意進程都對宿主機器的進程一無所知。
下面我們反向舉例,通過將命名空間更改爲主機,容器還可以查看系統上運行的所有其他進程。失去了Namespaces的隔離作用,容器與宿主機出現了安全隱患。
[[email protected] lijianxin1]# docker run -it --pid=host ubuntu ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 43252 3520 ? Ss Jul20 4:20 /usr/lib/system
root 2 0.0 0.0 0 0 ? S Jul20 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S Jul20 0:00 [ksoftirqd/0]
root 5 0.0 0.0 0 0 ? S< Jul20 0:00 [kworker/0:0H]
root 7 0.0 0.0 0 0 ? S Jul20 0:01 [migration/0]
root 8 0.0 0.0 0 0 ? S Jul20 0:00 [rcu_bh]
root 9 0.0 0.0 0 0 ? S Jul20 5:12 [rcu_sched]
root 10 0.0 0.0 0 0 ? S Jul20 0:04 [watchdog/0]
root 11 0.0 0.0 0 0 ? S Jul20 0:03 [watchdog/1]
root 12 0.0 0.0 0 0 ? S Jul20 0:09 [migration/1]
root 13 0.0 0.0 0 0 ? S Jul20 0:03 [ksoftirqd/1]
root 15 0.0 0.0 0 0 ? S< Jul20 0:00 [kworker/1:0H]
root 16 0.0 0.0 0 0 ? S Jul20 0:03 [watchdog/2]
root 17 0.0 0.0 0 0 ? S Jul20 0:00 [migration/2]
root 18 0.0 0.0 0 0 ? S Jul20 0:01 [ksoftirqd/2]
root 20 0.0 0.0 0 0 ? S< Jul20 0:00 [kworker/2:0H]
root 21 0.0 0.0 0 0 ? S Jul20 0:03 [watchdog/3]
root 22 0.0 0.0 0 0 ? S Jul20 0:09 [migration/3]
root 23 0.0 0.0 0 0 ? S Jul20 0:00 [ksoftirqd/3]
root 25 0.0 0.0 0 0 ? S< Jul20 0:00 [kworker/3:0H]
總結:即對於業務進程而已,Docker上的進程其實對於主機上是可見的,因爲Docker的Namespaces一般都是繼承自主機的root namespaces。我們可以通過直接掃描主機上的進程行爲來識別當前Docker中運行的進程是否有問題。
網絡Namespaces
如果Docker的容器通過Linux的命名空間完成了與宿主機進程的網絡隔離,但是卻又沒有辦法通過宿主機的網絡與整個互聯網相連,就會產生很多限制,所以Docker雖然可以通過命名空間創建一個隔離的網絡環境,但是Docker中的服務仍然需要與外界相連才能發揮作用。
每一個使用docker run啓動的容器其實都具有單獨的網絡命名空間,Docker爲我們提供了四種不同的網絡模式,Host(透明模式,和宿主機共享Namespaces)、Container(與其他容器共享namespaces)、None(有獨立的Namespaces,但沒有分配veth和IP)和Bridge(網橋模式)模式。
在這一部分,我們將介紹Docker默認的網絡設置模式:網橋模式。在這種模式下,除了分配隔離的網絡命名空間之外,Docker還會爲所有的容器設置IP地址。當Docker服務器在主機上啓動之後會創建新的虛擬網橋docker0,隨後在該主機上啓動的全部服務在默認情況下都與該網橋相連。
在默認情況下,每一個容器在創建時都會創建一對虛擬網卡,兩個虛擬網卡組成了數據的通道,其中一個會放在創建的容器中,會加入到名爲docker0網橋中。我們可以使用如下的命令來查看當前網橋的接口:
[[email protected] lijianxin1]# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024280d3380b no veth2228614
docker0會爲每一個容器分配一個新的IP地址並將docker0的IP地址設置爲默認的網關。網橋docker0通過iptables中的配置與宿主機器上的網卡相連,所有符合條件的請求都會通過iptables轉發到docker0並由網橋分發給對應的機器。
[[email protected] lijianxin1]# iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere !loopback/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 anywhere
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere
我們在當前的機器上使用docker run -d -p 6379:6379 redis命令啓動了一個新的Redis容器,在這之後我們再查看當前iptables的NAT配置就會看到在Docker的鏈中出現了一條新的規則:
DNAT tcp -- anywhere anywhere tcp dpt:6379 to:192.168.0.4:6379
上述規則會將從任意源發送到當前機器6379端口的TCP包轉發到192.168.0.4:6379所在的地址上。
我們就可以推測出Docker是如何將容器的內部的端口暴露出來並對數據包進行轉發的了;當有Docker的容器需要將服務暴露給宿主機器,就會爲容器分配一個IP地址,同時向iptables中追加一條新的規則。
掛載點Namespaces
如果一個容器需要啓動,那麼它一定需要提供一個根文件系統(rootfs),容器需要使用這個文件系統來創建一個新的進程,所有二進制的執行都必須在這個根文件系統中。
想要正常啓動一個容器就需要在rootfs中掛載以上的幾個特定的目錄,除了上述的幾個目錄需要掛載之外我們還需要建立一些符號鏈接保證系統IO不會出現問題。比如/dev目錄下的特殊設備,標準輸入與錯誤輸出等。
如果需要對掛載(mount)進行Namespaces控制,那麼就需要在新的容器進程中創建隔離的掛載點命名空間需要在clone函數中傳入CLONE_NEWNS,這樣子進程就能得到父進程掛載點的拷貝,如果不傳入這個參數子進程對文件系統的讀寫都會同步回父進程以及整個主機的文件系統。
舉個例子:
在沒聲明掛載命名空間的情況下,且docker啓動用戶爲root用戶的情況下,啓動Docker容器刪除宿主機/bin目錄下文件。
[[email protected] lijianxin1]# sudo cp /bin/touch /bin/touch.bak
[[email protected] lijianxin1]# docker run -it -v /bin/:/host/ alpine rm -f /host/touch.bak
[[email protected] bin]# ls |grep touch
Cgroup
我們通過Linux的命名空間爲新創建的進程隔離了文件系統、網絡並與宿主機器之間的進程相互隔離,但是命名空間並不能夠爲我們提供物理資源上的隔離,比如CPU或者內存,如果在同一臺機器上運行了多個對彼此以及宿主機器一無所知的『容器』,這些容器卻共同佔用了宿主機器的物理資源。
如果其中的某一個容器正在執行CPU密集型的任務,那麼就會影響其他容器中任務的性能與執行效率,導致多個容器相互影響並且搶佔資源。如何對多個容器的資源使用進行限制就成了解決進程虛擬資源隔離之後的主要問題,而Control Groups(簡稱CGroups)就是能夠隔離宿主機器上的物理資源,例如CPU、內存、磁盤I/O和網絡帶寬。
--cpu-shares #限制CPU共享
--cpuset-cpus #指定CPU佔用
--memory-reservation #指定保留內存
--kernel-memory #內核佔用內存
--blkio-weight (block IO) #blkio權重
--device-read-iops #設備讀iops
--device-write-iops #設備寫iops
容器Hids的實現
主機入侵檢測系統通常情況下是按在主機上的Agent對主機上的文件(包括可執行文件,配置文件,腳本文件等),進程,網絡情況,日誌審計情況等進行掃描和惡意識別,並且及時預警主機的安全狀況。
其實主機上單獨安裝Agent是可以完成勝任Docker的掃描和審計的。Docker的可讀寫層文件系統實際上是會掛載主機上的文件系統,可以通過從主機上來的相關文件目錄掃描來識別Webshell,惡意二進制文件等。對於業務進程而已,Docker上的進程其實對於主機上是可見的,因爲Docker的Namespaces一般都是繼承自主機的root namespaces。可以通過直接掃描主機上的進程行爲來識別當前Docker中運行的進程是否有問題。
感興趣的同學可以參考適用於Docker的開源入侵檢測系統Sysdig Falco。
Docker目前面臨的安全問題
主要分爲幾方面:
Docker自身漏洞:代碼執行、權限提升、信息泄漏、runC
Docker生態問題:Kubernetes漏洞
Docker源問題:惡意鏡像、存在漏洞的鏡像
Docker架構缺陷與安全機制:
-
Namespaces導致的:容器之間的局域網攻擊、共享root、未隔離的文件系統、默認放通所有
Cgroup導致的:DDoS攻擊耗盡資源
Docker安全基線:
-
內核級別的:Namespaces、Cgroup、SElinux
主機級別的:服務最小化、禁止掛載宿主機敏感目錄、掛載目錄權限設置爲644
網絡級別的:禁止映射特權端口、通過iptable設定規則並禁止容器之間的網絡流量
鏡像級別的:創建本地鏡像服務器、使用可信鏡像、使用鏡像掃描、合理管理鏡像標籤
容器級別的:容器以單一主進程方式運行、禁止運行SSH等高危服務、以只讀方式掛載根目錄系統
Docker安全規則
-
Docker remote api訪問控制
使用普通用戶啓動Docker服務
Docker client與Docker Daemon通訊安全
參考鏈接:
https://zhuanlan.zhihu.com/p/43586159
https://arkingc.github.io/2017/05/05/2017-05-05-docker-filesystem-overlay
https://draveness.me/docker/
作者:李建新,VIPKid資深基礎安全工程師,微信號:nixinge66
文章來源:VIPKID安全響應中心,點擊查看原文。
Kubernetes實戰培訓
Kubernetes實戰培訓將於2020年12月25日在深圳開課,3天時間帶你係統掌握Kubernetes,學習效果不好可以繼續學習。本次培訓包括:雲原生介紹、微服務;Docker基礎、Docker工作原理、鏡像、網絡、存儲、數據卷、安全;Kubernetes架構、核心組件、常用對象、網絡、存儲、認證、服務發現、調度和服務質量保證、日誌、監控、告警、Helm、實踐案例等,點擊下方圖片或者閱讀原文鏈接查看詳情。