VPS Linux systemd 系統管理 polkit

systemd 259 的 run0 --empower:用 ambient capability 加 polkit group 把 sudo 的 SUID root 假設拆掉

run0 從 v256 開始建路,v259 補上 --empower 這一塊之後,才真的能在多數場景取代 sudo。這篇拆解它怎麼靠 kernel capability 加 polkit empower 群組跳過「改 UID 成 root」這條路徑、SUID binary 為什麼是老包袱、以及在 VPS 上要怎麼配 polkit 規則、SSH 進來的 headless session 又要注意什麼。

Linux 世界抱著 sudo 這隻 SUID root binary 走了三十年。每一次 CVE-2021-3156「Baron Samedit」、CVE-2023-22809「sudoedit 環境變數注入」、CVE-2025-32463「chroot LPE」冒出來,社群都是同一個反應:緊急升級、掃描 sudoers、罵一句 setuid bit。問題不在 sudo 團隊寫得不夠謹慎,而是 SUID root 這個模型本質上就是一支不斷被戳的靶:一支普通使用者能執行、但真身是 UID 0 的 binary,攻擊面等於整個 libc、環境變數、terminal escape、以及所有它會 exec 出去的 helper。

systemd v256 在 2024 年放出來的 run0,第一次把「提權」這件事從 SUID binary 搬到透過 D-Bus 呼叫 PID 1 建立新 session 的路徑上。當時 run0 只做到「把使用者切成 root」,跟 sudo 的行為差不多,只是實作乾淨。真正把這條路走完的,是 2025 年 12 月釋出的 systemd 259 版加進來的 --empower 開關——命令會以原使用者的 UID 執行、不切成 root,但拿到 ambient capability 加上 polkit 認可的 empower 群組身分,實際上能做的事情跟 root 差不多。這是 sudo 三十年來第一次遇到結構上真正不一樣的替代品。

為什麼 SUID root 現在還是包袱

sudo 的 binary 上有 SUID bit,任何 UID 的使用者執行它,process 的 effective UID 立刻變成 0。這個機制是 1970 年代 UNIX 就有的設計,當年的假設是:只有幾支被審過的 binary 需要提權、跑完就結束、terminal 是可信的。

三個假設在 2020 年代都不成立。第一,SUID binary 遠不止 sudo——pingmountpasswdchshnewgrp 每支都是攻擊面,defense-in-depth 的建議是能拔就拔。第二,sudo 邏輯膨脹到現在有幾萬行 C 加各種 helper,每一次修 bug 都可能引進新洞,Baron Samedit 就是 sudoedit heap overflow 藏了十年才被抓到。第三,terminal 早就不可信——TIOCSTI ioctl 讓子程序能把字元「回打」給父 terminal,多少年來被拿來從 sudo session 逃出去執行 root 命令,Linux 6.2 才把它預設關掉。

換句話說,SUID root 的整個模型是一個「小信任區塊被大信任區塊包住」的倒過來設計。要修,得從 binary 本身沒有 UID 0 特權開始。

run0 走的是 PID 1 建立 session 這條路

run0 不是 setuid binary,也不會自己切 UID。使用者敲 run0 systemctl restart nginx,binary 做的事只有一件:透過 D-Bus 呼叫使用者 session 的 systemd,請它以指定的 UID 建立一個新的 transient service,把命令交給 PID 1 去 fork。認證由 polkit 負責,polkit 認證 prompt 走的是獨立通道,不受呼叫端 terminal 影響。

這條路跟 SSH 進本機 root 帳號的行為其實比較像,systemd 上游自己也是這樣描述的。一支獨立的 pseudo-TTY 會分配給新 session、標準輸入輸出獨立、環境變數重新初始化、$SUDO_USER $SUDO_UID $SUDO_GID 依 sudo 慣例填好,甚至 shell prompt 前綴預設加一個超人 emoji 提示這是特權 session(可以用 SYSTEMD_RUN_SHELL_PROMPT_PREFIX 蓋掉)。TIOCSTI 那類 terminal 攻擊在這個模型下不成立,因為 caller 的 terminal 跟被叫起來的 root shell 是兩個完全不同的 TTY。

到 v258 為止這都算是 sudo 的乾淨替代品,只是必須改一個習慣:目標 UID 預設是 root。run0 systemctl restart nginx 底下實際上是 UID 0 在跑 systemctl。這件事跟 sudo 一樣,能不能不切 UID 就把事情做完?v259 的 --empower 就是回答這個問題。

–empower 把提權跟改 UID 拆開

--empower 開起來之後,run0 的行為變成:不變更 target UID(預設維持呼叫者),但對這個新 session 做兩件事——設定完整的 ambient capability mask(包括 CAP_SYS_ADMIN),並把 process 加進名為 empower 的系統群組作為補充群組(supplementary group)。polkit 的預設規則認 empower 群組,這個群組成員預設能通過所有 polkit action 的授權檢查。

實務上結果是:以自己的 UID 跑 run0 --empower systemctl daemon-reload,systemctl 呼叫 D-Bus 到 systemd 要求重新載入,systemd 檢查 polkit,polkit 看到 caller 屬於 empower 群組直接放行,daemon-reload 就跑起來了——整個過程沒有任何 process 是 UID 0,也沒有 SUID binary。CAP_SYS_ADMIN 讓少數還走 capable() 檢查而不是 D-Bus 路徑的操作(例如某些 mountsysctl、bpf 系統呼叫)也能通過。

