你有沒有遇過,明明用 root 登入防火牆,卻連一個設定都存不了?
我在一台 OPNsense 上替 jt-ipam 建立專用帳號與 API key,準備測試憑證推送。結果建立 API key 失敗、新增帳號失敗、改使用者權限也失敗,每一個寫入動作都跳出同一個錯誤。原因是 root 自己的 Privileges 欄位被按過「全選」,連「禁止寫入設定」這個權限也一起勾上了。
這篇記錄判斷方式,以及網頁上已經沒有帳號能存設定時,怎麼從 shell 解鎖。
問題徵狀
在「系統 → 權限 → 使用者」替帳號建立 API key 時,畫面跳出錯誤:
General access
User root denied for write access (user-config-readonly set)

雖然出錯,瀏覽器仍然下載了一個金鑰檔,內容是:
key=undefined
secret=undefined
其他寫入動作也一樣:新增帳號、編輯 root 把 Privileges 清空後按「儲存」,都是同一個錯誤。當下的印象是「每個管理員帳號都壞了」。

原因:全選勾到禁止寫入權限
OPNsense 的權限清單裡,除了「允許使用某個頁面」的一般權限,還有一個作用相反的權限:
| 畫面上的名稱 | 內部名稱 | 作用 |
|---|---|---|
| System: Deny config write | user-config-readonly | 擁有這個權限的帳號,在會檢查它的存檔流程中不能儲存設定 |
它跟一般的頁面權限放在同一份下拉清單裡,按「全選」就會一起被勾進去。root 的 Privileges 原本應該是空的,事後看到的卻是「All pages, CrowdSec, Dashboard: ET Pro Telemetry…」一長串,看起來是被按過全選。

查 OPNsense 原始碼,判斷規則是這樣的:
- 權限是「使用者本身或所屬群組有,就算有」
帳號自己身上有user-config-readonly,就算 admins 群組沒有,也照樣被判定為唯讀。群組給的 All pages 蓋不掉它。 - 新式頁面連 root 也擋
使用者、群組、API key 這些頁面走 MVC API,儲存前會檢查這個權限,對 root 沒有例外。只有部分舊式頁面的存檔函式會略過 root。 - 金鑰檔是前端照樣下載的
建立 API key 時,後端在儲存設定這一步被拒絕,金鑰沒有存進設定,也沒有回傳 key 和 secret。前端沒有檢查結果就把回應組成檔案下載,所以內容是undefined。這個檔案沒有用,可以直接刪掉。
判斷被鎖住的帳號
錯誤訊息的格式固定是 User <帳號> denied for write access,這裡的帳號是目前登入的人,不是正在編輯的帳號。我每次失敗看到的都是 User root,代表這些操作都是以 root 身分被擋。「每個管理員都壞了」其實是一直用 root 登入、去編輯其他帳號,所以每次都被擋;其他帳號有沒有受影響,要另外檢查它們自己和群組的權限。
接著排除群組。如果這個權限掛在 admins 群組上,群組裡所有帳號都會被鎖。檢查 admins 群組,Privileges 只有「All pages」,沒有問題。

