> 「變更單記錄的是人以為發生的事;日誌記錄的,才是真實發生的事。」
在 Day 26 中,我們建立了規範化的 ISO 27001 合規用變更單機制,試圖回答「誰在什麼時候改了什麼」。但實務上大家都心知肚明,變更單是人寫的。那是操作者「記得寫的」、「願意寫的」,以及「兵荒馬亂中來得及寫」的版本。
資安稽核真正要看的是另一份紀錄:機器自己留下的痕跡。
sudo 記錄誰以 root 身分敲了什麼指令;將這兩份資料放在同一個時間軸上去做對比:
在 Day 26 執行第一張單 CR-2026-0001 的那一晚,VictoriaLogs 已經默默展示了這件事的威力。查詢變更窗內的資料,除了操作者手動敲的 change-mark.sh 標記外,收集端的 sudo 自己記下了「誰以 metrics 身分執行了命令」,NAS 事件日誌也忠實記錄了「Virtualization Station 建立了快照 CR-2026-0001-before」。
機器寫的那一份一直都放在硬碟中,也就是說呢,從 Day 21 把日誌管道串起來時就保持在那邊了。今天,剛好可以這套機制收斂成自動化比對系統:
audit-report.py),抓出 6 種特權管理動作,逐一對齊 Day 26 的變更窗口,有填單的歸入單號,沒填單的自動抓出。本日程式碼位於 onprem-logs v0.5.0 的
audit/目錄,警報規則位於 onprem-metrics v0.3.2。本日的環境改動本身,即是一張正式變更單:CR-2026-0007。
要進行稽核比對,首先盤點地端環境中最關鍵的 6 種特權行為:
| 來源類型 | 產生日誌的程序 / 格式 | 存放位置 | Day 27 之前的狀態 |
|---|---|---|---|
| FortiGate 設定變更與登入 | FortiOS event 日誌 |
記憶體環狀日誌(60F 約保留 8 天) | 未送出。Day 22 的 syslogd2 只送 traffic,event 被刻意排除 |
| PVE 任務 (Task) | qm、pve-ha-lrm、pveupdate 的 starting task UPID:... |
兩台 PVE 節點的 journald |
已串接(Day 21) |
| Linux sudo 提權 | 各節點的 sudo |
各自的 journald |
已串接 |
| SSH 登入 | OpenSSH 的 Accepted(PVE 9 上為 sshd-session) |
各自的 journald |
已串接 |
| NAS 事件 | QNAP QuLog 的 event log: |
Log Sender 透過 UDP 514 送出 | 已串接(Day 21,Day 22 僅取 conn log:) |
| 變更單標記 | change-mark.sh 與 logger -t change |
收集端的 journald |
已串接(Day 26) |
前 5 種日誌天天都在送入收集端,並自動封存進 WORM(這些來源均不在 ARCHIVE_SKIP 排除清單中)。唯獨缺了邊界防火牆的異動事件,而這往往是資安影響層面最廣、稽核要求最嚴格的一環。
在寫程式分析這 6 種行為時,現場實測後,也調整兩個原先採用官方文件的理想假設:
pvedaemon:原本看官方資料會以為任務日誌會由處理 API/Web GUI 的 pvedaemon 寫入。但實測發現現場 8 天內沒有任何一行 pvedaemon 的 UPID。真正產生 UPID 的是命令列與 HA 的 qm、負責 HA 故障轉移起停虛擬機的 pve-ha-lrm,以及每日排程更新清單的 pveupdate。sshd-session 處理,journald 的 SYSLOG_IDENTIFIER 隨之變更。若日誌過濾器只寫 sshd,兩台 PVE 的 SSH 登入會無聲無息地被漏掉。在 Day 22 中,刻意排除了 FortiGate 的 Traffic 日誌進入 WORM(流量巨大、包含大量內網雜訊、封存成本高)。但**事件日誌(Event Log)**的性質完全相反,它的每一筆設定改動,都是誰在什麼時候改了什麼的直接稽核證據。
然而,兩者由同一台設備產生,RFC 5424 標頭的 hostname 都是設備名稱。若送往同一個 Syslog 埠,兩者會混在同一個 Stream syslog-fgt-edge 中,一排除就通通排除。
因此,這次決定利用 VictoriaLogs 多埠監聽的特性,將 FortiGate 的 syslogd3 導向收集端的 TCP 515 埠,並在該埠掛上標籤 audit="fortigate",串流欄位定義為 (hostname, audit):
- -syslog.listenAddr.tcp=:514,:515
- -syslog.streamFields.tcp=["hostname","app_name"],["hostname","audit"]
- -syslog.extraFields.tcp={},{"audit":"fortigate"}
注意:在 VictoriaLogs 1.52.0 中,
extraFields的多個設定必須是獨立的 JSON 物件依序排列,若包裹成單一 JSON 陣列會導致啟動失敗。
這樣一來,每日封存腳本 archive-day.sh 只要多辨識一種類型,凡帶有 audit 欄位的即歸類為 audit-<設備名稱>,而既有的 syslog-<設備名稱> 查詢加上 -audit:* 進行排除。流量日誌依然不封存,而防火牆的特權變更天天進 WORM。
FortiGate 60F 記憶體日誌容量約能保留 8 天(共約 31,000 筆,每日 4,000 ~ 5,000 筆)。分析後發現,其中 86% 到 90% 居然全是 Wireless 噪音(AP 每兩分鐘回報終端 DHCP SOLICIT、鄰近 Rogue AP 上下線等)。這些資訊不僅對變更稽核毫無幫助,還夾帶大量內網 MAC 位址。
因此,我們在 FortiGate 端透過 free-style 過濾器直接在來源端消噪:
config log syslogd3 filter
set severity information
config free-style
edit 1
set category webfilter
set filter "(type utm)"
set filter-type exclude
next
edit 2
set category event
set filter "(subtype wireless)"
set filter-type exclude
next
end
end
過濾後,每日日誌量驟降至約 500 筆(主要是 5 分鐘一次的系統效能統計與 DHCP Ack),真正的設定變更與管理員登入僅佔個位數。以 Zstandard 壓縮封存後,一天僅約 28 KB,整年不到 10 MB,封存成本微乎其微。
透過 REST API 套用後,在 VictoriaLogs 進行即時驗證:
_time:>=2026-10-11T08:34:00Z audit:=fortigate
| unpack_logfmt from _msg fields (type, subtype, logid)
| stats by (type, subtype, logid) count() as n

