回復「資源」獲取獨家整理的學習資料!
Linux 是一種安全的作業系統,它把所有的系統權限都賦予了一個單一的 root 用戶,只給普通用戶保留有限的權限。root 用戶擁有超級管理員權限,可以安裝軟體、允許某些服務、管理用戶等。
作為普通用戶,如果想執行某些只有管理員才有權限的操作,以前只有兩種辦法:一是通過sudo提升權限,如果用戶很多,配置管理和權限控制會很麻煩;二是通過 SUID(Set User ID on execution)來實現,它可以讓普通用戶允許一個owner為 root 的可執行文件時具有 root 的權限。
SUID的概念比較晦澀難懂,舉個例子就明白了,以常用的passwd命令為例,修改用戶密碼是需要 root 權限的,但普通用戶卻可以通過這個命令來修改密碼,這就是因為/bin/passwd被設置了SUID標識,所以普通用戶執行 passwd 命令時,進程的 owner 就是 passwd 的所有者,也就是 root 用戶。
SUID雖然可以解決問題,但卻帶來了安全隱患。當運行設置了SUID的命令時,通常只是需要很小一部分的特權,但是SUID給了它 root 具有的全部權限。這些可執行文件是黑客的主要目標,如果他們發現了其中的漏洞,就很容易利用它來進行安全攻擊。簡而言之,SUID機制增大了系統的安全攻擊面。
為了對 root 權限進行更細粒度的控制,實現按需授權,Linux 引入了另一種機制叫capabilities。
雙十一虛擬機大促,主打機型大橫評!
01
Linux capabilities 是什麼?
Capabilities機制是在 Linux 內核2.2之後引入的,原理很簡單,就是將之前與超級用戶 root(UID=0)關聯的特權細分為不同的功能組,Capabilites 作為線程(Linux 並不真正區分進程和線程)的屬性存在,每個功能組都可以獨立啟用和禁用。其本質上就是將內核調用分門別類,具有相似功能的內核調用被分到同一組中。
這樣一來,權限檢查的過程就變成了:在執行特權操作時,如果線程的有效身份不是 root,就去檢查其是否具有該特權操作所對應的 capabilities,並以此為依據,決定是否可以執行特權操作。
Capabilities 可以在進程執行時賦予,也可以直接從父進程繼承。所以理論上如果給 nginx 可執行文件賦予了CAP_NET_BIND_SERVICEcapabilities,那麼它就能以普通用戶運行並監聽在 80 埠上。
02
capabilities 的賦予和繼承
Linux capabilities 分為進程 capabilities 和文件 capabilities。對於進程來說,capabilities 是細分到線程的,即每個線程可以有自己的capabilities。對於文件來說,capabilities 保存在文件的擴展屬性中。
下面分別介紹線程(進程)的 capabilities 和文件的 capabilities。
線程的 capabilities
每一個線程,具有 5 個 capabilities 集合,每一個集合使用64位掩碼來表示,顯示為16進位格式。這 5 個 capabilities 集合分別是:
PermittedEffectiveInheritableBoundingAmbient每個集合中都包含零個或多個 capabilities。這5個集合的具體含義如下:
Permitted
定義了線程能夠使用的 capabilities 的上限。它並不使能線程的 capabilities,而是作為一個規定。也就是說,線程可以通過系統調用capset()來從Effective或Inheritable集合中添加或刪除 capability,前提是添加或刪除的 capability 必須包含在Permitted集合中(其中 Bounding 集合也會有影響,具體參考下文)。如果某個線程想向Inheritable集合中添加或刪除 capability,首先它的Effective集合中得包含CAP_SETPCAP這個 capabiliy。
Effective
內核檢查線程是否可以進行特權操作時,檢查的對象便是Effective集合。如之前所說,Permitted集合定義了上限,線程可以刪除 Effective 集合中的某 capability,隨後在需要時,再從 Permitted 集合中恢復該 capability,以此達到臨時禁用 capability 的功能。
Inheritable
當執行exec()系統調用時,能夠被新的可執行文件繼承的 capabilities,被包含在Inheritable集合中。這裡需要說明一下,包含在該集合中的 capabilities 並不會自動繼承給新的可執行文件,即不會添加到新線程的Effective集合中,它只會影響新線程的Permitted集合。
Bounding
Bounding集合是Inheritable集合的超集,如果某個 capability 不在Bounding集合中,即使它在Permitted集合中,該線程也不能將該 capability 添加到它的Inheritable集合中。
Bounding 集合的 capabilities 在執行fork()系統調用時會傳遞給子進程的 Bounding 集合,並且在執行execve系統調用後保持不變。
當線程運行時,不能向 Bounding 集合中添加 capabilities。一旦某個 capability 被從 Bounding 集合中刪除,便不能再添加回來。將某個 capability 從 Bounding 集合中刪除後,如果之前Inherited集合包含該 capability,將繼續保留。但如果後續從Inheritable集合中刪除了該 capability,便不能再添加回來。Ambient
Linux4.3內核新增了一個 capabilities 集合叫Ambient,用來彌補Inheritable的不足。Ambient具有如下特性:
Permitted和Inheritable未設置的 capabilities,Ambient也不能設置。當Permitted和Inheritable關閉某權限(比如CAP_SYS_BOOT)後,Ambient也隨之關閉對應權限。這樣就確保了降低權限後子進程也會降低權限。非特權用戶如果在Permitted集合中有一個 capability,那麼可以添加到Ambient集合中,這樣它的子進程便可以在Ambient、Permitted和Effective集合中獲取這個 capability。現在不知道為什麼也沒關係,後面會通過具體的公式來告訴你。Ambient的好處顯而易見,舉個例子,如果你將CAP_NET_ADMIN添加到當前進程的Ambient集合中,它便可以通過fork()和execve()調用 shell 腳本來執行網絡管理任務,因為CAP_NET_ADMIN會自動繼承下去。
文件的 capabilities
文件的 capabilities 被保存在文件的擴展屬性中。如果想修改這些屬性,需要具有CAP_SETFCAP的 capability。文件與線程的 capabilities 共同決定了通過execve()運行該文件後的線程的 capabilities。
文件的 capabilities 功能,需要文件系統的支持。如果文件系統使用了nouuid選項進行掛載,那麼文件的 capabilities 將會被忽略。
類似於線程的 capabilities,文件的 capabilities 包含了 3 個集合:
PermittedInheritableEffective這3個集合的具體含義如下:
Permitted
這個集合中包含的 capabilities,在文件被執行時,會與線程的 Bounding 集合計算交集,然後添加到線程的Permitted集合中。
Inheritable
這個集合與線程的Inheritable集合的交集,會被添加到執行完execve()後的線程的Permitted集合中。
Effective
這不是一個集合,僅僅是一個標誌位。如果設置開啟,那麼在執行完execve()後,線程Permitted集合中的 capabilities 會自動添加到它的Effective集合中。對於一些舊的可執行文件,由於其不會調用 capabilities 相關函數設置自身的Effective集合,所以可以將可執行文件的 Effective bit 開啟,從而可以將Permitted集合中的 capabilities 自動添加到Effective集合中。
詳情請參考 Linux capabilities 的 man page。
03
運行 execve() 後 capabilities 的變化
上面介紹了線程和文件的 capabilities,你們可能會覺得有些抽象難懂。下面通過具體的計算公式,來說明執行execve()後 capabilities 是如何被確定的。
我們用P代表執行execve()前線程的 capabilities,P'代表執行execve()後線程的 capabilities,F代表可執行文件的 capabilities。那麼:
P'(ambient) = (file is privileged) ? 0 : P(ambient)P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding))) | P'(ambient)P'(effective) = F(effective) ? P'(permitted) : P'(ambient)P'(inheritable) = P(inheritable) [i.e., unchanged]P'(bounding) = P(bounding) [i.e., unchanged]
我們一條一條來解釋:
如果用戶是 root 用戶,那麼執行execve()後線程的Ambient集合是空集;如果是普通用戶,那麼執行execve()後線程的Ambient集合將會繼承執行execve()前線程的Ambient集合。執行execve()前線程的Inheritable集合與可執行文件的Inheritable集合取交集,會被添加到執行execve()後線程的Permitted集合;可執行文件的 capability bounding 集合與可執行文件的Permitted集合取交集,也會被添加到執行execve()後線程的Permitted集合;同時執行execve()後線程的Ambient集合中的 capabilities 會被自動添加到該線程的Permitted集合中。如果可執行文件開啟了 Effective 標誌位,那麼在執行完execve()後,線程Permitted集合中的 capabilities 會自動添加到它的Effective集合中。執行execve()前線程的Inheritable集合會繼承給執行execve()後線程的Inheritable集合。這裡有幾點需要著重強調:
上面的公式是針對系統調用execve()的,如果是fork(),那么子線程的 capabilities 信息完全複製父進程的 capabilities 信息。可執行文件的Inheritable集合與線程的Inheritable集合併沒有什麼關係,可執行文件Inheritable集合中的 capabilities 不會被添加到執行execve()後線程的Inheritable集合中。如果想讓新線程的Inheritable集合包含某個 capability,只能通過capset()將該 capability 添加到當前線程的Inheritable集合中(因為 P'(inheritable) = P(inheritable))。如果想讓當前線程Inheritable集合中的 capabilities 傳遞給新的可執行文件,該文件的Inheritable集合中也必須包含這些 capabilities(因為 P'(permitted) = (P(inheritable) & F(inheritable))|...)。將當前線程的 capabilities 傳遞給新的可執行文件時,僅僅只是傳遞給新線程的Permitted集合。如果想讓其生效,新線程必須通過capset()將 capabilities 添加到Effective集合中。或者開啟新的可執行文件的 Effective 標誌位(因為 P'(effective) = F(effective) ? P'(permitted) : P'(ambient))。在沒有Ambient集合之前,如果某個腳本不能調用capset(),但想讓腳本中的線程都能獲得該腳本的Permitted集合中的 capabilities,只能將Permitted集合中的 capabilities 添加到Inheritable集合中(P'(permitted) = P(inheritable) & F(inheritable)|...),同時開啟 Effective 標誌位(P'(effective) = F(effective) ? P'(permitted) : P'(ambient))。有 有Ambient集合之後,事情就變得簡單多了,後續的文章會詳細解釋。如果某個 UID 非零(普通用戶)的線程執行了execve(),那麼Permitted和Effective集合中的 capabilities 都會被清空。從 root 用戶切換到普通用戶,那麼Permitted和Effective集合中的 capabilities 都會被清空,除非設置了 SECBIT_KEEP_CAPS 或者更寬泛的 SECBIT_NO_SETUID_FIXUP。關於上述計算公式的邏輯流程圖如下所示(不包括Ambient集合):
04
簡單示例
下面我們用一個例子來演示上述公式的計算邏輯,以ping文件為例。如果我們將CAP_NET_RAW capability添加到 ping 文件的Permitted集合中(F(Permitted)),它就會添加到執行後的線程的Permitted集合中(P'(Permitted))。由於 ping 文件具有capabilities 意識,即能夠調用capset()和capget(),它在運行時會調用capset()將CAP_NET_RAWcapability 添加到線程的Effective集合中。
換句話說,如果可執行文件不具有capabilities 意識,我們就必須要開啟 Effective 標誌位(F(Effective)),這樣就會將該 capability 自動添加到線程的Effective集合中。具有capabilities 意識的可執行文件更安全,因為它會限制線程使用該 capability 的時間。
我們也可以將 capabilities 添加到文件的Inheritable集合中,文件的Inheritable集合會與當前線程的Inheritable集合取交集,然後添加到新線程的Permitted集合中。這樣就可以控制可執行文件的運行環境。
看起來很有道理,但有一個問題:如果可執行文件的有效用戶是普通用戶,且沒有Inheritable集合,即F(inheritable) = 0,那麼P(inheritable)將會被忽略(P(inheritable) & F(inheritable))。由於絕大多數可執行文件都是這種情況,因此Inheritable集合的可用性受到了限制。我們無法讓腳本中的線程自動繼承該腳本文件中的 capabilities,除非讓腳本具有capabilities 意識。
要想改變這種狀況,可以使用Ambient集合。Ambient集合會自動從父線程中繼承,同時會自動添加到當前線程的Permitted集合中。舉個例子,在一個 Bash 環境中(例如某個正在執行的腳本),該環境所在的線程的Ambient集合中包含CAP_NET_RAWcapability,那麼在該環境中執行 ping 文件可以正常工作,即使該文件是普通文件(沒有任何 capabilities,也沒有設置 SUID)。
05
終極案例
最後拿 docker 舉例,如果你使用普通用戶來啟動官方的 nginx 容器,會出現以下錯誤:
bind() to 0.0.0.0:80 failed (13: Permission denied)因為 nginx 進程的Effective集合中不包含CAP_NET_BIND_SERVICEcapability,且不具有capabilities 意識(普通用戶),所以啟動失敗。要想啟動成功,至少需要將該 capability 添加到 nginx 文件的Inheritable集合中,同時開啟 Effective 標誌位,並且在 Kubernetes Pod 的部署清單中的 securityContext --> capabilities 欄位下面添加NET_BIND_SERVICE(這個 capability 會被添加到 nginx 進程的Bounding集合中),最後還要將 capability 添加到 nginx 文件的Permitted集合中。如此一來就大功告成了,參考公式:P'(permitted) = ...|(F(permitted) & P(bounding)))|...和P'(effective) = F(effective) ? P'(permitted) : P'(ambient)。
如果容器開啟了securityContext/allowPrivilegeEscalation,上述設置仍然可以生效。如果 nginx 文件具有capabilities 意識,那麼只需要將CAP_NET_BIND_SERVICEcapability 添加到它的Inheritable集合中就可以正常工作了。
當然了,除了上述使用文件擴展屬性的方法外,還可以使用Ambient集合來讓非 root 容器進程正常工作,但 Kubernetes 目前還不支持這個屬性,具體參考 Kubernetes 項目的 issue。
雖然 Kubernetes 官方不支持,但我們可以自己來實現,具體實現方式可以關注我後續的文章。
06
參考資料
Linux Capabilities: Why They Exist and How They WorkUnderstanding Capabilities in LinuxLinux Capabilities in a nutshellLinux的capabilities機制
雙十一虛擬機大促,主打機型大橫評!
本文通過OpenWrite的免費Markdown轉換工具發布
留言交流不過癮
關注我,回復「加群」加入各種主題討論群
從零搭建創業公司後臺技術棧別死寫代碼,這 25 條比漲工資都重要效率至上:K8S一鍵部署了解一下?Java 14 可能帶來什麼新特性?面試最後一問:你有什麼問題想問我嗎?
朕已閱