照理說,換另一個沒被鎖的管理員登入,就能把 root 改回來。我遇到的情況是另一個管理員帳號的密碼不在手邊,網頁上已經沒有能存設定的帳號,只能從 shell 處理。
從 shell 移除禁止寫入權限
主控台選單有兩個看起來相關的選項,但都不是最好的做法:
3) Reset the root password解決不了
它會重設密碼、解除停用、還原登入 shell,驗證來源不是本機資料庫時還會詢問要不要改回 Local Database,但不會動 Privileges。13) Restore a backup能解決,但影響範圍大
可以還原到按全選之前的設定版本,但那之後做的其他修改也會一起消失,不建議當成第一個選擇。
比較乾淨的做法是只把這一個權限從設定檔拿掉。從主控台或 SSH 用 root 登入,選 8) Shell。shell 不受網頁權限限制。直接改設定檔時,請確認沒有其他人或自動化程式同時在存設定,避免互相覆蓋。
注意:選單 8 進入的是 csh,不支援 $( ),下面的指令用反引號。
步驟一:備份設定檔
cp -n /conf/config.xml /root/config.xml.bak-`date +%Y%m%d%H%M%S`
ls -l /root/config.xml.bak-*
步驟二:找出權限掛在哪個帳號或群組
先看有幾處、在第幾行:
grep -n "user-config-readonly" /conf/config.xml
OPNsense 25.1 起使用者頁面改成 MVC,一個帳號的所有權限寫在同一個 <priv>,用逗號隔開。我的結果只有一行:
257: <priv>page-all,page-user-crowdsec,…,user-config-readonly,page-system-firmware-manualupdate,…</priv>
這一行看不出屬於誰。OPNsense 一定有 PHP,可以用它列出帶著這個權限的帳號與群組名稱:
php -r '$x=simplexml_load_file("/conf/config.xml"); foreach(["user","group"] as $t) foreach($x->system->$t as $n) foreach($n->priv as $p) if(is_int(strpos((string)$p,"user-config-readonly"))) echo "$t {$n->name}\n";'
我的輸出只有 user root,確認只有 root 被鎖。這段指令沒有用到 !,因為 csh 在單引號裡也會把它當成歷史指令展開。
步驟三:只移除目標帳號的權限
如果還有其他帳號或群組是特地設成唯讀的,不能整份檔案一起替換,否則它們的限制也會被拿掉。用步驟二查到的行號,把替換限定在那一行:
sed -i '' -e '257s/,user-config-readonly//' -e '257s/user-config-readonly,//' -e '257s#<priv>user-config-readonly</priv>##' /conf/config.xml
257 換成你自己查到的行號。三段規則分別處理它在清單中間或結尾、在開頭、單獨一個的情況,同一行的其他權限都保留。FreeBSD 的 sed 用 -i 時要接備份副檔名,這裡已經在步驟一手動備份,所以給空字串 '',不讓 sed 另外建立備份。
步驟四:確認並重新登入
先確認三件事:設定檔還是合法的 XML、跟備份比只差這一個權限、步驟二的指令不再列出 root。
php -r 'var_dump((bool)simplexml_load_file("/conf/config.xml"));'
diff /root/config.xml.bak-<時間> /conf/config.xml
第一行要輸出 bool(true),diff 應該只有第 257 行不同、少了 user-config-readonly。回到網頁登出再用 root 登入,就能建立帳號與 API key,下載的檔案也有真正的 key= 與 secret=。萬一改壞,用步驟一的備份蓋回去即可:
cp /root/config.xml.bak-<時間> /conf/config.xml
權限設定建議
- 管理員帳號身上不要另外掛權限
完整權限一律由 admins 群組提供。清除前先確認帳號確實在 admins 群組、群組有 All pages、來源網路沒有限制到你的連線位置;再逐一把個人 Privileges 按「全部清除」後儲存,每清一個就重新登入確認。 - 不要對 Privileges 按「全選」
需要完整權限時,把帳號加進 admins 群組即可。 - 保留一個能救援的管理員
確認至少有兩個管理員帳號的密碼是可用的,一個被鎖時還能用另一個修。 - 唯讀帳號照最小權限設定
只做讀取或備份的帳號移出 admins 群組,只給需要的頁面權限,再加上「System: Deny config write」。這樣才不會因為唯讀設定影響到管理員。這個權限擋的是儲存設定,不是所有操作,例如啟動、停止服務的 API 就不檢查它,給頁面權限時要一併考慮。 - 要寫入設定的整合帳號不能勾禁止寫入
API key 屬於帳號,用 API key 呼叫時一樣受這個權限限制。jt-ipam 推送憑證需要寫入設定,所以它的帳號不能勾。
結論
這次的問題只是一個「全選」,但它把 root 鎖成唯讀,而且 root 沒辦法在使用者頁面替自己解鎖。其他管理員的密碼又不在手邊,只能改從 shell 修復。
判斷的關鍵是看清楚錯誤訊息寫的是哪個帳號,它指的永遠是目前登入的人。之後我的管理員帳號一律不掛個人權限,權限統一由群組給。
參考資料
- OPNsense 原始碼:權限定義 ACL.xml
https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml - OPNsense 原始碼:API 寫入前的唯讀檢查 ApiControllerBase.php
https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/controllers/OPNsense/Base/ApiControllerBase.php - OPNsense 原始碼:使用者與群組權限的判斷 ACL.php
https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/models/OPNsense/Core/ACL.php - OPNsense 原始碼:建立 API key 的 UserController.php
https://github.com/opnsense/core/blob/master/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/UserController.php - OPNsense 25.1 版本說明(使用者與群組改為 MVC)
https://docs.opnsense.org/releases/CE_25.1.html - OPNsense 文件:User Management
https://docs.opnsense.org/manual/how-tos/user-local.html



