VPS Debian LXC Proxmox systemd

Debian 13 升級後 unprivileged LXC 一堆服務 243/CREDENTIALS 起不來:systemd 257 unit credentials 打到容器 namespace 的解法

Debian 13 Trixie 帶進 systemd 257,unit credentials 預設打開,直接把 Proxmox 上的 unprivileged LXC 大量服務打死在 243/CREDENTIALS。這篇拆解成因、比較 nesting 跟 lxc.generator 兩種解法,以及升級前後該怎麼處理才不會把容器變成磚頭。

從 2026 年 6 月底 Debian 13 Trixie 正式釋出開始,Proxmox 論壇跟 GitHub issue tracker 上多了一波很集中的求救文——把 unprivileged LXC 容器裡的 Debian 12 dist-upgrade 到 13 之後,systemctl --failed 按下去,journaldagettysystemd-sysctlsystemd-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.servicesystemd-udev-load-credentials.servicesystemd-journald.service——這條路就從「進階功能」變成「開機必經路徑」。Debian 12 的 systemd 252 沒這條,所以感覺不到;一升到 13 就整條爆掉。

nesting=1 能繞過,但這是拿主機隔離做代價

Proxmox 論壇最先出現的解法是在容器的 config 打開 nesting

1
2
pct set <CTID> --features nesting=1
pct reboot <CTID>

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
2
3
4
5
6
7
8
9
[Service]
LoadCredential=
ImportCredential=
ProcSubset=all
ProtectProc=default
ProtectControlGroups=no
ProtectKernelTunables=no
NoNewPrivileges=no
PrivateNetwork=no

空的 LoadCredential=ImportCredential= 會清掉上游 unit 裡的值——這是 systemd config 的標準覆蓋語法,不是註解掉。剩下那幾行則是把 systemd 針對非容器場景的 sandboxing 一併拆掉,因為那些同樣會在 unprivileged 環境撞牆。

安裝步驟在 dist-upgrade 之前就要做完:

1
2
3
4
5
6
sudo mkdir -p /etc/systemd/system-generators
sudo curl -fsSL \
https://sources.debian.org/data/main/d/distrobuilder/3.2-2/distrobuilder/lxc.generator \
-o /etc/systemd/system-generators/lxc
sudo chmod 0755 /etc/systemd/system-generators/lxc
sudo systemctl daemon-reload

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
2
systemctl cat systemd-sysctl.service | grep -iE 'credential'
systemctl cat systemd-journald.service | grep -iE 'credential'

Debian 12 那時候多半是空的,Debian 13 這幾個 unit 會出現 ImportCredential=sysctl.extraLoadCredential=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 查看服務規格。

需要穩定的雲端主機?

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

查看 VPS 方案 →