這件事的實際意義比看起來大。傳統 sudo 有一個很難堵的洞:任何以 root 身分建立的檔案、寫入的 config、留在 /tmp 的 lock file,事後 chown 回原本使用者的機率是零。加上 root context 下的 shell history、環境變數污染、umask 差異,team-server 上一堆詭異的權限問題往回追常常都是「上一個維運跑 sudo 亂改東西沒 chown 回來」。run0 --empower 下這些檔案的 owner 就是呼叫者本人,本來就不會歪。

--empower 有兩個 caveat 要拿出來講清楚,這是 systemd 上游明白寫進 NEWS 的:

第一,--empower 對「純靠 UID 判斷、不看 capability 也不查 polkit」的老程式沒用。這類程式的授權模型就是 if (geteuid() != 0) exit(1);,跟 capability 系統無關。這種情境還是得走 target UID = root,直接 run0 systemctlrun0 su -

第二,如果同 UID 底下有其他 unprivileged process,那些 process 可以透過 ptrace、/proc/<pid>/mem、signal 等機制去干擾被 empower 的 process。傳統 UID 隔離會擋掉這條攻擊面(UID 不同就無法 ptrace),--empower 因為 UID 相同就沒了。結論是這個模式適合乾淨 login session 加一支 empower shell,不適合塞進共用帳號或會被非特權 daemon 觸碰到的環境。

VPS 上實務怎麼配

Debian 13、Ubuntu 26.04、Fedora 42+、Arch 現行的 systemd 套件都內建 run0pam_systemd_run0。VPS 上的最小可用配置是把管理員加進本地系統群組,然後寫一條 polkit 規則允許該群組免密碼過關。傳統做法是拿 wheel 這條線,run0 的預設 polkit 檔案也認 wheel。

1
2
3
4
5
6
7
// /etc/polkit-1/rules.d/49-wheel-run0.rules
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});

這條規則讓 wheel 成員透過 D-Bus 對 systemd 做 unit 管理不需要密碼。要更嚴格可以把條件收到單一 action 或單一 unit;要對整個 empower 場景放行,就把 rule 條件寫成 subject.isInGroup("empower") 加上具體的 action id。

SSH 進 VPS 的情境要注意 polkit 的認證通道。SSH session 沒有 seat、也沒有 graphical polkit agent,所以互動式的密碼提示會失敗。三個做法可以走:一是像上面那樣寫 polkit rule 讓群組直接通過,等於配 NOPASSWD 的 sudoers;二是在 SSH session 內先跑一次 systemd-inhibit --mode=block cat,讓 systemd 幫這個 session 起一個文字模式的 pkttyagent;三是用 run0 --pipe 明確聲明沒有 TTY、只透過 pipe 走 stdin/stdout,這個模式下 polkit 會走 non-interactive rule,配合上面第一種做法可以在 shell script 裡串起來用。

自動化腳本、Ansible playbook、CI/CD 部署的常見情境是第三種。舉例把 become_method: sudo 換成透過 SSH 呼叫 run0 --pipe --empower,加上 wheel 群組免認證的 polkit 規則,整套流程不需要在遠端保留任何 SUID binary、也不需要 sudoers file,成員權限完全用 group + polkit rule 管。

什麼時候別換

--empower 的 caveat 決定了它不適合的情境。第一,run0 依賴一支還活著的 systemd user manager;WSLg 之類把 systemd 剝掉的環境會直接失效。第二,容器裡沒有 host 的 PID 1 systemd 可以呼叫,run0 這種模型基本上跑不動——container 內部的權限控制走 capabilities drop 加 seccomp 才對。第三,超小型 rescue 環境(single-user mode、initrd、部分 embedded distro)也不會有 polkit,這時候直接用 su 反而簡單。

還有一個實務層面的問題:run0 目前 polkit 檢查只有 caller 的 UID/group,沒有辦法像 sudoers 那樣寫「這個帳號只能對 nginx.service 做 restart,不能對 sshd.service 動任何東西」。systemd/systemd#39676 已經提出把 programcommand_line 變數塞進 polkit action 讓 rule 可以匹配,但這個功能在 v259 還沒進去。細粒度授權還在等這個補齊。

結論很直接:跑 systemd 259 或更新的 VPS,把 run0 --empower 排進遷移計畫,先從個人 admin session 開始,替換掉互動式的 sudo -i,觀察一週再往 CI/CD 跟部署腳本推進。SUID root sudo 短期內不會真的消失(Debian 生態還沒把它從預設拔掉),但新機器上「預設不裝 sudo」這件事已經是 Fedora Silverblue、部分 immutable distro 的作法,方向清楚。

NCSE Network 的臺灣 VPS 適合這樣配

NCSE Network 提供的 VPS 主機跑在臺灣是方電訊機房、Intel Gold CPU 加 NVMe SSD,出廠映像檔支援 Debian 13、Ubuntu 26.04、Fedora 41+,systemd 版本都到 259 以上。要試 run0 --empower 或評估把 sudo 全數換掉,直接開一臺乾淨環境就能跑,不需要為了驗證新工具而弄壞既有伺服器。想了解 NCSE Network 的 VPS 方案細節,可以到 ncse.tw 看一下。

需要穩定的雲端主機?

NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。

查看 VPS 方案 →