鏡像被打包出來(lái)之后一般被保存在鏡像倉(cāng)庫(kù)中,對(duì)于Docker場(chǎng)景下,我們可以使用Docker Hub來(lái)保存和分發(fā)鏡像文件。如果讀者使用的類如阿里云提供的容器鏡像服務(wù)ACR,那么鏡像文件會(huì)被安全的保存在云供應(yīng)商的數(shù)據(jù)中心,開(kāi)發(fā)人員只需要簡(jiǎn)單的使用push和pull來(lái)推送和拉取鏡像文件。
從標(biāo)準(zhǔn)化的角度看,目前社區(qū)正在基于OCI規(guī)范來(lái)制定容器鏡像分發(fā)規(guī)范,這個(gè)規(guī)范最終肯定會(huì)和主流的如Docker Hub,阿里云ACR倉(cāng)庫(kù)兼容,雖然說(shuō)規(guī)范的標(biāo)準(zhǔn)化工作尚未完成,但向后兼容確保現(xiàn)有的容器鏡像分發(fā)服務(wù)能夠工作是規(guī)范制定過(guò)程中最高的優(yōu)先級(jí)。
容器鏡像在Docker Hub倉(cāng)庫(kù)上每層(layer)以二進(jìn)制大對(duì)象blob的形式被保存,我們可以通過(guò)唯一的hash(摘要)來(lái)確定組成容器鏡像的每一層。為了節(jié)省空間,相同的層只會(huì)被保存一次,但是可以被多個(gè)容器鏡像同時(shí)引用。另外在倉(cāng)庫(kù)中除了保存layer數(shù)據(jù)之外,每個(gè)容器鏡像還有一個(gè)manifest文件,這個(gè)文件可以看成鏡像的配置文件,里邊主要的信息是組成鏡像所有層的信息。
我們通過(guò)對(duì)鏡像的manifest進(jìn)行摘要,得到的哈希值就是鏡像的digest,如果我們對(duì)鏡像的數(shù)據(jù)做了修改,比如增加一層,或者對(duì)原有的層進(jìn)行了修改,那么重新計(jì)算鏡像的digest會(huì)的到不同的值。筆者這樣要強(qiáng)調(diào)的是,這個(gè)摘要信息是我們唯一確定鏡像的唯一標(biāo)識(shí)。可能有些同學(xué)聽(tīng)說(shuō)過(guò)鏡像的tag,筆者在前邊的文章中也多次用過(guò)k8ssample:v1.2這樣的鏡像,冒號(hào)后邊的v1.2就是tag,但是大家要注意,tag并不能唯一的確定一個(gè)容器鏡像,比如latest這個(gè)標(biāo)簽,因?yàn)槟憬裉炖〉膌atest和明天拉取的latest可能是完全不同的鏡像。
我們可以在自己的機(jī)器上運(yùn)行docker image ls --digests命令,來(lái)輸出本地機(jī)器上容器鏡像的摘要:
?? source docker image ls --digests
REPOSITORY? ? ? ? ? ? ? ? ? ? ? ? ? ? ? TAG? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? DIGEST? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? IMAGE ID? ? ? CREATED? ? ? ? SIZE
qigaopan/agents-jvm? ? ? ? ? ? ? ? ? ? latest? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? sha256:7fac45cce6181348c98c7331db85a047808e370d25dda7f28c8085dcfd02551a? eb37d3272627? 5 weeks ago? ? 466MB
當(dāng)我們使用docker pull來(lái)拉取鏡像的時(shí)候,我們可以使用摘要(digests)來(lái)精確的引用到唯一的鏡像,說(shuō)到這里,我們接著來(lái)看看如何引用到一個(gè)具體的鏡像。通常情況下我們引用鏡像的URL模式如下:
<Registry URL>/<Organization or user name>/<repository>@sha256:<digest>
<Registry URL>/<Organization or user name>/<repository>:<tag>
第一部分<Registry URL>標(biāo)識(shí)了被引用鏡像具體保存在哪個(gè)倉(cāng)庫(kù)中,如果我們?cè)谝萌萜麋R像的URL中不包含這部分,那么就默認(rèn)指向本地或者Docker Hub。
第二部分<Organization or user name>用來(lái)指定用戶或者組織的賬戶名稱,最后一部分是我們要引用的鏡像名稱和tag信息。咱們用上篇中的sensitive這個(gè)鏡像為例,我們可以通過(guò)以下兩種方式來(lái)引用這個(gè)鏡像文件:
docker pull qigaopan/sensitive
docker pull qigaopan/sensitive:sha256:db77c6f39d1e75619c49e724ba44becef79323ae466e9c2085fc8e
是用戶使用的角度看,通過(guò)摘要來(lái)引用鏡像文件略顯得繁瑣和不人道,因此大部分情況下,無(wú)論是在Docker中還是在Kubernetes中,我們一般都是通過(guò)tag來(lái)引用具體的鏡像文件。tag是邏輯上的概念,我們可以給一個(gè)鏡像文件關(guān)聯(lián)多個(gè)tag,并且也可以把某個(gè)tag從一個(gè)鏡像文件移動(dòng)到另外一個(gè)鏡像文件(比如不同的版本)。比如咱們?cè)谇斑叾嗥恼轮惺褂玫溺R像k8ssamples,我們用版本作為鏡像的tag來(lái)標(biāo)注不同版本的應(yīng)用程序,比如我們?cè)贒ocker Hub上看到的這個(gè)應(yīng)用的v1.5版本,