收集端收到的第一筆 FortiGate 設定變更日誌,恰好就是 syslogd3 被啟用的瞬間:
logid="0100044546" type="event" subtype="system" level="information" vd="root" logdesc="Attribute configured"
user="api-admin" ui="https(192.168.100.101)" action="Edit" cfgtid=10289367 cfgpath="log.syslogd3.setting"
cfgattr="status[disable->enable]server[->192.168.100.149]mode[udp->reliable]port[514->515]format[default->rfc5424]"
msg="Edit log.syslogd3.setting "
這筆日誌完美展現了稽核所需的關鍵要素:
user="api-admin":操作者是誰。ui="https(192.168.100.101)":從哪個介面與來源 IP 進行的操作。cfgpath、cfgattr:改了哪條設定,從什麼值變更為什麼值。cfgtid:交易編號(一次 Web GUI 儲存涉及多個欄位時,會共用同一個交易 ID)。這是設備核心直接產生的硬證據,任何人都無法在事後竄改或否認。
![收集端收到的第一筆 FortiGate 事件,與封存演練的 MANIFEST.tsv 裡新的 audit-fgt-edge 列]
audit-report.py有了日誌來源,接著是核心邏輯,如何將機器日誌與 Git 上的變更單自動比對?
這次們實作了 audit-report.py。它從 VictoriaLogs 拉取 6 種來源,將異質的日誌正規化為統一的 5 個欄位:時間、來源、主機、操作者、行為摘要:
| 來源代碼 | 操作者判定 | 行為摘要正規化格式 |
|---|---|---|
fortigate-config |
user 欄位 |
action cfgpath cfgobj cfgattr (ui) |
fortigate-login |
user 欄位 |
logdesc reason=... (ui) |
pve-task |
UPID 內的使用者 | 任務類型、目標與呼叫者,例:qmstart 102 (pve-ha-lrm) |
sudo |
觸發 sudo 的帳號 | as root: /usr/bin/...(被拒絕時前綴 REFUSED) |
ssh-login |
登入的使用者 | login publickey from <ip> key ED25519 SHA256:... |
nas-event |
Users: 欄位 |
Application/Category: Content |
接著,腳本讀取本機同步的 onprem-changes Git 儲存庫,找出狀態為 scheduled、in_progress、done、rolled_back 的變更單時段。
[ VictoriaLogs 異質日誌 ] [ Git 變更單時窗 (RFC) ]
(FortiGate, PVE, Sudo...) (CR-2026-xxxx)
│ │
└───► [ audit-report.py ] ◄───┘
│
┌────────────────┴────────────────┐
▼ ▼
[ 變更窗內操作 ] [ 無變更單操作 ]
(正常執行,歸檔至單號) (日常排程?補單?資安發現?)
│
┌────────┴────────┐
▼ ▼
[ ignore.txt ] [ 稽核異常發現 ]
(白名單排除) (觸發告警 / 調查)
audit/ignore.txt):aptupdate、監控系統探針)。ignore.txt 採用正規表達式匹配「操作者 + 行為」。每一條規則都必須附帶註解說明為什麼它是日常情況,這樣以後方便自己知道和稽核查詢。
done 的變更單,時窗內完全沒有出現 CR-xxxx begin 標記,代表工程師「做了變更,卻沒跑 change-mark.sh」(此時系統監控告警未被靜音、變更軌跡未被記錄)。腳本會直接將該單列為「已完成但無標記」。執行分析過去 8 天(涵蓋 Day 19 到 Day 27)的歷史資料:
audit-report.py --since 2026-10-03T00:00:00+08:00 --until 2026-10-11T16:40:00+08:00 --top 10
![第一份稽核報告:依來源、沒有變更單的動作、依變更單、沒有標記的單子]
面對這 6,345 筆「無單操作」,經聚合分析後歸納為 394 種行為,並逐一還原真相:
nas-secondary 曾於非維護時段被特定帳號手動重開機與關機,完全沒有對應的工單記錄。spark01 上有 25 次來自 NAS IP 的密碼登入。追查確認是管理員經由 Tailscale VPN 登入,雖確認為內部人員,但突顯了主機未強制停用密碼認證的風險,列入待辦改善。報告是給人看的,但高風險的未授權變更必須在一小時內被發現。我們將 audit-report.py 設為每 10 分鐘執行一次,並匯出 Prometheus Textfile 指標(audit.prom)。
在 onprem-metrics 中定義了 4 條對帳告警規則:
| 告警名稱 | 等級 (Severity) | 觸發條件與設計初衷 |
|---|---|---|
AuditUnattributedFortigateChange |
Warning | 1 小時內出現未歸屬變更單的 FortiGate 設定異動。防火牆政策變更影響全網,若無工單,不是偷改就是緊急變更未補單,必須即時介入。 |
AuditUnattributedActions |
Info | 1 小時內出現其他來源的未歸屬行為。納入每日報告檢視,多數為日常排程,應補入 ignore.txt 或補開單。 |
AuditChangeWithoutMarks |
Info | 變更單標記為完成,但時窗內缺漏 change-mark.sh 紀錄。 |
AuditReportStale |
Warning | 對帳腳本超過 30 分鐘未產出指標(靜默失效防護),觸發時會抑制(Inhibit)其餘告警。 |

