三個前沿背後的同一個問題
你已經爬了好長一段路。你知道一次系統呼叫為什麼有代價——因為它要跨進核心;你知道為什麼一個虛擬位址不是實體位址;你知道一個 Hypervisor 怎麼在作業系統上跑作業系統;而從那篇驗證系統的指南裡,你也知道為什麼像 seL4 這樣的微核心會把它那個受信任的核心保持得極小。這最後一篇與其說是一個新機制,不如說是一個方向。系統領域裡三個最有活力的前沿——unikernel、以權能為基礎的安全,以及機密運算——全都在從不同角度回答同一個問題:跑你的程式碼的那個東西,能做到多小、又能多值得信任?每一個都把我們以前視為理所當然的某樣東西縮小了:作業系統、身分的概念,或者你被迫去信任的那組元件。
有一條線貫穿這三者,值得先點明,因為它正是這一整級的寓意。一個系統越小、邊界越分明,就越容易推理——容易測試、容易驗證、容易信任。一個通用作業系統能力強得驚人,但它的這份「通用」本身就是個負擔:數百萬行的程式碼,其中任何一行都可能藏著那個讓攻擊者闖進來的臭蟲。這篇裡的每一個想法,都是靠移除通用性來買到安全。那正是你在即時系統、以及 seL4 的證明裡遇過的那個「可預測勝過強大」的交易:你放棄一些方便、一些彈性,去換一個你真的能推理的東西。把這個取捨記在心裡;它就是讓這三個想法都豁然開朗的那副眼鏡。
Unikernel:把整個堆疊塌縮成一個映像檔
想像一個普通程式是怎麼摸到硬體的。你的程式碼在使用者空間裡呼叫 malloc() 和 open();每一次系統呼叫都陷入一個龐大的通用核心,那個核心管理著許多行程、許多使用者、一整套檔案系統、一套 TCP/IP 堆疊、幾十個驅動程式。但一個雲端服務通常就只是一個程式。它不需要多個使用者、不需要能跑別的程式的本事、也不需要那些驅動程式裡的大多數。一個 unikernel 把這個觀察推到底:把你的應用程式連同它實際會用到的那幾塊作業系統元件——一片網路堆疊、一個記憶體配置器、一個排程器——一起連結成一個自足的機器映像檔,它直接在一個 Hypervisor 上開機,底下根本沒有任何通用作業系統。
後果很戲劇性,而且全都來自「移除使用者/核心的邊界」。整個映像檔只有一個位址空間、一個特權層級,所以一次系統呼叫不再是一次陷阱與模式切換——它變成映像檔內部一個普通的函式呼叫,這確實比較便宜。一個 unikernel 映像檔可以是幾 MiB、而不是好幾百 MiB,能在幾十毫秒內開機,並呈現出極小的攻擊面:沒有 shell 可以掉進去、沒有第二個行程可以橫向移動、沒有沒用到的驅動程式可以利用。如果你的程式碼根本沒連結進檔案系統函式庫,那麼映像檔裡就真的連一個 open() 都沒有可以攻擊。你把沒用到的部分編譯掉,而你沒出貨的那些部分,也就無從被反過來對付你。
權能:用不可偽造的權杖取代身分檢查
現在來縮小另一樣東西:身分這個概念。想想一個普通作業系統是怎麼決定你能不能讀一個檔案的。你對一條路徑呼叫 open(),核心就問:「這是哪個行程,它的憑證准不准它存取這個物件?」這就是以身分為基礎的存取控制——系統握著一張「誰可以做什麼」的清單,並在每一次操作時拿你去對照它。它能動,但它有一個著名的失敗模式:混淆代理(confused deputy)。一個有特權的程式(比方說一個能寫進受保護日誌的編譯器)可以被一個較低特權的呼叫者誆騙,去代呼叫者使用它自己的權限,因為那個檢查是針對代理的身分、而不是針對「這個特定的請求到底該不該被允許」。
一個權能(capability)把這件事翻轉過來。權能不是一個讓系統去清單裡查找的身分,而是一個不可偽造的權杖,它同時指名一個物件、並授予對它的某個特定權利——「這個權杖讓持有者可以讀檔案 X」。要做一件事,你不是說「我是 Alice,請檢查我的權限」;你是把權杖拿出來,而握有它就是那份權限。你其實已經認得一個樸素的版本:一個檔案描述符幾乎就是一個權能。一個行程一旦握有一個開啟的 fd,它就能對它 read() 而不必再去檢查路徑的權限,它甚至能透過一個 Unix 網域通訊端把那個 fd 交給另一個行程——傳遞的是那份權利本身,而不是身分。以權能為基礎的安全把這件事一般化:每一個權利都是一個權杖,權限是「你握有什麼」、而不是「你是誰」,而你授予存取的方式是把一個權杖交出去,不是去編輯一張全域清單。
有兩個性質讓這在安全上很有力。第一,它讓最小權限原則變得自然:一個元件只握有它需要的那些物件的權杖,而且連別的東西的名字都叫不出來,這就封住了混淆代理那個洞——要濫用權限,你得先有人把權杖交給你,而一個謹慎的程式就根本不把它交出去。第二,它能乾淨地組合。上一篇的 seL4 正是為了這個理由才是一個以權能為基礎的微核心:當每一個核心資源(一條執行緒、一頁記憶體、一個中斷)都只能用一個權能來指名時,「這個元件到底可能做什麼?」這個問題就有了一個精確、可列舉的答案——它握有的那組權杖——這正是讓 seL4 的權限封閉變得可證明的東西。你在普通的 Linux 上也會碰到這個想法的一小劑:拋棄 POSIX 權能會把一束被收窄的權力交給一個行程,而 seccomp 則限制它根本被允許發出的那組系統呼叫。
機密運算:連主機都擋在門外
這裡是最激進的一次縮小——縮小你必須去信任的對象。到目前為止每一種保護都假設核心和硬體站在你這邊:核心施行記憶體保護、Hypervisor 把虛擬機彼此隔開。但把你的工作負載跑在別人的雲上,一個尖銳的問題就冒出來了:你信任那台機器的操作者嗎?主機作業系統、Hypervisor,以及任何在那台機器上有 root 的人,原則上都可以讀到客體的記憶體。對一家銀行或一間醫院來說,那不是疑神疑鬼;那就是威脅本身。機密運算(confidential computing)用一個硬體特性來對付這件事,好讓你的資料和程式碼在記憶體裡保持加密,並只在 CPU 自己內部被解密,在那裡連主機核心和 Hypervisor 都看不進去。那個機制是一個受信任執行環境(trusted execution environment,TEE),有時被稱為飛地(enclave)或機密虛擬機:CPU 維護一塊受保護的區域,它被一把活在晶片裡面、永遠不暴露給該區域以外任何軟體的金鑰加密——不給作業系統、不給 Hypervisor、也不給一個在記憶體匯流排上接探針的人。一個 TEE 裡的程式碼在真正的 CPU 上以全速執行,但每一條被寫出到 DRAM 的快取列都會被透明地加密,所以主機能看見的全是密文。這正是虛擬記憶體那個慣常模型的反面:在那裡,核心對一個行程的記憶體無所不能;在這裡,目標明確就是要讓核心無法讀取它名義上在管理的那塊受保護記憶體。
但光是加密留下了一個缺口:坐在家裡的你,怎麼知道你的程式碼真的跑在真實硬體上一個貨真價實的 TEE 裡,而不是跑在操作者搭來收割你機密的一個模擬器裡?答案是遠端證明(remote attestation),它正是讓這整套東西可用的那一塊。在你送進任何機密之前,你要求飛地自我證明:CPU 產生一份簽署過的報告——對「確切載入這個 TEE 的那份程式碼與設定」做的一個度量(一個密碼學雜湊),由一把晶片廠商背書的金鑰簽署。你核對那個簽章、並核對那個度量是否符合你預期的程式碼,然後、也只有在這之後,你才建立一條加密通道、把你的資料交出去。遠端證明把「相信我」變成「這裡有硬體背書的證據,證明到底是什麼東西在跑」,而這正是讓你能安全地使用一台你不信任其主人的機器的關鍵。
Confidential computing, end to end:
1. provision host loads your code+data into a TEE region
(host can see only ciphertext in DRAM)
2. attest CPU signs a report = hash(code, config)
signed by a vendor-rooted key
3. verify you check the signature + that
hash == the code you intended
4. seal you open an encrypted channel into the TEE
and send the secret (key, data) ONLY now
5. run decrypt happens inside the CPU; host OS,
hypervisor, root user all see ciphertext
trusted: CPU + your enclave code
NOT trusted: host OS, hypervisor, operator, DRAM bus前沿正往哪裡走
這些想法在實務上正在匯流,這才是把它們放在一起學的真正理由。現代的雲端平台在 microVM 裡跑不受信任的程式碼——那是開機只要幾毫秒、裝置模型極簡的精簡虛擬機,非常符合 unikernel 那種「只出貨你需要的」精神。一個 microVM 給你一個 Hypervisor 那種強隔離,卻只要一小部分的啟動成本和攻擊面,而你會從那篇虛擬化指南裡認出底下的 virtio 介面和第一型 Hypervisor 機制。在它之上疊一個 TEE,你就得到一個機密虛擬機;用 seccomp 和 權能收窄它的權力,你就有了從硬體往上的最小權限。從這一級最開頭那篇指南,你也遇過 WebAssembly 和 Wasm 執行環境在軟體裡做著一件相關的工作:一個帶有權能風格介面的小沙箱(WASI 交給一個模組的是明確的把手,而不是無所不在的檔案系統存取)。最小的足跡、明確的權限、硬體背書的信任——同樣的三條線,編在一起。
由這裡作為這道階梯的終點是恰當的,因為這些前沿也正是這個領域最深的那場爭論上演的地方——也就是安全對控制的辯論所指名的那一場。幾十年來,C 讓你能對記憶體做任何事,這正是讓系統程式設計既強大又危險的東西;你花了整整好幾級去學未定義行為、緩衝區溢位和釋放後使用,正是因為那份控制伴隨著一長串的 CVE 死亡名單。這篇裡的每一個想法,都往另一極推:拿走一些控制——叫得出任意物件名字、讀得到任意位址、以一個臃腫通用作業系統身分執行的本事——去換一份你能驗證、甚至能證明的安全。那正是 Rust 在語言層面提供的、seL4 在核心層面提供的同一個交易:不是魔法、不是一切臭蟲的終結,而是一次刻意的交換,用原始的強大去換有界限、可查核的信任。
所以這就是你站在階梯頂端時所在的位置。你一開始連指標是什麼都不知道;現在你能讀懂一條機密運算的證明流程,並在它底下看見你一級一級建起來的虛擬記憶體、檔案描述符、系統呼叫的邊界,以及那些信任邊界。這些前沿沒有一個是完成了的——TEE 還在針對旁路攻擊加固、unikernel 還在掙取它的維運工具、被驗證過的核心還很小。那不是一個弱點;那是一份邀請。系統的前沿,正是「它能做到多小、又能多值得信任?」這個問題還有更好的答案等著被建造出來的地方,而你現在已經有了去幫忙建造它們的根基。