實體主機服務檔案系統沙箱:限制寫入並驗證拒絕邊界
同一台實體主機執行多個後端服務時,程式通常只需要寫入狀態目錄,卻可能因目錄擁有者權限設定過於寬鬆而改動部署檔案。為服務建立專用帳號,只能劃出第一層邊界;還需要將服務所見的檔案系統設為唯讀,僅保留必要的寫入路徑,才能縮小誤寫範圍。驗收不能停留在「服務啟動成功」,還必須證明不該寫入的位置確實無法寫入。
一、先列出寫入清單,再設定沙箱
本文以 Ubuntu Server 24.04 LTS 為例,使用 systemd 管理系統層級的服務。操作需要管理員權限與 Python 3;所有路徑都是獨立的實驗目錄,不會修改現有的業務服務單元。AmdMetal 實體主機的登入憑證可在付款後至控制台查看,Linux 使用 SSH 連線。
首先區分程式碼、持久狀態、執行階段檔案與暫存檔。程式碼應由部署帳號更新,服務帳號僅能讀取;狀態目錄用來存放重新啟動後仍須保留的資料;通訊端、處理程序識別碼等執行階段檔案,以及暫存檔,則應另外列入清單。如果日誌寫入標準輸出,可交由日誌系統接收,不必再開放整個日誌目錄的寫入權限。
核心設定是 ProtectSystem=strict:在服務的掛載命名空間內,將檔案系統階層設為唯讀,但部分 API 檔案系統等項目除外。接著使用 ReadWritePaths 為指定目錄保留寫入能力。這不會改變主機全域的掛載狀態,也不是完整的處理程序隔離方案。
唯讀限制不等於保密。原本可讀的檔案不會因此自動隱藏,網路存取也不會自動受到限制;寫入白名單同樣不能取代目錄權限。
二、建立能區分拒絕原因的權限基準
實驗準備兩個目錄:一個應允許寫入;另一個在一般權限下允許寫入,但應由沙箱攔截。這樣才能排除「原本就沒有權限」造成的偽陽性。請先確認以下帳號名稱與路徑未被現有服務使用;它們是實驗對象,不是主機預設的登入帳號。
sudo useradd --system --no-create-home \
--shell /usr/sbin/nologin sandbox-check
sudo install -d -o root -g root -m 0755 \
/srv/sandbox-check
sudo install -d -o sandbox-check -g sandbox-check -m 0750 \
/srv/sandbox-check/blocked \
/var/lib/sandbox-check
sudo runuser -u sandbox-check -- /usr/bin/python3 -c \
'import tempfile, os; f, p = tempfile.mkstemp(dir="/srv/sandbox-check/blocked"); os.close(f); os.unlink(p)'
最後一條命令應成功結束:它會在預定禁止寫入的目錄內建立隨機暫存檔,然後立即刪除。如果失敗,應先檢查目錄的逐層存取權限、ACL 與底層掛載屬性,不能繼續將後續失敗歸因於沙箱。
部署的指令稿本身應由管理員擁有,避免服務帳號改寫檢查邏輯。實驗目錄刻意讓服務帳號以擁有者身分取得寫入權限,只是為了證明限制的來源;正式應用程式的程式碼目錄仍應維持服務帳號不可寫入,不能照搬這項實驗例外。
三、設定寫入白名單與獨立探針
將下列指令稿儲存為 /srv/sandbox-check/probe.py,擁有者設為管理員,權限設為 0644。允許的路徑必須成功寫入;禁止的路徑必須回傳唯讀檔案系統錯誤,其他錯誤一律視為驗收失敗。
import errno
import os
import tempfile
def create_and_remove(directory):
fd, path = tempfile.mkstemp(prefix="probe-", dir=directory)
try:
with os.fdopen(fd, "w") as stream:
stream.write("sandbox-check\n")
stream.flush()
os.fsync(stream.fileno())
finally:
os.unlink(path)
create_and_remove("/var/lib/sandbox-check")
try:
create_and_remove("/srv/sandbox-check/blocked")
except OSError as exc:
if exc.errno != errno.EROFS:
raise
else:
raise RuntimeError("Write boundary was not enforced")
print("Allowed write passed; blocked write returned EROFS")
這裡使用隨機檔名,避免覆寫現有資料;fsync 用於檢查寫入呼叫鏈,不代表已完成效能或斷電持久性測試。探針輸出的是驗收結果,不含憑證或業務內容。
建立 /etc/systemd/system/sandbox-check.service:
[Unit]
Description=Filesystem sandbox verification
[Service]
Type=oneshot
User=sandbox-check
Group=sandbox-check
ExecStart=/usr/bin/python3 /srv/sandbox-check/probe.py
ProtectSystem=strict
ReadWritePaths=/var/lib/sandbox-check
PrivateTmp=true
NoNewPrivileges=true
PrivateTmp=true 為服務提供隔離的暫存目錄視圖,因此不能將「只允許一個持久目錄寫入」理解成「整個處理程序只有一個可寫入的位置」。NoNewPrivileges=true 限制透過執行程式取得額外權限,但不會撤銷已授予的檔案存取權限。
四、同時驗收寫入成功與拒絕結果
先檢查單元設定,再載入並執行:
sudo systemd-analyze verify /etc/systemd/system/sandbox-check.service
sudo systemctl daemon-reload
sudo systemctl start sandbox-check.service
sudo systemctl show sandbox-check.service \
-p Result -p ExecMainStatus -p ProtectSystem -p ReadWritePaths
sudo journalctl -u sandbox-check.service -b --no-pager -n 30
預期結果為 Result=success、ExecMainStatus=0,且日誌中出現探針通過的訊息。單次執行的服務結束後顯示未執行狀態,並不是異常;這裡並未設定持續執行或保留作用中狀態。
只查看設定值,不足以證明邊界有效。有效證據應同時包含:相同帳號在沙箱外寫入成功、在沙箱內的允許目錄寫入成功,以及向禁止目錄寫入時回傳 EROFS。如果得到 EACCES,只能證明一般權限或其他安全政策拒絕存取,不能視為通過本項驗收。
正式服務還須測試實際業務路徑
探針通過後,再為實際服務編寫獨立的安全強化設定。執行一次涵蓋上傳、快取產生、狀態儲存與暫存檔清理的業務測試。如果應用程式透過「暫存檔加重新命名」更新資料,目標目錄本身也必須可寫入;只為單一檔案設定例外,往往並不足夠。
同時檢查符號連結實際指向的位置。設定中看似位於狀態目錄內的連結,最終可能指向另一個唯讀路徑,不能藉由放寬整個父目錄的權限來掩蓋問題。
五、疑難排解、復原與驗收紀錄
服務在啟動前失敗時,先閱讀日誌,確認是否無法建立掛載命名空間、路徑不存在,或設定拼寫有誤。啟動後才失敗時,再依檔案操作的位置判斷:程式是否暗中向程式碼目錄寫入快取、是否依賴共用暫存檔,以及是否需要額外的執行階段目錄。
正式服務若需要執行階段目錄,可使用 RuntimeDirectory 讓 systemd 管理其生命週期,再測試實際存取。不要直接將 /var、/srv 全部加入白名單;應依已確認的寫入需求逐項放行。
設定變更後,daemon-reload 只會重新載入單元定義,不會將新沙箱套用到已在執行的處理程序。正式服務必須透過受控的重新啟動,建立新的處理程序環境,並重新執行正向與反向測試。
撤銷實驗時,先停止單元、刪除本次建立的單元檔案,再重新載入定義。實驗資料目錄與帳號應在確認沒有處理程序使用後個別清理,不要執行遞迴刪除業務目錄的通用命令。正式服務則應還原變更前保留的設定,並重新啟動驗證。
最後記錄單元實際生效的設定、服務帳號、允許寫入清單、探針結束狀態,以及實際業務測試結果。驗收目標不是取得更高的安全評分,而是確保每一項必要寫入都成功,每一項預期禁止的寫入都有可解釋、可重複驗證的拒絕證據。
常見問題
ReadWritePaths 能讓服務寫入原本沒有權限的目錄嗎?
不能。它只在唯讀檔案系統沙箱中保留可寫路徑,不會改變目錄擁有者、模式位元或 ACL;服務帳號仍須具備一般檔案權限。
ProtectSystem=strict 能阻止服務讀取金鑰嗎?
不能。它主要限制寫入,不會自動隱藏可讀檔案,也不限制網路存取。敏感檔案仍須設定獨立權限,並依需求另外設定路徑隱藏與網路邊界。
AMD EPYC 7543P 獨享實體主機
每月 $458.7 起