筆者要提醒大家的是,由于tag可以從一個(gè)鏡像被移動(dòng)到另外一個(gè)鏡像文件,因此通過(guò)k8ssamples:v1.5這樣應(yīng)用到的鏡像無(wú)法保證唯一性。這句話聽(tīng)起來(lái)很繞,咱們來(lái)舉個(gè)例子說(shuō)明一下,比如k8ssamples:v1.5這個(gè)鏡像文件,如果我今天在應(yīng)用中修復(fù)了一個(gè)缺陷,然后通過(guò)docker build重新打包構(gòu)建了這個(gè)標(biāo)簽的鏡像并推送到Docker hub倉(cāng)庫(kù),那么如果生產(chǎn)環(huán)境中有個(gè)容器實(shí)例掛了,重建后docker pull就會(huì)拉取到最新的版本(包含缺陷修復(fù)的版本),造成的結(jié)果是,生產(chǎn)環(huán)境中3個(gè)容器實(shí)例,有兩個(gè)運(yùn)行著沒(méi)有修復(fù)缺陷的版本,而1個(gè)運(yùn)行著修復(fù)了缺陷的版本,這會(huì)造成我們所說(shuō)的配置漂移問(wèn)題。
那么如何解決這個(gè)問(wèn)題呢?我們可以通過(guò)摘要來(lái)唯一的引用某個(gè)鏡像文件,因?yàn)閺脑砩现v,如果我們給鏡像增加了某些功能,會(huì)產(chǎn)生不同的摘要,因此結(jié)果就是不同的鏡像文件了。不過(guò)通過(guò)tag來(lái)引用鏡像文件很多時(shí)候符合我們的版本策略,比如咱們的k8ssamples:v1.5鏡像的例子,如果我們修復(fù)了一些缺陷,打包的時(shí)候會(huì)復(fù)用主版本號(hào)和次版本號(hào),因此結(jié)果就是打包出來(lái)的容器的tag和上個(gè)未修復(fù)這些缺陷的tag一致,好處是下次我們?cè)賞ull鏡像的時(shí)候,就可以得到修復(fù)了缺陷的版本。筆者在過(guò)往的幾個(gè)項(xiàng)目上也采用了這個(gè)策略,當(dāng)有重大缺陷修復(fù)的時(shí)候,會(huì)在業(yè)務(wù)低峰時(shí)候縮容然后擴(kuò)容,目的就是來(lái)主動(dòng)的拉取到更新缺陷后的應(yīng)用程序鏡像。
不過(guò)在有些場(chǎng)景下,鏡像的唯一性非常重要,特別是安全相關(guān)的場(chǎng)景。舉個(gè)例子,很多企業(yè)都會(huì)對(duì)鏡像進(jìn)行掃描,特別是部署到生產(chǎn)環(huán)境之前,需要確保鏡像中沒(méi)有靜態(tài)的風(fēng)險(xiǎn)。在Kuberntes環(huán)境中,我們一般會(huì)使用admission controller來(lái)對(duì)容器鏡像文件的漏洞,風(fēng)險(xiǎn)進(jìn)行掃描,掃描通過(guò)的會(huì)記錄下來(lái),下次就不會(huì)再次掃描,那么我們就需要對(duì)鏡像有唯一性判斷。如果采用tag的方式來(lái)記錄哪些鏡像掃描過(guò),那么就會(huì)有重大安全漏洞,因?yàn)閍dmission controller可能會(huì)漏掉某些已經(jīng)被惡意攻擊者植入惡意代碼的鏡像,雖然這些鏡像看起來(lái)tag沒(méi)有發(fā)生變化。有了上邊這些信息的鋪墊,接著咱們就可以名正言順的聊聊鏡像安全了。
對(duì)于鏡像安全來(lái)說(shuō),最核心的問(wèn)題是如何保障鏡像的完整性,也就是你怎么知道docker pull拉取到的鏡像是你”以為的“鏡像文件?惡意攻擊者完全可以截獲到你的docker pull請(qǐng)求,然后給你返回注入惡意代碼的鏡像,讓你覺(jué)得這是你預(yù)期的鏡像文件,然后在自己的生產(chǎn)環(huán)境將容器實(shí)例基于這個(gè)鏡像運(yùn)行起來(lái),這個(gè)時(shí)候惡意攻擊者基本上可以干任何他自己想干的事情了。上邊的例子只是冰山一角,對(duì)于容器鏡像來(lái)說(shuō),如何保障從構(gòu)建,保存到運(yùn)行這幾個(gè)階段的安全,是需要我們從整體上要來(lái)考慮的,那么具體會(huì)遇到哪些安全風(fēng)險(xiǎn)呢?請(qǐng)讀者仔細(xì)看下圖:

