從 2026 年 6 月底 Debian 13 Trixie 正式釋出開始,Proxmox 論壇跟 GitHub issue tracker 上多了一波很集中的求救文——把 unprivileged LXC 容器裡的 Debian 12 dist-upgrade 到 13 之後,systemctl --failed 按下去,journald、agetty、systemd-sysctl、systemd-udev-load-credentials 這一整組東西通通掛在 status=243/CREDENTIALS 上。極端一點的案例是全新開的 Debian 13 容器,還沒動過任何設定,一開機就是 19 個 failed unit。這不是升級 script 沒清乾淨的殘骸,是 systemd 257 把一個沒有考慮到 unprivileged 容器場景的功能全域打開。
243/CREDENTIALS 這串代碼指的是什麼
systemd 從 v250 開始逐步引入 unit credentials 這套機制。設計目的是讓 service 在啟動時能透過 LoadCredential=、ImportCredential=、SetCredentialEncrypted= 這些指令拿到一份被隔離的機敏資料——密鑰、憑證、tokens——而不用整個丟到環境變數或者掛在檔案系統的公開路徑。實作上 systemd 會為每個 service 開一個新的 mount namespace,把 tmpfs 掛在 /run/credentials/<unit> 底下,再把資料寫進去。整個過程對 host 完全隔離,service 結束後 tmpfs 一 unmount 就消失。
問題出在「掛 tmpfs」跟「開新的 mount namespace」這兩個動作,都需要容器內部具備對應的 capability。特權容器 (privileged) 有 CAP_SYS_ADMIN,做這件事沒問題;unprivileged 容器把 user namespace 疊在最外面,這些操作在容器 view 下會被 kernel 直接拒絕,systemd 就把整個 unit 標成 243/CREDENTIALS 放棄啟動。
到 systemd 256 為止,這個功能還是要在 unit file 裡明確寫 LoadCredential= 才會被觸發。到 257,Debian 上游把 ImportCredential= 之類的呼叫塞進了一堆基礎 service 的 upstream unit——systemd-sysctl.service、systemd-udev-load-credentials.service、systemd-journald.service——這條路就從「進階功能」變成「開機必經路徑」。Debian 12 的 systemd 252 沒這條,所以感覺不到;一升到 13 就整條爆掉。
nesting=1 能繞過,但這是拿主機隔離做代價
Proxmox 論壇最先出現的解法是在容器的 config 打開 nesting:
1 | pct set <CTID> --features nesting=1 |
nesting 這個 flag 的名字容易誤解,實際做的事情不是「允許在容器裡跑 Docker」那麼單純——它同時放寬 procfs、sysfs 的遮罩,允許容器建立自己的 user/mount namespace 疊層。開了之後 systemd 就能自己開新的 mount namespace 掛 tmpfs,credentials 那條路就跑得通。
但這個代價很少人算清楚。開 nesting 等於把主機的 /proc、/sys 內容更完整地暴露給容器裡的 process 讀取。共用一台 Proxmox host 給多個信任等級不同的服務跑的情境,這樣做等於幫每個容器都開一扇小窗。Proxmox 官方文件寫得很含蓄——「若容器內需要跑巢狀虛擬化才需要開」——實務上把它當萬用開關來用,是很多 self-hosted 環境沒被追認的技術債。
lxc.generator 才是符合上游意圖的解法
真正的正解是把 lxc.generator 塞進 /etc/systemd/system-generators/lxc。這個 generator 是 LXC distrobuilder 專案裡一直存在的東西,systemd 在啟動時會呼叫它一次,讓它動態生成一批 drop-in 檔案覆蓋在 /run/systemd/system/service.d/ 底下。內容大致像這樣:
1 | [Service] |
空的 LoadCredential= 跟 ImportCredential= 會清掉上游 unit 裡的值——這是 systemd config 的標準覆蓋語法,不是註解掉。剩下那幾行則是把 systemd 針對非容器場景的 sandboxing 一併拆掉,因為那些同樣會在 unprivileged 環境撞牆。
安裝步驟在 dist-upgrade 之前就要做完:
1 | sudo mkdir -p /etc/systemd/system-generators |
daemon-reload 完可以直接 cat /run/systemd/system/service.d/zzz-lxc-service.conf 檢查生成物有沒有出現。這一步做完再改 sources.list 從 bookworm 換到 trixie、跑 apt dist-upgrade,就能乾淨落地。
要事先知道自己的環境會不會踩到,方法是在 Debian 12 的容器裡直接查一下 upstream unit 有沒有暗埋 credential 呼叫:
1 | systemctl cat systemd-sysctl.service | grep -iE 'credential' |
Debian 12 那時候多半是空的,Debian 13 這幾個 unit 會出現 ImportCredential=sysctl.extra、LoadCredential=journald.forward_to_socket 之類的行——只要看到就代表升級後會撞。這個檢查也可以在 Ansible check 階段拉出來做為升級 gate。
代價是這份 drop-in 把 NoNewPrivileges=no 也塞進去,等於把 upstream service 針對「限制 setuid 攻擊面」的預設保護關掉。這是刻意選擇——unprivileged 容器已經在最外層被 user namespace 包起來一次,多這層 per-service sandbox 帶來的邊界收益不高,但撞到不能開機的機率極高。權衡下去掉這層是合理的。
已經升壞的容器怎麼救回來
沒有事先裝 generator 就直接 dist-upgrade,開機起來八成 console 是黑的,但 pct enter 通常還進得去。console 死掉是因為 agetty 在 credentials 那條路撞死,但底層的 shell 環境還在。進去之後照上面的安裝步驟補一份 generator,systemctl daemon-reload 然後從 host 端 pct reboot 出來就能救回來。
需要注意的是,lxc.generator 不能只裝來救火然後移掉。這份設定要永久留著,一移掉下次重開機就再次爆炸。這跟 Debian 12 時代的處理方式不一樣,那時候有人習慣「升完就拆」,在 systemd 257 之後這個做法不能沿用。
新開的容器建議在 template 建立階段就把 generator 放進去,別等 provisioning script 跑到一半才發現 systemd 已經在噴 credentials 錯誤。用 Terraform 或 Ansible 管 Proxmox 的環境,可以在 initial user-data 或 first-boot playbook 加一段安裝步驟,跟 SSH key 部署擺在同一個階段。
另外一個常見誤區是把 unit override 寫在 /etc/systemd/system/systemd-sysctl.service.d/ 這種傳統路徑,覺得比較「乾淨」。這條路走不通,因為每次上游 unit 加入新的 credential 呼叫就要手動追一次,維護成本會迅速累積。generator 每次開機動態掃描所有 unit 產生 drop-in,才是能一勞永逸的處理方式。
這條坑本來應該由 Debian 上游而不是使用者處理
站在使用者角度,這是明顯的 regression——同一個 upstream package 升一版之後在支援已久的環境跑不動。上游會怎麼答通常有幾種說法:unprivileged 容器不是官方 supported 環境、credential 是資安改善不能關、應該由容器 runtime 提供對應 shim 讓 systemd 感知到自己在容器裡。這些說法都各有道理,但無法反過來要求下游把已經跑很久的 unprivileged LXC 全部改成 privileged 或直接搬到 VM。
比較乾淨的長期方向是 LXD/Incus 那邊在 systemd-detect-virt 回傳 lxc 時,讓 systemd 自動禁用需要新 mount namespace 的 unit setting——這是 systemd 上游已經在推的方向,但目前覆蓋率不夠、也還沒下放到 stable。在覆蓋完之前,lxc.generator 就是每個維護 Proxmox host 的人都要放進 base template 的一份必備檔案。
結語
Debian 13 這一波升級把 LXC 生態一個結構性問題直接推到每個管理者的臉上。真正的問題不是 systemd 257 本身,而是 unprivileged 容器跟現代 systemd 對「隔離」的想像從來沒有真正對齊過。在對齊完成前,唯一能做的就是把 lxc.generator 寫進 provisioning 流程,dist-upgrade 前先跑一次,別讓新開的 template 開機就是 19 個 failed unit。
NCSE Network 的臺灣 VPS 服務底層以 Proxmox 提供 KVM 與 LXC 兩種選項,機房位於是方電訊,硬體採用 Intel Gold CPU 與 NVMe SSD。正在規劃把測試環境或 production LXC 從 Debian 12 升到 13、或是需要一台已經處理好這類升級細節的容器主機,可以到 https://ncse.tw 查看服務規格。