能力與非 root 運行#

容器內常常以 root 身分跑,但「容器裡的 root」其實被削弱了不少 — 它拿不到主機真正 root 的全部權力。讓這件事成立的核心機制是 Linux 能力(Capabilities):它把過去「全有或全無」的 root 特權拆成數十個獨立的小權限,可逐一授予或剝奪。搭配 user namespace 的 UID 重映射,容器才能在「看起來是 root」與「實際上沒那麼危險」之間取得平衡。

傳統 setuid 的「全有或全無」問題#

在能力機制出現前,Linux 的權限模型是二元的:UID 0(root)幾乎能做任何事,非 root 則受層層限制。要讓一個普通使用者執行需要特權的程式(例如 ping 要開 raw socket),傳統做法是給執行檔加上 setuid bit,讓它以 root 身分執行。

  • setuid 程式一旦被攻破,攻擊者就拿到完整 root,而不是只拿到那個程式需要的單一權限
  • 「只想給開 raw socket 的權限」卻不得不「給全部 root」,權限放大得太多
  • 最小權限原則(principle of least privilege)在這個模型下難以落實

能力機制就是為了拆解 root 而生:把 root 的權力切成一個個 capability,需要哪個就給哪個。

常見 Capability 分類#

Linux 有數十個 capability,以下是與容器最相關的幾類:

Capability賦予的權力風險等級
CAP_NET_ADMIN設定網路介面、路由、iptables
CAP_NET_BIND_SERVICE綁定 1024 以下的特權埠
CAP_NET_RAW使用 raw / packet socket(如 ping)
CAP_CHOWN任意變更檔案擁有者
CAP_DAC_OVERRIDE繞過檔案讀寫執行的權限檢查
CAP_SETUID / CAP_SETGID任意設定 UID / GID
CAP_SYS_PTRACE對其他行程做 ptrace(除錯、注入)
CAP_SYS_ADMIN巨量管理操作(mount、namespace、許多 syscall)極高

四組 Capability Set#

每個行程攜帶多組能力集合,理解它們的關係才能讀懂 /proc/<pid>/status 裡的那幾行:

  • Permitted(P):行程「被允許擁有」的能力上限。能力不能超出這個集合
  • Effective(E):行程「此刻實際生效」的能力。核心做權限檢查時看的是這組
  • Inheritable(I):在 execve 跨越執行檔時,可能被保留下來的能力(傳統機制,限制多)
  • Ambient(A):較新引入,補足 inheritable 的不足,讓非 setuid 程式也能在 exec 後保有指定能力

關係簡述:

  • 程式可以把 permitted 裡的能力「點亮」到 effective(需要時才生效),用完再關掉,縮小受攻擊面
  • effective 不能包含 permitted 以外的能力
  • exec 新程式時,最終能力由 permitted / inheritable / ambient 與執行檔的 file capabilities 共同計算
# 查看某行程的能力集合(CapPrm / CapEff / CapInh / CapBnd / CapAmb)
grep Cap /proc/<pid>/status

# 把十六進位的能力遮罩解碼成可讀名稱
capsh --decode=00000000a80425fb

# 查看 / 設定執行檔上的 file capability
getcap /usr/bin/ping            # 例如 cap_net_raw=ep
sudo setcap cap_net_raw+ep ./mytool

Docker 的 –cap-drop / –cap-add#

容器執行環境預設就會丟掉一大票危險能力,只保留容器一般運作所需的一小組。可以再進一步增減:

# 丟掉全部能力,再只加回真正需要的(最小權限做法)
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp

# 反例:加上 SYS_ADMIN(除非真的必要,否則別這樣)
docker run --cap-add=SYS_ADMIN risky-app

實務原則:

  • 起手式 --cap-drop=ALL,再依需求 --cap-add 個別加回,而非反過來
  • 大多數應用其實一個 capability 都不需要(純跑業務邏輯、不碰網路設定也不碰 mount)
  • 需要綁 80 / 443 埠時,加 NET_BIND_SERVICE 即可,不必給整個 root

User Namespace 與 UID 重映射#

能力解決「root 能做什麼」,user namespace 則解決「容器裡的 root 對映到主機的誰」。

  • user namespace 讓容器內的 UID 0(root)映射到主機上的某個非特權 UID(例如 100000)
  • 容器內程式以為自己是 root、id 顯示 uid=0,但對主機核心而言它只是一個普通使用者
  • 即使容器內 root 被攻破、又設法逃逸到主機檔案系統,它在主機上也只有那個非特權 UID 的權限
# 觀察 user namespace 的 UID 映射關係
cat /proc/<pid>/uid_map
# 格式:<容器內起始 UID> <主機對應起始 UID> <範圍長度>
# 例如:0 100000 65536  → 容器內 0~65535 映射到主機 100000~165535

這是縱深防禦(defence in depth)的關鍵一環:把「容器內 root」與「主機 root」徹底脫鉤。能力削弱了容器 root 的權力,user namespace 則讓這個 root 連身分都不是真的。

seccomp 與 AppArmor(一句帶過)#

除了能力與 user namespace,容器還有兩道常見的補強:seccomp 用過濾器限制容器能呼叫哪些系統呼叫(syscall),縮小核心攻擊面;AppArmor(或 SELinux)則以強制存取控制(MAC)規範行程能存取的檔案與資源。兩者與能力機制互補,共同構成容器的安全層。

串起本章的隔離全貌#

把 07 到 10 節合起來看,容器的「邊界」是多種機制疊加的結果:

  • Namespaces(04):隔離視圖 — 看不到外面
  • cgroups(07):限制配額 — 用不過量
  • 虛擬記憶體與 OOM(08):記憶體上限的落地與失控時的處置
  • rootfs 與 pivot_root(09):換到自己的檔案系統根、斷開主機
  • Capabilities + user namespace(本節):削弱並脫鉤容器內的特權

沒有任何單一機制能獨力構成容器,它們各管一塊,缺一不可。這正是本章一開始所說的:容器不是黑盒子,是 Linux 核心多項機制加上執行環境工具的組合。

延伸閱讀#