如上圖所示,我們從左到右來(lái)仔細(xì)梳理一下。對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),必須確保開(kāi)發(fā)的代碼沒(méi)有安全漏洞,我們一般通過(guò)代碼的靜態(tài)掃描來(lái)降低代碼和依賴的安全風(fēng)險(xiǎn)。另外從技術(shù)管理的角度,我們也需要定期的代碼評(píng)審機(jī)制,安全測(cè)試來(lái)持續(xù)的提升代碼的安全風(fēng)險(xiǎn)。筆者單獨(dú)把代碼這部分拿出來(lái)說(shuō)是有原因的,因?yàn)槿萜骰渴鸷蛡鹘y(tǒng)的部署方式在代碼這個(gè)層面,安全保障手段基本是一致的。接下來(lái)我們來(lái)看看在代碼層面之外的安全風(fēng)險(xiǎn)。
首先從鏡像構(gòu)建開(kāi)始,鏡像構(gòu)建簡(jiǎn)單理解就是將Dockerfile文件轉(zhuǎn)換成容器鏡像的過(guò)程,雖然說(shuō)這一步看起來(lái)很簡(jiǎn)單直接,實(shí)際上這個(gè)步驟有很多安全風(fēng)險(xiǎn)需要我們來(lái)應(yīng)對(duì)。構(gòu)建鏡像的指令來(lái)自于Dockerfile,并且在構(gòu)建的過(guò)程中,Dockerfile中包含的所有指令都會(huì)被執(zhí)行來(lái)生成最終的鏡像文件,如果惡意攻擊者篡改了Dockerfile,那么最終生成的鏡像文件就有很大的安全風(fēng)險(xiǎn),這也是我們保護(hù)鏡像的第一道防線,風(fēng)險(xiǎn)具體包括:
- 植入惡意代碼或者挖礦程序
- 竊取敏感數(shù)據(jù)的代碼
- 掃描生產(chǎn)環(huán)境的網(wǎng)絡(luò)拓?fù)浣Y(jié)構(gòu)
- 攻擊鏡像構(gòu)建機(jī)器來(lái)篡改鏡像數(shù)據(jù)
為了緩解這些風(fēng)險(xiǎn),首先要做的就是讓Dockerfile的訪問(wèn)和修改需要授權(quán),其次我們需要確保Dockerfile包含的指令和構(gòu)建信息沒(méi)有安全風(fēng)險(xiǎn),比如基礎(chǔ)鏡像等。筆者將鏡像涉及到的安全建議梳理如下:
- 確保基礎(chǔ)鏡像的安全。筆者經(jīng)歷的大部分容器化部署項(xiàng)目在客戶側(cè)都有專門的基礎(chǔ)鏡像來(lái)部署應(yīng)用程序,并且盡量確?;A(chǔ)鏡像只包含支持應(yīng)用程序運(yùn)行的依賴,不要包含太多無(wú)關(guān)的功能。鏡像的尺寸越小,攻擊面越小,并且傳輸起來(lái)更快
- 在構(gòu)建的過(guò)程中使用多階段構(gòu)建模式。multi-stage構(gòu)建模式可以讓我們構(gòu)建出來(lái)的鏡像尺寸更小,比如我們構(gòu)建go應(yīng)用程序的時(shí)候,需要go編譯器,但是在最終的鏡像中,go編譯器不是運(yùn)行應(yīng)用所必須的,因?yàn)槲覀兛梢詫?gòu)建的過(guò)程分為兩個(gè)階段:第一個(gè)階段通過(guò)go編譯器來(lái)構(gòu)建二進(jìn)制可執(zhí)行文件,第二階段接著第一階段將構(gòu)建出來(lái)的二進(jìn)制文件和所有的依賴打包成鏡像文件。
- Non-root用戶,在Dockerfile文件中,指定非root用戶來(lái)運(yùn)行容器應(yīng)用,因?yàn)槟J(rèn)情況下(不指定),容器實(shí)例以root權(quán)限運(yùn)行。
- 需要特別關(guān)注RUN命令,因?yàn)檫@是很多惡意攻擊者用來(lái)下載惡意程序到鏡像中的常用方式。
- 檢查容器的掛載信息,確保沒(méi)有敏感數(shù)據(jù)被掛載到容器中。
- 不要在容器中包含敏感數(shù)據(jù),具體原因我們?cè)冢ㄉ希┻@篇文章中詳細(xì)介紹過(guò)。
- 不要在容器中包含任何設(shè)置了setuid位的可執(zhí)行程序,因?yàn)檫@些程序會(huì)以文件的owner賬戶的權(quán)限運(yùn)行。
- 容器鏡像自包含,盡量杜絕容器在啟動(dòng)運(yùn)行的時(shí)候,需要從外部下載依賴包,這也是immutable image的基礎(chǔ)。
理解了如何保護(hù)容器鏡像之后,接下來(lái)我們首先看看容器進(jìn)項(xiàng)構(gòu)建機(jī)器可能遭遇的安全攻擊,以及解決方案。具體來(lái)說(shuō),構(gòu)建容器鏡像的機(jī)器主要會(huì)受到來(lái)自如下兩種類型的攻擊:
- 如果惡意攻擊者攻破了構(gòu)建機(jī)器,那么就可以以容器鏡像構(gòu)建機(jī)器為跳板,訪問(wèn)其他的機(jī)器資源。
- 如果惡意攻擊者攻破了構(gòu)建機(jī)器,那么就可以在構(gòu)建的過(guò)程中,植入惡意代碼,造成毀滅性的損失。比如在交易系統(tǒng)中嵌入交易信息截取代碼,讓公司的核心數(shù)據(jù)泄漏。
因此我們必須像對(duì)待生產(chǎn)環(huán)境一樣來(lái)提升構(gòu)建機(jī)器的安全性。比如減少構(gòu)建機(jī)器上安裝的應(yīng)用程序,構(gòu)建機(jī)器必須授權(quán)才能訪問(wèn)以及為構(gòu)建機(jī)器設(shè)置獨(dú)立的VPC和防火墻規(guī)則等?;诠P者過(guò)往的經(jīng)驗(yàn),將構(gòu)建機(jī)器和生產(chǎn)環(huán)境隔離開(kāi)來(lái)是大家最常用的一種安全策略,這樣即便是構(gòu)建機(jī)器被攻破,那么惡意攻擊者也無(wú)法輕易的就獲取到生產(chǎn)環(huán)境的訪問(wèn)權(quán)限。
解決了構(gòu)建機(jī)器的安全問(wèn)題后,接著我們需要確保構(gòu)建出來(lái)的容器鏡像被安全的存儲(chǔ)。如果惡意攻擊者很容易就用篡改過(guò)的鏡像來(lái)代替我們辛辛苦苦構(gòu)建出來(lái)的鏡像,那么運(yùn)行在生產(chǎn)環(huán)境的應(yīng)用程序就有極大的風(fēng)險(xiǎn)包含惡意代碼。
筆者過(guò)往的項(xiàng)目中,大部分客戶都會(huì)運(yùn)行自己的Harbor倉(cāng)庫(kù)來(lái)保存容器鏡像,并且在Harbor上開(kāi)啟權(quán)限控制,這樣就能最大限度的確保鏡像的存儲(chǔ)安全。這種私有部署的倉(cāng)庫(kù)也完全解決了DNS劫持的風(fēng)險(xiǎn),因此建議大家項(xiàng)目上如果對(duì)鏡像安全要求很高,這種私有化部署是最佳的選項(xiàng)。
如果項(xiàng)目對(duì)阿里云的ACR或者Docker Hub這樣的外部倉(cāng)庫(kù)可以接受,那么建議國(guó)內(nèi)的用戶最好用阿里云提供的ACR,無(wú)論從安全性還是易用性,以及和阿里云的EDAS,ACK集成度來(lái)看,阿里云的ACR都是最佳的選擇。
最后我們來(lái)聊聊部署安全。首先需要確保部署使用的YAML文件的安全,這和我們保護(hù)Dockerfile安全的手段一致,筆者這里不再累述。另外有時(shí)候我們可能會(huì)從外網(wǎng)下載YAML文件,筆者這里要強(qiáng)調(diào)的是,下載的YAML文件一定要經(jīng)過(guò)嚴(yán)格的評(píng)審,要不然可能會(huì)包含惡意的指令來(lái)下載或者掛載惡意程序,導(dǎo)致敏感數(shù)據(jù)被竊取。
在Kuberntes部署環(huán)境中,我們可以使用admission control機(jī)制來(lái)評(píng)估所有需要部署到集群中資源的安全風(fēng)險(xiǎn),比如基礎(chǔ)鏡像是不是符合公司的安全規(guī)范要求等,如果評(píng)估結(jié)果不通過(guò),那么資源是不會(huì)被寫(xiě)到etcd,也就不會(huì)被部署到集群中。具體來(lái)說(shuō)admission controller可以進(jìn)行如下列出的安全檢查:
- 鏡像是否已經(jīng)經(jīng)過(guò)安全的靜態(tài)掃描
- 鏡像是否來(lái)自于可信的倉(cāng)庫(kù)
- 鏡像是否持有可行的簽名
- 鏡像是否被批準(zhǔn)可以部署
- 鏡像是否以root權(quán)限運(yùn)行
通過(guò)admission controller我們就可以嚴(yán)格的控制資源是否可以被部署到集群中,這是很多集群的標(biāo)配。建議讀者使用阿里的ACK托管服務(wù),通過(guò)簡(jiǎn)單的配置就可以實(shí)現(xiàn)admission controller提供的所有安全保障。
今天這篇文章的主要內(nèi)容就這么多了,在文章的結(jié)尾處,我們來(lái)聊一下GitOps,Gitops是一套通過(guò)源代碼控制系統(tǒng)來(lái)管理系統(tǒng)狀態(tài)的方法論。在GitOps模式下,如果我們要對(duì)生產(chǎn)環(huán)境做變更,我們要做的是修改YAML文件到期望的狀態(tài)(比如服務(wù)實(shí)例的個(gè)數(shù)從3個(gè)變成30個(gè)來(lái)支持即將到來(lái)的雙十一大促),然后將修改過(guò)的YAML文件check in到Y(jié)AML文件管理倉(cāng)庫(kù),后臺(tái)運(yùn)行的自動(dòng)化工具(我們一般稱之為GitOps operator)將修改后的YAML文件apply到集群,通過(guò)集群中的控制器來(lái)將應(yīng)用的狀態(tài)拉到期望的狀態(tài)。讀者需要注意的是,GitOps模式下,我們不再直接修改系統(tǒng)的狀態(tài),而是以源代碼管理的方式來(lái)提交系統(tǒng)狀態(tài)的期望。
而Gitops這種模式對(duì)系統(tǒng)安全來(lái)說(shuō),具有深遠(yuǎn)的意義,因?yàn)橛脩舨辉僦苯釉L問(wèn)生產(chǎn)系統(tǒng),從而造成的結(jié)果就是系統(tǒng)的攻擊面大大減小了,如下圖所示:

咱們下篇文章繼續(xù)討論如何確保鏡像中應(yīng)用程序的安全,敬請(qǐng)期待!