咱們在增強容器隔離性兩篇文章中給大家介紹了通過約束容器對資源的訪問來增強隔離性,讀過文章的同學(xué)可能有這樣的疑問,我覺得默認(rèn)的隔離性也沒有什么問題啊,為啥要費那么多事呢?坦白講筆者接觸過很多同學(xué),基本上都有這個疑問。那么今天這篇文章我們就從剖析一下默認(rèn)配置下,我們所說的這種安全風(fēng)險到底是什么,以期讓大家對系列文章介紹的安全措施有更加深刻全面的理解。
注:這篇文章的目的是告訴讀者容器部署如果不做配置和安全方面的約束,可能會造成嚴(yán)重的數(shù)據(jù)泄露或者安全問題,這些內(nèi)容并不是最佳實踐,希望讀者謹(jǐn)慎對待!
容器化部署最為人詬病的就是容器實例以root權(quán)限運行這個事實,不過這句話本身并不會引起很多人的注意,因為操作系統(tǒng)非常復(fù)雜,再結(jié)合虛擬化,root這個概念也會有很多不同的理解。本質(zhì)上把容器默認(rèn)情況下以root權(quán)限運行說清楚其實很簡單,直接在安裝了Docker Desktop的機器上啟動一個容器實例就行(當(dāng)然不改變?nèi)魏闻渲脜?shù))。
筆者在自己的macOS上運行如下的命令:
?? source whoami
gaopanqi
?? source docker run -it alpine sh
/ # whoami
root
/ #
從輸出的結(jié)果大家不難看出,雖說gaopanqi這個賬戶并不是root賬戶,但是通過這個賬戶創(chuàng)建的容器實例,竟然以root賬戶運行。讀者可能會反駁說,在容器里的賬戶是root,并不代表在宿主機上也是root賬戶啊。咱們來繼續(xù)分析。首先通過命令docker exec -it 3e62e14bff5b sh打開另外一個終端(注意替換為自己的容器實例ID),在這個新打開的終端中運行sleep 100來啟動sleep進(jìn)程,并睡眠100秒,目的是我們可以有時間從宿主機上查看這個進(jìn)程的運行情況。
然后我們在宿主機上(注意,如果讀者使用的macOS,需要使用特殊的技巧來登陸到這臺基于HyperKit的VM上)運行ps -f sleep,返回的結(jié)果如下:
/ # ps -f sleep
PID? USER? ? TIME? COMMAND
3855 root? ? ? 0:00 sleep 100
從輸出的PID可以看出,這個進(jìn)程在宿主機上的ID為3855,并且也證實了進(jìn)程在宿主機上也是以root權(quán)限運行這個事實。
注:在macOS上進(jìn)入Docker Desktop創(chuàng)建的虛擬機有兩種方式,第一種方式是使用screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty的方式;第二種方式是通過共享命名空間的方式來訪問宿主上的進(jìn)程列表,因此在macOS上運行docker run -it --rm --privileged --pid=host justincormack/nsenter1,在打開的終端上運行自己的命令,比如上邊的ps -f sleep。
如果我們使用的運行時是runc,那么相同的例子就沒有那么有說服力了,主要原因是只有root權(quán)限的賬戶才能運行runc容器實例。背后的邏輯想必了解Docker的同學(xué)也很清楚,我們發(fā)送docker run指令給deamon,而不是直接通過docker客戶端來運行,而deamon本身就是以root權(quán)限運行,因此創(chuàng)建容器實例需要的各種權(quán)限都已經(jīng)持有。反過來看runc,因為這是一種deamonless的機制,因此runc本身就必須持有創(chuàng)建命名空間等權(quán)限,才能順利的完成容器實例的創(chuàng)建。
通過上邊這個例子,筆者希望大家能夠深信默認(rèn)情況下容器以root賬戶運行這個事實,因為這是我們后邊很多討論的基礎(chǔ)。從安全的角度,我們以非root賬號”gaopanqi“啟動容器,但是運行起來的容器實例在宿主機上以root權(quán)限運行,這是典型的權(quán)限提升場景,雖然說對運行在容器中的應(yīng)用和容器實例來說不礙事,但是這種權(quán)限提升的場景,大家一定要小心,是很多安全攻擊的來源。舉個例子,如果惡意攻擊者能夠從容器中逃逸,那么在宿主機上就是root權(quán)限了,root權(quán)限意味著惡意攻擊者可以干任何事情,我們肯定不希望這種場景在生產(chǎn)環(huán)境出現(xiàn)。
容器實例默認(rèn)以root權(quán)限運行的場景當(dāng)然Docker公司也清楚,因此Docker提供了讓容器實例以非root用戶運行的方法,我們可以在容器鏡像的Dockerfile文件中使用USER指令來指定非root賬戶,或者使用rootless容器,接下來咱們詳細(xì)的介紹一下。
對于OCI-compliant類型的運行時,比如Docker,我們可以在Dockerfile中使用USER指令來指定容器實例運行的賬號,另外Docker也提供了在命令行通過--user參數(shù)來重新鏡像中制定用戶的機制,比如我們可以通過命令docker run -it --user 1000 ubuntu bash來讓容器實例以用戶ID 1000運行,在筆者的機器上輸出如下:
?? source docker run -it --user 1000 ubuntu bash
I have no name!@362ce2754e62:/$ ps
? PID TTY? ? ? ? ? TIME CMD
? ? 1 pts/0? ? 00:00:00 bash
? 10 pts/0? ? 00:00:00 ps
I have no name!@362ce2754e62:/$ whoami
whoami: cannot find name for user ID 1000
從輸出的信息可以看出,容器實例運行在賬戶1000下。讀者需要特別注意的是,我們從Docker Hub或者很多其他的倉庫下載的鏡像,基本上都不會通過Dockerfile添加USER指令的方式來修改運行賬戶,原因很簡單,構(gòu)建容器鏡像的時候不知道目標(biāo)環(huán)境啊。因此大部分從外網(wǎng)下載的容器鏡像默認(rèn)情況下都是以root權(quán)限運行,如果我們不加限制,那么默認(rèn)情況下,容器實例就是宿主機上以持有root權(quán)限的進(jìn)程運行。
咱們還是繼續(xù)通過例子來佐證一下。云原生架構(gòu)中要解決一個很重要的問題就是connectivity,連接性描述的是前端如何連接到我們的服務(wù),以及服務(wù)實例之間如何連接來共同對外提供服務(wù)。Nginx是我們用的比較多的負(fù)載均衡器,不過Nginx出現(xiàn)的時間遠(yuǎn)比容器化部署要早,因此Nginx從設(shè)計之初就是針對物理機或者虛擬機這樣的部署場景,簡單說就是直接運行在服務(wù)器上。不過隨著容器化部署的成熟,我們有強烈的訴求,在容器中將Nginx服務(wù)運行起來,因此大家可以在Docker Hub上找到很多Nginx鏡像。
咱們來啟動一個nginx鏡像實例驗證一下,看看Docker Hub上的nginx具體以什么類型的賬戶在運行。在自己的機器上運行docker run -d --name nginx nginx命令,筆者機器上輸出如下:
?? source docker run -d --name nginx nginx
0ed94b581e427a13e8d9ad53cf9b26e9cecad19d5808936a0c7368376d525308
?? source docker top nginx
UID? ? ? ? ? ? ? ? PID? ? ? ? ? ? ? ? PPID? ? ? ? ? ? ? ? C? ? ? ? ? ? ? ? ? STIME? ? ? ? ? ? ? TTY? ? ? ? ? ? ? ? TIME? ? ? ? ? ? ? ? CMD
root? ? ? ? ? ? ? ? 4145? ? ? ? ? ? ? ? 4118? ? ? ? ? ? ? ? 0? ? ? ? ? ? ? ? ? 02:20? ? ? ? ? ? ? ?? ? ? ? ? ? ? ? ? 00:00:00? ? ? ? ? ? nginx: master process nginx -g daemon off;
uuidd? ? ? ? ? ? ? 4202? ? ? ? ? ? ? ? 4145? ? ? ? ? ? ? ? 0? ? ? ? ? ? ? ? ? 02:20? ? ? ? ? ? ? ?? ? ? ? ? ? ? ? ? 00:00:00? ? ? ? ? ? nginx: worker process
從輸出的信息可以看到,nginx server以root的權(quán)限運行,這也不難理解,畢竟nginx服務(wù)器要監(jiān)聽80端口,而監(jiān)聽(Open)小于1024的端口需要CAP_NET_BIND_SERVICE權(quán)限(能力),解決這個問題最簡單的方案就是讓Nginx以root賬戶運行。但是熟悉容器網(wǎng)絡(luò)的同學(xué)就不淡定了,不是有端口映射嘛,誰說容器中運行的應(yīng)用一定非要監(jiān)聽80啊,從宿主機host到容器實例做個端口映射不就行了,因此容器實例根本不需要這個CAP_NET_BIND_SERVICE的能力,因此也不需要root權(quán)限。
考慮到容器實例以root權(quán)限運行會有安全風(fēng)險,也有很多廠商提供了以非root權(quán)限運行的Nginx鏡像,比如https://github.com/nginxinc/docker-nginx-unprivileged,感興趣的同學(xué)可以嘗試一下。
筆者聽到另外一個需要root權(quán)限運行容器實例的理由是在安裝外部依賴,其實編寫過Dockerfile的同學(xué),通過docker build打包過容器鏡像的同學(xué),都應(yīng)該清楚yum或者apt這樣的指令只是在容器構(gòu)建階段進(jìn)行外部依賴的安裝,而當(dāng)鏡像打包好之后,我們一般不會在容器實例啟動的時候來安裝外部的應(yīng)用,這也是很多公司的安全標(biāo)準(zhǔn)中,特別是針對Dockerfile的安全規(guī)范中,最后一句基本都是USER指令來讓基于這個鏡像運行的實例使用非root賬號。
另外集合筆者多年的實戰(zhàn)經(jīng)驗,強烈建議大家不要在容器實例啟動后安裝外部的軟件包,這不光可運維性的問題,本質(zhì)上會造成很多安全的風(fēng)險,羅列如下:
- 特別的低效,如果你要在每個容器實例中安裝外部的依賴,干嘛不直接在鏡像構(gòu)建階段解決?
- 容器運行期間安裝的外部依賴,誰為這些安裝包,三方庫,jar包的漏洞負(fù)責(zé),或者說這些實時下載的軟件包是否包含安全漏洞,如何掃描?
- 運行時下載安裝的軟件包版本可能會有差異,這樣當(dāng)我們知道某個版本的軟件包有漏洞,需要更新替換的時候,作為運維和開發(fā)人員,我們?nèi)绾沃滥男嵗惭b了哪些版本的應(yīng)用?在容器實例特的數(shù)量別龐大的場景下,手動來確認(rèn)幾乎不可行。
- 考慮到這些風(fēng)險,筆者強力建議容器實例掛載的文件系統(tǒng)設(shè)置為只讀,如果容器實例要寫數(shù)據(jù),可以寫到特定的區(qū)域,這樣做的目的是禁止容器實例在運行期間下載外部的軟件包并安裝。
- 最后,特別是我們反復(fù)強調(diào)過容器的immutable特性以及帶來的好處(安全角度),因此大家如果允許容器實例運行期間下載和安裝軟件包,本質(zhì)上是和immutable理念相悖。
還有些同學(xué)說我的容器實例在運行期間需要修改操作系統(tǒng)內(nèi)核,加載特定的模塊等,如果你打算讓啟動的容器實例這么干,筆者建議大家想清楚因此帶來的巨大風(fēng)險,大白話來說:要對自己的行為負(fù)責(zé)。
注:說到容器運行時權(quán)限的問題,不能不提Kubernetes啊,大家可以參考這個地址:https://github.com/lizrice/running-with-scissors,了解一下以root權(quán)限運行容器實例造成的安全風(fēng)險。
好了,這篇文章的內(nèi)容就這么多了,咱們下篇文章繼續(xù)討論如何以rootless的方式運行容器,解決默認(rèn)以root權(quán)限可能造成的風(fēng)險,敬請期待!