在 Alertmanager 配置抑制規則,避免比對用程式壞掉時發出一堆誤報。
變更管理的即時性考驗:
本次設定CR-2026-0007原訂時窗為 14:00~16:00,但 PR 合併延遲至 15:51,只剩 9 分鐘。如果硬著頭皮在窗外施作,今天寫的這套系統就會「大義滅親」,立刻發出AuditUnattributedFortigateChange警告!因此依規範發起 PR #3 將時窗展延至 16:30~18:30,在合規時窗內從容完成變更。
在 Day 21 承諾過必須定期抽查設備端原生紀錄與收集端紀錄的時間一致性,確保沒有日誌在傳送過程中遭遇時序竄改或延遲。
比對 Day 26 建立快照的事件紀錄:
22:46:30 與 22:46:44
22:46:30 與 22:46:44

比對本次啟用 syslogd3 的事件(cfgtid=10289367):
eventtime:16:34:21.818
_time:16:34:22
前一筆系統事件 0100022905 差距為 +0.25 秒。考量 Syslog RFC 5424 標頭時間精度與網路傳輸延遲,誤差完全落在 1 秒內的可信度範圍。
建了這套自動化對帳機制,就代表能高枕無憂了嗎?身為資安與資訊工程師,必須清楚認知系統的極限與盲區:
CR-2026-0006 時,為了重設忘記的 root 密碼,我們透過 NAS 的 Virtualization Station 虛擬機器主控台(VNC)進入 GRUB 單人模式。這種操作完全不經過網路與 SSH,機器日誌只記錄到「虛擬機關機、開機」,完全無法得知「是誰坐在主控台前面重設了密碼」。audit-report.py 只檢查操作「是否落在時窗內」,無法細緻比對「指令是否符合工單計畫」。若變更窗申請了 2 小時,實際施作僅花 10 分鐘,操作者在剩餘時間順手修改了無關的機器,系統依然會將其標記為合法。時窗開得越寬,防護網就越脆弱。
ignore.txt 是人寫的。如果維運人員為了圖方便,將整台伺服器或廣域 IP 放入忽略名單,這套稽核機制就會名存實亡。以下為本日場域實作(CR-2026-0007)的完整標準作業程序:
# 1. 變更時窗開始:打上 Git 標記並觸發監控靜音
sudo -u metrics /opt/onprem-changes/scripts/change-mark.sh begin CR-2026-0007
# 2. FortiGate 變更前完整備份(注意:FortiOS 備份端點必須使用 POST)
curl -sk -X POST -H "Authorization: Bearer $FGT_TOKEN" \
"https://<fgt-edge>/api/v2/monitor/system/config/backup?scope=global&destination=file" -o fgt-backup.conf
# 3. 收集端配置:防火牆白名單新增 515 埠,更新 compose 監聽設定並重建
sudoedit /etc/default/onprem-logs-allowlist # 確保 AUDIT_FROM 包含防火牆 IP
sudo systemctl restart onprem-logs-allowlist
sudo docker compose up -d
# 4. FortiGate 套用 syslogd3 設定(設定 filter 與 server),並進行驗證
curl -s http://127.0.0.1:9428/select/logsql/query -d \
'query=_time:1h audit:=fortigate | unpack_logfmt from _msg fields (type, subtype, logid) | stats by (type, subtype, logid) count()'
# 5. 部署稽核對帳腳本與排程
sed -n '/Day 27/,$p' audit/collector.cron | sudo tee -a /etc/cron.d/onprem-logs
sudo -u metrics env CHANGES_DIR=/opt/onprem-changes/changes \
./audit/audit-report.py --since 2026-10-03T00:00:00+08:00 --top 25
# 6. Prometheus 規則驗證與熱載入
promtool check rules prometheus/rules/audit.yml
curl -s -X POST http://127.0.0.1:9090/-/reload
# 7. 重啟 Alertmanager 以載入新的單檔 mount 配置
sudo docker restart onprem-metrics-alertmanager-1
# 8. 變更時窗結束:解除標記與靜音
sudo -u metrics /opt/onprem-changes/scripts/change-mark.sh end CR-2026-0007
# 9. 翌日驗證:檢驗 WORM 封存清單是否正確納入 audit- 來源
ARCHIVE=/mnt/worm/logs ./archive/verify-archive.sh "$(date -u -d yesterday +%F)" --against-live | grep audit-
tests/smoke-audit.sh,涵蓋雙監聽器驗證、假工單對帳、白名單過濾、長時窗單據判定與 Textfile 指標格式。MANIFEST.tsv 新增 audit-fgt-edge 封存列。變更管理不該是「防禦性填表」的形式主義。當我們將機器日誌(Log)與工程師的工單(Ticket)透過程式自動對帳,變更單就不再只是一紙應付稽核的報告,而是變成即時維護系統架構清晰度、防範未授權入侵的實體防線。
明天(Day 28),本系列文章將進入維運流程中最嚴峻的考驗,回滾(Rollback)與 BCP 災難演練。雖然在 Day 26 的每張單子都寫了復原計畫,但它們真的能動嗎?
不知道,因此將隨機抽查 3 張單子的回退程序,以秒為單位計時實測,看回滾之後日誌與稽核軌跡會留下什麼痕跡囉。
CR-2026-0007
Day 1|系列規劃與場域總覽:從跑得起來到可長期維運,先談為什麼要封裝
Day 2|Container Station 薄殼架構:QPKG 骨架、生命週期腳本與狀態頁
Day 3|版本鎖定與供應鏈:image digest、SHA256SUMS 與授權檔
Day 4|GitHub Actions 自動打包與 release:從 git tag 到可驗證的套件
Day 5|案例 Jellyfin 與 Roon:硬體轉碼的裝置直通與主機網路
Day 6|案例 changedetection.io 與 Homepage:資安與資訊公告監看 + 整合入口
Day 7|在地自建 ComfyUI 的生存指南:自建 Image 鎖定、GPU 直通與三層目錄架構
Day 8|案例實戰:Open WebUI + Ollama 重構記,把專案回到通用框架會省下什麼呢?
Day 9|容量規劃:用模型與硬體搭配矩陣決定地端節點與儲存規格
Day 10|上游引擎一改版,地端 AI 環境用的配方壞了怎麼辦 ? patch 移植實戰紀錄
Day 11|DGX Spark 節點基線:DGX OS 初始設定、帳號與 SSH 加固
Day 12|熱與統一記憶體守護:取樣器、自動降載與長時間負載實測
Day 13|別讓節點踩爆模型!NFS 集中模型庫的單寫多讀、權限凍結與完整性治理
Day 14|儲存規劃與路徑實測:RAID 60 與 RAIDZ2 試算,SSD、NFS、iSCSI 載入時間解析
Day 15|Proxmox VE 雙節點 HA:Virtualization Station 上的 QDevice
Day 16|Proxmox VE 備份與還原:HDP for Business Beta 的排程、保留與還原演練
Day 17|儲存設備備份策略與還原演練:快照、HBS 3 與 3-2-1 備份策略
Day 18|3-2-1 的最後一步:異地物件儲存、保留政策與冷熱資料成本精算
Day 19|指標收集:把前十八天的取樣器接進同一個時間序列
Day 20|儀表板與告警,打造一個簡潔的單一面板
Day 21|日誌集中與保存:節點不裝新代理,封存要能比對
Day 22|被擋下的封包才是便宜情報:從防火牆靜默盲點到日誌降噪與自動化告警
Day 23|日誌找不到原因?從 tcpdump 最小權限封包擷取到 WORM 封存的網路排錯實戰
Day 24|稽核問「某台工作站上個月連到哪裡了」?FortiGate AUP 雙軌日誌實踐:線上 LogsQL 檢索與離線對帳修煉
Day 25|資產生命週期與授權 EOL 監看:到期日只是一個數字,通通收進 Prometheus 時間序列!
Day 26|變更管理實戰:串起 Git 變更單、監控與日誌,讓每次改動都有跡可循
Day 27|自動化稽核比對:當機器留下的日誌,對上工程師寫的變更單(VictoriaLogs × FortiGate × PVE)
Day 28|Rollback 實戰演練:沒真正執行過的復原步驟,永遠只是一場美麗的假設
Day 29|打造地端維運的 AI 圍欄:Agent 授權閘道實作,無工單即無工具,無核准即無不可逆變更
Day 30|完賽總結|算清一年的電費、硬體折舊與雲端代價:地端 AI 維運 30 天留下的真實資產