AI 代理無法 sudo,而這為您劃清了界線
讓 AI 代理程式清理生產環境的伺服器聽起來很冒險,直到你注意到它無法執行 `sudo`。權限邊界本身就將工作劃分為「代理程式安全」和「僅限人類」兩部分。
我交給一個 AI 編碼代理一項實際工作:在伺服器被清除並運送前,透過 SSH 深度清理一台生產伺服器。對此,直覺上會感到緊張。一個代理在運作中的機器上執行破壞性指令,正是人們所警告的那種情境。實際發生的情況是,該代理完全無法執行工作中危險的那一半,因為它無法使用 sudo。權限模型將這項工作分成了代理安全軌道和僅限人類軌道,而沒有人刻意設計這種劃分。
代理程式嘗試了什麼,以及它在哪裡停止
代理程式首先處理了可回收的雜亂資料。它執行了容器工具來清除已停止的容器、無用的映像檔和建置快取,然後移除了家目錄中一個過時的簽出版本。這些操作都不需要提升權限:容器的清除是透過登入使用者已可存取的 daemon socket 進行,而在家目錄下刪除檔案則是基本的所有權操作。這些空間很快就累積起來了:
| 回收項目 | 數量 |
|---|---|
| 已停止的容器 | 3.4 GB |
| 無用的映像檔 | 34.5 GB |
| 建置快取 | 15.1 GB |
| 家目錄雜物 | ~1 GB |
大約清除了 39 GB,全都來自以登入使用者身份執行的指令。接著,清單移至服務層:停用舊的 systemd 單元、清除無頭伺服器不需要的套件,以及將預設開機目標從桌面環境改掉。這些操作每一個都需要 sudo,而代理程式就在這裡停了下來。
這次停止並不是一個警告或確認提示。而是一個回傳錯誤且未執行任何其他動作的指令。提示是代理程式可以回應的東西,而回應它感覺像是一種進展。一個沒有後備方案的錯誤就像一堵牆。
為何代理程式無法跨越那條線
有兩件事在代理程式和 root 之間築起了一道堅固的牆,而這兩者都值得保留。
首先,代理程式執行指令所透過的 shell 沒有控制終端機。sudo 會從終端機裝置讀取密碼,而在沒有附加終端機的情況下,它不會靜默地改用標準輸入。它會當場失敗。根據 sudo 的版本和設定,您會看到以下其中一種情況:
sudo: no tty present and no askpass program specified
sudo: a terminal is required to read the password
底下的工具以其各自的方言失敗,而每則訊息都點明了真正的障礙:
Failed to disable unit: Interactive authentication required.
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
E: Unable to acquire the dpkg frontend lock, are you root?
systemctl 會透過 polkit 路由請求,而 polkit 想要詢問人類,卻找不到可以詢問的對象。apt 永遠無法通過 dpkg 鎖定,因為鎖定檔是由 root 所擁有。同樣一道牆,三種不同的描述方式。
第二,更重要的是,將密碼提供給代理程式在設計上是不可行的。一個能夠在提示符號中輸入您 root 密碼的代理程式,就是一個可能被說服出於錯誤理由而這麼做的代理程式。考量的點不是一個惡意的模型。代理程式會讀取該機器上的東西:日誌、設定檔、README 檔案、其他人撰寫的程式的輸出。所有這些都是輸入,而輸入可以攜帶指令。代理程式持有的憑證,可以被代理程式讀取的任何東西所存取。「代理程式絕不直接處理憑證」這條規則,使得第一個限制成為一個特性,而非一個惱人的問題。
第三個原因事後才會顯現:歸屬。sudo 會在驗證日誌中記錄每一條提升權限的指令,以及叫用它的使用者。當一個人執行需要提升權限的部分時,記錄會記下該人員的姓名和確切的指令。若為代理程式設定無密碼規則,同樣的條目會記下一個服務帳號,而不會提及是誰決定要執行它。
權限模型劃出的分野
因此,清理工作自然而然地分成了兩路,且沒有任何政策文件對其進行描述:
人員在一個獨立的終端機中,透過 ssh host -t sudo ... 執行了第二個通道,其中 -t 會強制使用一個真正的 TTY,這樣密碼提示才能正常運作。代理程式已經完成了大量的清理工作,並留下了一份關於剩餘內容的清晰報告,所以輪到人員操作時,只需處理一份簡短、經過審查的高權限指令清單,而不是一個開放式的會話。
這個順序是值得仿效的部分。緩慢的工作是調查:找出哪些東西可以安全刪除、衡量它能釋放多少空間、檢查哪些單元仍在運作中。代理程式擅長做這些事,而且這些工作都不需要 root 權限。留給人員處理的,只剩下十幾行指令需要閱讀和批准。閱讀十幾行高權限指令只需一分鐘。監督一個開放式的 root 會話則需要花上一個下午的時間。
為何這是個好的預設,而非待修復的限制
把無密碼的 sudo 規則交給代理程式,讓它完成所有工作,這很誘人。請克制這種想法。需要 root 權限的指令,正是您最希望由真人先讀過的指令:停用服務、清除套件、變更機器開機時的行為。sudo 的邊界正好將這些指令交給真人處理,而將可逆、影響範圍小的工作留給代理程式。
這個邊界的價值體現在代理程式出錯那天,而不是它做對那天。代理程式犯的是看似合理的錯誤,而非顯而易見的瘋狂錯誤:一個解析為空字串的路徑變數、一個清理目標是此處仍在運作但指令來源機器上已過時的資料目錄、一個根目錄設得太高一層的遞迴刪除。沒有提升權限,這些嘗試會在本機大聲地失敗,在遇到第一個 root 擁有的項目時因 Permission denied 而停止。rm -rf / 甚至不會開始執行,因為 rm 會拒絕根目錄,除非您傳遞一個旗標表示您是認真的。處於風險之中的是登入使用者所擁有的東西:會造成實際損害,但可復原,且在任何人需要相信任何人的判斷之前,範圍是受限的。
只要您不撤銷它,就能免費獲得這個好處。將憑證遠離代理程式之手,不要添加一概而論的無密碼規則,這種分工就會自然形成:
- 代理程式在登入使用者權限下處理容器清理、快取回收和檔案移除,並回報它無法觸及的內容。
- 任何需要 root 的事情,根據設計會進入真人處理的流程,透過一個明確的
ssh -t sudo會話,在代理程式已界定範圍的簡短清單上執行。 - 失敗的指令成為交接項目:一個指名單位或套件的錯誤,是比憑記憶寫下的摘要更精確的工作項目。
- 如果您必須授予臨時的權限提升,請將其設為一個狹窄、有時間限制的規則,並在機器離開您手中之前將其移除,而不是一個永久的授權。
邊界比看起來更薄弱
清理通道無需 sudo 即可運作,因為登入使用者可以存取容器守護行程通訊端,通常是透過 docker 群組的成員資格。該權限比表面上看起來更接近 root 權限。任何可以啟動容器的人都可以將主機檔案系統掛載到容器中,並以 root 身分寫入:
docker run --rm -v /:/host alpine rm -rf /host/etc/systemd/system/some.service
所以,agent 遇到的阻礙是工作流程邊界,而非用來抵禦主動繞行嘗試的安全邊界。它之所以能起作用,是因為 agent 會為工作執行普通指令,取得錯誤後回報。但對於知道 socket 存在的對手而言,這道防線便無法成立。
這點具有雙向的影響。如果您將 agent 視為不可信,那麼在處理 sudo 策略之前,容器存取權是需要修補的漏洞:使用無根執行環境,或是一個只暴露您所需呼叫的 socket 代理。而如果您想擴大權限,讓 agent 能獨自完成工作,請注意,容器存取權並不像表面上看起來那麼一小步。將使用者加入 docker 群組,或是在共享主機命名空間的容器內以 root 身分執行 agent,會導致相同的結果:兩條通道合而為一,而免費的審查步驟也隨之消失。
常見問題
代理程式可以直接使用 sudo -S 並從標準輸入讀取密碼嗎?
從機制上來說可以,但這正是要避免的路徑。將密碼透過管道傳送給 sudo -S 會將憑證放在命令列、shell 歷史記錄檔、共享機器上的處理程序清單,以及代理程式自己的腳本記錄中。你會用一個花費五分鐘人力時間的邊界,換來一個存在於四個你無法控制之處的 root 秘密。
一個狹窄的 NOPASSWD 規則是可以接受的嗎?
有時候可以,如果它是狹窄且暫時的。陷阱在於,其參數的影響範圍比指令名稱所暗示的要廣。一個針對 systemctl 的 NOPASSWD 項目涵蓋了機器上的每一個單元。一個針對包裝腳本的項目會將 root 權限授予任何可以編輯該腳本的人,而這個人可能就是代理程式。將規則固定在特定的指令和參數上,並在新增它的同一個工作階段中將其移除。
當代理程式在容器內執行時,這些原則還適用嗎? 機制改變了,但問題依然存在。在許多映像檔中,shell 本身就已經是 root,所以沒有 sudo 這道牆可以依賴。真正重要的是你掛載了什麼,以及你授予了哪些能力。一個以讀寫模式掛載主機檔案系統的容器完全沒有邊界;而一個使用唯讀掛載、捨棄了能力、且沒有守護進程通訊端的容器則有很強的邊界。
如果代理程式確實需要 root 所擁有的資料該怎麼辦? 授予讀取權限,而非寫入權限。新增一個群組並賦予其對特定目錄的讀取權限,或者由你親自收集資料轉存並交給它。大多數看起來需要 root 權限的任務,其實是需要查看某些東西,而不是改變某些東西,而讀取權限是你可以低成本分享並在之後收回的那一半。
讓代理程式接近生產伺服器時,最令人安心的事情莫過於發現它在結構上有多少事情是做不到的。