FR-167 · 需求討論稿 · 2026-10-29(v8 待審)

需求討論稿:為什麼這樣決定

有權限的管理員按下載、按更新,其餘全自動;每一步有時限,失敗、卡住、斷電都自己退回,資料一筆不掉,不必派人到現場

現在每出一版,客戶要自己把 2.9GB 的出貨包搬上主機、照手冊敲指令升級;中途出事,要人照著畫面印出的還原指令一行行打,打不出來就得派人過去。客戶要是在國外,這條路走不通。本案分兩層解:第一層是讓升級指令絕不出事——在資料庫複本上升級、每步有時限、失敗或超時或斷電都自動退回,開放使用之前現在那份資料庫從頭到尾沒動過,所以退回不需要還原、不會掉資料。第二層是把搬檔與敲指令拿掉——系統每天向我們的更新站問一次有沒有新版,有就通知管理員;管理員確認後系統才下載(跟 iPhone 一樣),下載驗證完,有「系統更新」權限的人按「立即更新」或選排程時間,其餘全自動;連不到外網的客戶自己上傳更新包。客戶主機上不多任何常駐程式、不開任何新的埠:主系統只會寫一張更新單,主機上的 systemd 看到更新單才呼叫裝機時就在的 guidantai upgrade;真要人出手,走客戶本來就有的主機登入。

已定:確認後才下載、離線自己上傳、按鈕觸發+系統更新權限、複本升級、自動退回、停服務後不給取消鈕、不強制停工作、不做救援頁、不做常駐助手、Agent 先推一台、必經版本規則、更新站與授權中心分開、不分安全修補版與功能版、一律人按不自動裝、授權過期仍可升同主版號最新版、更新站放 Cloudflare Worker 先用免費方案、發布印章在決策者電腦一正一備、不做頻道、拒絕名單手動加、升級指令先換自己再跑、發布清單 90 天有效 待裁:沒有了。CRA 兩題決策者裁先不管 前作:FR-065 install.sh --upgrade、FR-066 Agent 安裝包、FR-062 授權中心、FR-064 簽章 雛形:prototype/index.html
§1

這案要解決什麼

人工升級失敗時,現在要人照畫面印出的指令手動還原

會掉主系統停機那段時間,Agent 送不出的檢測結果現在會被丟掉

5 步install.sh --upgrade 已有的升級步驟,本案沿用

650MBPOC 資料庫大小,複製一份只要幾十秒

決策者的話:「要能在 user 按下 accept 後自動化更新,要顯示進度,而且不可以卡住,就算卡住也要能復原」「不能因為卡死了就沒辦法復原,這樣還是我們人要去一次,要是客戶在俄羅斯怎辦」「要可以在更新的時候讓 user 反悔或是中斷」「資料絕對不可以掉」「救援頁感覺像後門」「有沒有現成的工具可以用,不要這樣搞」「沒有輸入指令這件事,就是讓有權限的人按」。

需求拆成兩層,價值差很多:

層 內容 價值 怎麼做到 詳見
A 更新不會出事 複本升級、每步時限、失敗/超時/斷電自動退回、進度看得到、可重試、資料不掉 這是「不派人到俄羅斯」的全部 改現有的升級指令,幾百行 §5 更新流程、§6 資料不掉、§7 卡死怎麼救
B 不用搬檔、不用敲指令 系統自己發現新版、確認後自己下載;有權限的人按一下或選時間,到點自己跑 管理員不必登入主機 更新站+更新中心頁;「按一下就跑」靠更新單+systemd 觸發器(D26) §11 更新站

A 是本案第一期,先做;B 第二、三期。

§2

分工概述(30 秒版)

整案:升級指令改成絕不出事;我們這邊多一個更新站;畫面多一頁更新中心負責通知、下載、按更新。客戶主機上不多任何常駐程式、不開任何新的埠。

期 做什麼 產出 完成怎麼判定(決策者檢查法)
一 升級變安全 guidantai upgrade 改成:預檢 → 停接新工作、等做完 → 停服務 → 複製資料庫 → 在複本上升級 → 健康檢查 → 開放;每步有時限、寫進度檔;失敗或超時自動退回;斷電後開機自動退回;保留上一版映像;前門在停機期間顯示進度頁;必經版本由 manifest 自動算;CI 拿真實舊庫排練;Agent 送不出的結果先存著、恢復後補送 IT 現在就敲的那道指令變安全,不必等畫面 在測試機裝 1.23.1、灌 POC 資料庫備份,一道指令直升新版成功;放一支會失敗的 migration,自動退回,資料筆數與升級前一致;升級跑到一半直接重開機,開機後自動退回;停機期間打開原網址看得到進度
二 更新站 我們這邊:獨立更新站(與授權中心分開)、發布 CLI(決策者電腦離線簽名)、CI 出包後自動登記草稿 我們發一版,客戶查得到 跑一次 tag pipeline,更新站後台出現草稿;在自己電腦跑發布指令變「已發布」;用 DEV 的授權問一次,回應裡有新版
三 更新中心 客戶這邊:每日檢查、通知、管理員確認後才下載、驗簽名、離線上傳、預檢、就緒後「立即更新」或選排程時間、寫更新單、主機 systemd 觸發器看到更新單呼叫 guidantai upgrade、進度、結果、更新紀錄、診斷包下載;新增「系統更新」權限 管理員不必搬檔、不必登入主機 DEV 收到「有新版」通知,按「下載」,看到就緒;按「立即更新」,更新中心與維護頁都看得到進度跑到完成;再試一次選排程,到點自己跑;沒有「系統更新」權限的帳號看不到更新鈕;拔網路改走離線上傳,一樣就緒
四 Agent 跟進 主系統升完後,Agent 先推一台、正常再推其餘;心跳帶目標版本 Agent 不必逐台手動升 三台 Agent 的測試環境:主系統升完,先一台變新版,其餘兩台接著升;故意讓第一台失敗,其餘兩台不會被推

依賴:一與二可以同時做;三要等一、二;四要等三。決策者功課:更新站網域出貨前再定(開發期用 workers.dev 子網域);CRA 兩題先不管;在自己電腦產發布印章一正一備(見 D3)。

§3

完整走一次:客戶 A 從 1.24.1 升到 1.27.0

背景:客戶 A 跑 1.24.1,接了 3 台 Agent。我們剛發 1.27.0,可以從 1.22.0 以上直升。

時間 誰 發生什麼
週三 我們 CI 出 1.27.0,在更新站登記草稿;決策者在自己電腦跑 guidant-release publish 1.27.0 簽名發布
週四 03:00 系統 每日檢查:向更新站出示本機授權,問「我 1.24.1,有新的嗎」。拿回簽好的版本資訊(幾 KB):有 1.27.0、可直升、2.9GB、更新說明。這時還沒下載
週四 09:00 管理員 登入看到「有新版本 1.27.0」,點進更新中心看更新說明、「服務會中斷約 10 到 20 分鐘」、預檢結果(空間夠、版本可直升)
週四 09:05 管理員 按「下載」。系統記下是誰按的
週四 09:05–09:20 系統 這時才開始下載,服務照常。下載完驗簽名與雜湊,通過後更新中心顯示「已就緒」,並寄通知給管理員
週四 09:25 管理員 回更新中心,選「排程」週六 22:00,按「確認更新」。主系統在主機上寫一張更新單(版本、時間、誰按的)
週六 21:30 系統 頁面上方提示所有人「30 分鐘後系統更新」
週六 22:00 主機 systemd 觸發器到點呼叫 guidantai upgrade:預檢 → 停止接新工作,等跑到一半的證據分類做完 → 停服務 → 複製資料庫 → 在複本上套 migration → 啟動新版 → 健康檢查
週六 22:00–22:15 管理員 打開原網址看到維護頁:目前在第幾步、這步跑了多久、剩多少時限。這頁由前門直接顯示,主系統與資料庫停機時照樣看得到
週六 22:15 系統 健康檢查通過 → 開放使用 → 通知管理員「已更新到 1.27.0」
週六 22:20 系統 先讓 1 台 Agent 升級,正常後推給另外 2 台
週一 管理員 更新中心頁顯示:主系統 1.27.0 正常、Agent 3/3 正常、下載人王大明、按更新的人王大明(排程)

變化情境:

情境 怎麼走
22:08 套 migration 卡住不動 超過時限沒有進度 → 自動退回 1.24.1 → 通知管理員「更新失敗,已退回,資料完整」,附原因與診斷包下載鈕。管理員把診斷包寄給我們,我們修好再發
22:05 機房斷電 來電開機 → 升級指令的開機檢查讀進度檔,發現上次沒做完 → 自動退回 1.24.1
22:03 管理員臨時反悔 服務還沒停(還在等工作做完):更新中心按「取消」,立刻恢復接新工作,什麼都沒動。服務已經停了:沒有取消鈕(D25),這段每步有時限,最多跑滿時限就自動有結果——升完,或退回
22:00 還有一件檢測派送在跑 等它做完,不強制停。等到 23:30 維護時段結束還沒做完,這次就不更新,恢復接新工作,通知管理員改約時間
docker 本身當掉,自動退回沒跑 唯一要人登入主機的情況:客戶 IT 用 SSH 執行 sudo guidantai recover,效果等同自動退回。這是客戶本來就有的門
客戶 A 停在 1.20.0 比必經版本 1.22.0 舊 → 更新中心頁顯示「需先升 1.22.0,再升 1.27.0」。按一次,兩個包都下載;更新分兩段跑;第二段失敗會停在 1.22.0,那是一個穩定、可用的版本
週四下載到一半網路斷了 恢復後從斷點續傳;沒下載完就不會顯示「已就緒」,更新鈕不會出現,升級指令也會拒絕用不完整的包開始
客戶 B 的主機不能連外網 走離線更新:我們把 1.27.0 更新包交給客戶,管理員在更新中心「離線更新」上傳。從驗簽名開始,後面完全一樣;只是沒有自動通知
升完兩天才發現功能有問題 不退回(退回會丟掉這兩天的資料),我們發修正版
§4

已經定案的

D22 先通知,管理員確認後才下載(跟 iPhone 一樣)

系統每天只問「有沒有新版」,拿回的是幾 KB 的版本資訊,不下載。管理員按「下載」後才開始下載 2.9GB;下載時服務照常,下載完驗過簽名才算就緒。不先下載的理由:客戶不一定要升這一版,先下載會白佔頻寬與硬碟。

D23 離線更新:管理員自己上傳

連不到外網的客戶,我們把更新包交給客戶,管理員在更新中心「離線更新」上傳。上傳後從驗簽名開始,跟線上下載完全同一條路。

D24 記錄是誰更新的

每一次更新都記:誰按的下載、誰按的更新(主系統帳號與名稱、時間、立即或排程)、誰取消的;若是 IT 在主機上手動執行或用 guidantai recover,記主機登入帳號(由指令寫進進度檔)。主系統走既有稽核事件;指令這邊寫在進度檔,主系統恢復後同步回稽核紀錄。更新中心的更新紀錄表每一列都顯示操作人。

D13 客戶機上由 guidantai upgrade 換版,主系統不碰 docker

主系統跑在容器裡,換不掉自己。換版的是裝機時已經放在主機上的 guidantai 指令(bash,/usr/local/bin/guidantai,隨出貨包更新),用 root 跑。主系統容器沒有 docker 權限,只負責通知、下載、驗簽名、顯示、寫一張更新單;能停容器、換版本的權限等於整台主機的最高權限,不交給對外的網頁服務。

D19 跑到一半的工作:等它做完,永遠不強制停

更新開始先停止接新工作,等進行中的證據分類、檢測派送做完。超過 30 分鐘還沒完,通知管理員「還有 N 筆在跑」,繼續等。等到維護時段結束還沒完,這次就不更新:什麼都沒動,恢復接新工作,通知管理員改約時間。理由:等待發生在服務停掉之前,放棄更新等於什麼事都沒發生,最壞只是延期;而半夜排程沒人能回答「要不要強制停」,延期是系統唯一不會傷到東西的決定。

D20 不做救援頁,不開第二道門

一個能退回、能重啟容器的網頁,等於拿著主機最高權限,又開在另一個埠、用另一組裝機時產生的固定密碼——對外就是多一道門,而管理埠正是攻擊者最愛掃的目標。它原本要解的情況,幾乎都已經由「時限+自動退回+開機檢查」自動處理;剩下真的要人出手的,走客戶本來就有的主機登入(guidantai recover)。詳見 §7。

D21 不做常駐的更新助手;執行者就是 guidantai upgrade,由 systemd 觸發

不新增容器、不新增獨立執行檔、不給任何容器 docker 權限。主系統寫更新單,主機上一個 systemd 檔案觸發器(兩個十行的設定檔)看到更新單幾秒內就呼叫 guidantai upgrade,排程的話到點才叫——Debian 的自動安全更新就是這個形狀。install.sh 本來就要求主機有 systemd、本來就在主機上裝 guidantai,所以這不是新成本。GitLab Runner、Ansible 這類現成工具能代勞的只有「等到時間、把腳本跑起來」這件小事,代價卻是客戶主機上多一條我們能下任意指令的通道,不划算。

D25 服務停掉之後不給取消鈕,靠時限自動判失敗、自動退回

服務停掉之前(下載中、等工作做完)隨時能取消,什麼都沒動。服務停掉之後(複製資料庫、升級、健康檢查)每一步各有時限(見 §5),任何一條觸發就自動退回,管理員不用按任何東西,最多等到時限就有結果。不給取消鈕的理由:那幾分鐘資料庫是停的,網頁沒辦法確認按的人是不是管理員,要做就得另發票或另開一道門;為了提早幾分鐘退回不值得。真要在這幾分鐘人工介入,IT 在主機敲 guidantai recover。

D26 沒有「敲指令」這件事:有「系統更新」權限的人在畫面上按

權限系統加一個「系統更新」能力,預設給系統管理員,可以另外授權。按「立即更新」或選排程時間後,主系統只做一件事:在主機上寫一張更新單(版本、時間、誰按的)。主機上的 systemd 檔案觸發器看到更新單就呼叫 guidantai upgrade,排程的到點才叫。主系統拿到的權限只有「寫一個檔」。指令本身仍在,IT 想手動跑也可以,當備援。

D14 失敗處理:開放使用前一律退回;開放使用後不退回,發修正版

開放使用那一刻是唯一的「不可退回點」。在那之前資料沒有變過,退回不掉資料;在那之後退回一定會丟掉這段時間的新資料。

D15 下載要管理員確認;更新要有「系統更新」權限的人按,可立即也可排程;任何版本都不自動裝

更新會停機、會改功能,時間由客戶挑;客戶不更新是客戶的事。D28 說明為什麼連安全修正也不自動裝。

D16 Agent 先推 1 台,正常再推其餘

Agent 失敗只影響那 1 台,主系統不受影響。Agent 版本永遠不比主系統新。

D17 必經版本只有三種情況才設

資料要分兩步搬、基線重整、升級流程本身改了。寫 migration 的人在 manifest 標記,封包時自動算出「從哪版可直升」寫進更新包;CI 用比必經版本更舊的庫試升必須被擋、用必經版本的庫試升必須成功。

D18 資料不掉的做法:在複本上升級

詳見 §6。

D2 更新站與授權中心分開

授權中心可能不對客戶開放;更新來源與簽發站合在一起,攻破一個等於兩個都破。客戶出示本機簽好的授權檔,更新站用授權中心的公鑰驗,不連授權中心、不存客戶名單。

D7 授權過期的客戶仍可升到同一個主版號的最新版;換主版號(2.0)要重新授權

授權擋的是「能用什麼」,不是「能裝什麼」:產品現在就有這道門,授權過期系統進唯讀、模組照授權檔鎖。過期客戶升到最新版,拿到的是一套唯讀的最新版,新功能照樣鎖著,我們沒有少收錢;而過期客戶的系統還在跑、還連著網路,有洞不補對雙方都是風險。所以更新站不看授權到期日,只看兩件事:這是不是一台合法安裝的機器(本機授權檔由授權中心簽過)、有沒有在拒絕名單。主版號是唯一的範圍限制:授權檔簽的是「1.x」,出 2.0 時過期客戶就停在 1.x 的最後一版,這是買斷軟體「含同代升級、換代另購」的常見做法(JetBrains、Sublime、Beyond Compare)。現在只有 1.x,這條規則目前零成本,先寫下來免得出 2.0 再吵。詳見 §10 CRA。

D27 只維護最新版;安全修正就是發下一版;五年支援靠「永遠可以免費升到最新版」達成;要找懂 CRA 的人確認兩題

我們每一版都是修正加功能混著出,從沒出過「只修安全」的版,以後也不打算分:有洞就修、修完出下一個版號,release note 寫明是安全修正。舊版的洞靠「免費升到最新版、升級保證資料不掉」解決,不另外維護舊版本線。「五年支援」寫成「五年內永遠有一條免費、不掉資料的路徑升到有修正的版本」。要拿去問懂 CRA 的人的兩題:①我們到底受不受 CRA 管(客戶在台灣、產品沒賣進歐盟的話整節 §10 可以拆掉);②若受管,「單一版本線、安全修正與功能一起出、新功能由授權鎖」這種寫法是否滿足「安全更新與功能更新分開提供」那條(CRA 原文有「技術上可行時」的但書)。CRA 主要義務 2027 年 12 月適用,design 定案前先問。

D28 一律人按,不做自動安裝;安全修正也不例外

決策者裁示:「絕對不要自動裝,客戶不更新是他的事。」理由站得住:落地企業軟體更新要停機、客戶有自己的維護時段、裝了出事是客戶的 IT 要收拾,GitLab、Jira、Sentry 這些自架產品也都不自動裝;而且我們沒有「只修安全、不動資料庫」的小版(D27),每一版都可能帶 migration,自動裝的風險跟功能版一樣大。CRA 那條原文是「適用時」才要預設自動裝,CRA 文件寫一段「本類產品不適用自動裝」的理由即可(併進 D27 第②題一起問)。代價:有洞的客戶拖著不升,我們只能在更新中心一直提醒,不能替他按。

D29 每一版的更新包至少保留 10 年

CRA 第 13 條第 9 項要求安全更新至少 10 年可下載;我們不分安全版,所以每一版都留。放 R2,一年十幾版也才幾十 GB,費用可以忽略。

D30 升級指令開跑第一步先換成新版的自己,再跑剩下的步驟;畫面上不顯示指令版本

主機上的 guidantai 是裝機時從出貨包複製到 /usr/local/bin/ 的,客戶跑 1.24.1 時那支指令也是 1.24.1 版。新版的升級流程(複本升級、每步時限、自動退回)寫在新版指令裡,舊指令不會做。所以 guidantai upgrade 第一件事是從已驗過簽名與雜湊的新更新包取出新版指令、換掉 /usr/local/bin/guidantai、再以新版接著跑;之後每次升級跑的都是「目標版本的升級流程」,舊版指令只要會「驗包、換自己」就夠(GitLab omnibus、Docker installer 都這樣)。代價:第一期出去那版指令要把「換自己」這一步做對,之後才不會再卡;換自己失敗就當預檢不過,什麼都不動。這是指令內部的事,更新中心頁不顯示「指令版本」「相容」之類的欄位,客戶不需要知道有這支指令。

D31 發布清單 90 天有效,過期重新蓋章;不做「回應 24 小時內」

更新站手上沒有私鑰(D3),簽章是決策者發版時離線蓋的,更新站只是原樣轉交,沒辦法對每次回應重新簽時間。所以「回應超過 24 小時拒收」做不到。改成清單本身帶簽發時間與有效期 90 天,客戶端超過就拒收;一版發出去 90 天內沒下一版,決策者跑一次 guidant-release publish 重新蓋章延長。這是 TUF 的 timestamp 角色的簡化版:防的是有人把過期的舊清單一直餵給客戶讓他升不上來,90 天是「被卡的最長時間」。

更新站那一組

D1 更新包放在 S3 相容的私有空間,用 Cloudflare R2

程式只認 S3 介面,換供應商改一個網址。R2 對外下載流量不收費:客戶每次更新抓 2.9GB,放別家每出一版都要付一次流量費。

D2a 更新站跑在 Cloudflare Worker,先用免費方案

更新站是我們這邊唯一對客戶開放的服務:回答「有沒有新版、你能不能拿」(合法安裝、不在拒絕名單、同主版號)、產短效下載網址、收客戶回報的版號。對外服務最花的是守機器的人力,不是寫程式的時間;R2、弱點資料網域、文件站都已在 Cloudflare,多開一個功能不多一家帳務。先用免費方案(每天 10 萬次請求,我們一天不到 100 次),跑一段時間再看要不要升付費方案(每月 5 美元)或改成自己養一台。代價:驗章要改呼叫 Cloudflare 的加密函式,多一份驗章實作,用授權中心真簽出來的檔當守衛測試。排除:跟授權中心同一台(D2)、自己養一台對外主機(長期要守)、完全不寫程式只在 R2 放清單檔(做不到 D6、D9、D12)。

D3 發版的最後一步在決策者電腦蓋章;發布印章一正一備,跟授權中心的印章分開

更新包要帶一個只有我們蓋得出來的印章(Ed25519 簽章),客戶系統用程式裡的印樣(公鑰)對章,對不上整包丟掉。CI 在 188 出包、上傳 R2、在更新站登記成「草稿」,客戶看不到;決策者在自己電腦跑 guidant-release publish <版> 蓋章,才變「已發布」。印章(私鑰)只在決策者電腦,188 被攻破的人出得了包、發不了版——這是「上版要決策者明示放行」鐵律的程式化。

印章怎麼保護、怎麼換(業界標準做法,TUF、apt、Android 系統更新都這樣):

  • 一開始刻兩顆:主力那顆放決策者電腦,備用那顆產完就封存放別處,不在任何電腦上。產品出貨時兩顆的印樣都帶著。
  • 每顆印章有編號,發布清單寫「這版是幾號印章蓋的」,客戶系統拿對應編號的印樣對章;沒那個編號直接判驗證失敗。主產品驗授權檔已經是這樣做(PUBLIC_KEYS 以 kid 認公鑰),照搬。
  • 換印樣只能藏在一次「對章通過」的更新裡:用客戶現在認的印章蓋一版包,包裡印新印樣;客戶系統先拿手上的舊印樣對章,對得上才裝、裝完才收下新印樣。包裡的印樣在對章失敗那一刻跟整包一起被丟掉,進不了門。更新站、R2、管理員畫面都不能換印樣。
  • 新舊印樣重疊一版:換印樣那版裝完客戶兩個都認,下一版才拿掉舊的,中間漏一版的客戶也認得。
  • 主印章丟了或被偷:拆封備用印章,發一版宣告舊印章作廢、帶一顆新的備用印樣。已經裝好的舊版完全不受影響,對章只發生在下載新版那一刻。
  • 兩顆都丟了,才需要人到客戶那邊重裝,所以備用那顆不跟主印章放同一台電腦。
  • 很久沒更新的客戶,中間我們換過兩次印章,先裝換第一次印章那版再往上升,就是必經版本規則(D17),不另外做東西。

D6 不做頻道;草稿可以指定給幾台內部機器先裝

本意是 STG、POC 比客戶早拿到新版當白老鼠。頻道是給客戶看的概念,但客戶永遠只該看到正式版;改成草稿在後台指定「哪幾台機器看得到」,一個欄位解決,後台也少一欄。

D8 不做差異更新;D9 客戶只回報授權編號、機器指紋、現行版號、最近一次升級結果;D10 每天檢查一次、管理員可關;D11 客戶出示本機授權檔、更新站用授權中心公鑰驗

四項在流程表(§5)、安全原則(§11)與 D2 已寫定,這裡只登記。D10 補一句:每日檢查是唯一的開關;關了會在更新中心一直顯示提醒。

D12 拒絕名單,第一版決策者在更新站後台手動加

授權過期也能升同主版號最新版(D7)之後,拒絕名單只剩「我們主動要切斷某客戶」這一種情況,不常用,不值得自動化。

客製化版本的客戶怎麼走更新,這輪先不談。

§5

更新流程

每一步都有「失敗或卡住怎麼辦」和「能不能取消」。技術選擇掛在步驟底下,不單獨討論。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
  A0["① 發現新版<br/>每日檢查・只拿版本資訊"] --> B["② 管理員按下載<br/>記錄是誰"]
  U["離線:管理員上傳更新包"] --> V
  B --> V["③ 下載・驗簽名<br/>服務照常"]
  V --> T["④ 有權限的人按更新<br/>立即或排程・寫更新單<br/>systemd 呼叫 guidantai upgrade・預檢"]
  T --> D["⑤ 停止接新工作<br/>等進行中的做完・不強制停"]
  D --> E["⑥ 停服務・複製資料庫"]
  E --> F["⑦ 在複本上升級<br/>載入映像・套 migration"]
  F --> G["⑧ 啟動新版・健康檢查"]
  G --> H["⑨ 開放使用<br/>不可退回點"]
  H --> I["⑩ Agent 跟進<br/>先 1 台再其餘"]
  V -. 驗不過 .-> X0["停在更新前<br/>列出原因・什麼都沒動"]
  T -. 預檢不過 .-> X0
  D -. 取消・等不完 .-> X0
  E -. 失敗・斷電 .-> R
  F -. 失敗・超時・斷電 .-> R
  G -. 不過・斷電 .-> R
  R["自動退回<br/>改接現在那份資料庫<br/>啟動舊版映像"] --> X1["回到更新前<br/>資料完整・通知管理員"]
  style H fill:#F6EBD5,stroke:#9C6B12,stroke-width:2px
  style R fill:#FDECEC,stroke:#B42318
  style X1 fill:#EEF6EE,stroke:#3F7D45
圖 1 — 更新流程:服務停掉之前取消等於什麼都沒發生;停掉之後任何一條失敗的線都自動回到「更新前」
步驟 做什麼 失敗或卡住時 能取消嗎 技術選擇
① 發現新版 每天向更新站出示本機授權問一次,拿回簽好的版本資訊(幾 KB),通知管理員。不下載 連不到更新站就安靜略過,更新中心顯示「最近一次檢查失敗」 — 照弱點資料每日同步的形狀做;管理員也可按「立即檢查」
② 管理員按下載 看更新說明、停機時間、要不要先升必經版本;按「下載」。記錄是誰按的。任何版本都不自動下載 — 能 主系統的更新中心頁
③ 下載、驗簽名 確認後才下載 2.9GB,服務照常;斷線續傳;下載完驗簽名與雜湊,通過後顯示「已就緒」並通知管理員。離線客戶改成管理員上傳,從驗簽名開始同一條路 驗不過就丟掉重下;沒下載完就不顯示就緒,更新鈕不出現 能,取消下載 包就是現在出貨的 guidant-ai-<版>.tar.gz,不另開格式;更新站只給短效下載網址,檔案直接從私有倉庫拿
④ 按更新、預檢 有「系統更新」權限的人按「立即更新」或選排程時間,任何版本都不自動裝(D28)。主系統寫更新單;systemd 觸發器呼叫 guidantai upgrade(排程的到點才叫);先提醒在線使用者,再預檢:空間夠不夠放複本與新映像、資料庫連得上、版本路徑合法;先從已驗過簽名的新包裡取出新版 guidantai 換掉自己,再用新版跑後面每一步(D30) 列出不過的項目,什麼都不動 能 就緒時更新中心先跑一次預檢,執行時再跑一次;空間不足時提示可清掉多少舊映像;指令仍可由 IT 手動執行當備援
⑤ 停止接新工作 不再派新的證據分類、檢測工作給 Agent;等進行中的做完。不強制停(D19) 超過 30 分鐘通知管理員,繼續等;到維護時段結束還沒完就這次不更新,恢復接新工作 能:更新中心按「取消」,什麼都沒動 背景工作現有的租約機制
⑥ 停服務、複製資料庫 停 API、背景工作、資料庫;把整個資料庫資料夾複製一份 複製出錯或超過預估時間三倍:刪掉複本,啟動舊版 沒有取消鈕(D25) 先停 API 是 POC 踩過的教訓(不停的話 Agent 心跳會讓 migration 鎖死);複製時間跟資料庫大小成正比,預檢要把預估停機時間算給管理員看
⑦ 在複本上升級 載入新映像、驗雜湊、對複本套 migration,逐支回報進度 載入出錯或超過 5 分鐘、任一支 migration 超過 10 分鐘沒有新進度:自動退回 沒有取消鈕 現有 migrate.sh 已經逐支套用,只要每支寫進度檔
⑧ 啟動新版、健康檢查 新版接複本啟動,跑健康檢查 180 秒內沒變健康、或登入與主要 API 打不通:自動退回 沒有取消鈕 現有 180 秒健康檢查沿用,加上登入與幾支主要 API 的實打
⑨ 開放使用 換上新版前門、解除維護頁、讓使用者進來 — 不能,這是不可退回點 之後有問題發修正版
⑩ Agent 跟進 主系統正常後,Agent 目標版本改成新版;先 1 台,正常再推其餘 只影響那台 Agent 個別 Agent 可暫停 見 §9

整段總上限 60 分鐘:⑥~⑧ 加起來超過一律判失敗退回,不會永遠卡著。

進度檔:一個放在主機上的純文字檔(upgrade-progress.json),只有 guidantai upgrade 寫,記錄這次更新的編號、目前第幾步、每步開始時間、時限、結果、錯誤訊息、誰觸發的。更新中心頁、維護頁、開機檢查都讀這一份,所以三邊看到的永遠一樣。

更新期間畫面從哪來

更新中途主系統和資料庫是停的,畫面不能靠它。分三段:

階段 畫面誰給 資料從哪來
服務停掉之前(下載、預檢、等工作做完) 主系統的更新中心,跟平常一樣 後端讀進度檔
服務停掉之後(複製資料庫、升級、健康檢查) 前門(nginx)直接給一張維護頁,後端、資料庫都不在 維護頁每 3 秒讀一次進度檔
開放使用之後 主系統的更新中心 後端讀進度檔,顯示結果、寫進更新紀錄
  • 維護頁是 nginx 的內建用法:後端連不上時改送一張靜態頁,很多網站的「系統維護中」就是這樣做的。這張頁是幾十行的 HTML,畫出步驟清單、進度條、這步跑了多久、還剩多少時限。沒有任何按鈕。進度檔從主機以唯讀方式掛進 nginx 容器;頁面與設定隨 FE 映像出貨。
  • 前門最後才換:nginx 自己也是要升級的容器之一,現在 install.sh --upgrade 是整組一起重建,那樣維護頁會跟著消失。改成舊版 nginx 從頭留到尾負責顯示,第 ⑨ 步才換成新版;使用者只在「開放使用」那一刻感覺到幾秒斷線。
  • IT 若是手動執行指令,終端機上同時看到同樣的步驟逐行印出來,跟現在一樣。
§6

資料絕對不掉怎麼做到

在複本上升級,現在那份不動

現有做法 本案做法
升級對象 直接在現在那份資料庫上套 migration 先複製一份,在複本上套
失敗時 從升級前的備份還原 改接回現在那份,啟動舊版映像
退回要多久 還原一次,資料越多越久,而且是在最慌的時候做 幾秒
退回本身會不會失敗 會:還原是寫入動作 幾乎不會:沒有寫入,只是換接

中間沒有新資料:從第 ⑥ 步停服務到第 ⑨ 步開放使用,服務是關著的,沒有任何新資料寫進來。所以退回到現在那份等於一筆都沒掉。

開放使用之後:新資料寫進新的那份。舊的那份保留到下一次更新成功為止,標示為「升級前快照」,只用於災難救援,不是回滾——拿它還原會丟掉開放後的資料,要由我們判斷才用。

其他會掉資料的地方

地方 風險 處理
**證據檔案(SeaweedFS,POC 約 10GB) 太大,每次複製太慢 不複製**。服務停著時沒有新檔案寫進來,檔案不會變。規則:升級過程不准改寫既有檔案;真有一版需要改檔案,就把那版標成特殊版,那次才連檔案一起複製
設定檔與憑證 migration 或新版可能改設定 第 ⑥ 步一起複製一份,退回時一起換回
Agent 跑完但送不出的結果 現在 Agent 回報失敗只記 log,結果直接丟掉(evidence-agent core/task_executor.py 的 _result) 第一期補:Agent 送不出的結果先存在本機,主系統恢復後補送。第 ⑤ 步停止派新工作也能減少這種情況
跑到一半的背景工作 停服務時被中斷 不會發生:第 ⑤ 步等它做完才停,不強制停(D19);等不完就這次不更新

實查 POC 的數字

項目 實際值 對設計的意義
資料庫 533MB,資料夾約 650MB 複製一份幾十秒,可行。客戶用兩年到 50GB 時可能要十幾分鐘,預檢要把預估停機時間算出來
證據檔案 約 10GB 不複製的理由
硬碟剩餘 20GB,已用 90% 預檢一定要先算空間
主機上的映像與 build 殘留 映像 35GB、build 快取 21GB POC 自己的殘留,不是產品問題;但預檢要提示可清掉多少
§7

卡死也救得回來,不必派人、不開新的門

原則:救援靠自動,不靠人按

所有「回來」都由升級指令自己完成,不需要人在旁邊,也不需要任何新的對外入口:

  • 每步有時限:超時判定卡住,自動退回。
  • 開機檢查:guidantai 隨 docker 啟動時先讀進度檔,上次更新停在 ⑥~⑧ 沒做完,就自動退回。
  • 退回不是還原:只是改接回沒動過的那份資料庫、啟動保留的舊版映像,幾十秒,幾乎不會失敗。

真的要人出手的情況只剩「連自動退回都沒跑」(例如 docker 本身當掉),那時走客戶本來就有的主機登入:IT 用 SSH 進主機執行 sudo guidantai recover。我們沒有多開任何門,誰能進主機、怎麼管,是客戶自己的政策。

各種情況

情況 結果 要人嗎
migration 卡住不動 超時自動退回 不用
新版起不來 健康檢查不過,自動退回 不用
更新中斷電或主機重開 開機檢查讀進度檔,自動退回 不用
管理員反悔(服務停掉之前) 更新中心按「取消」,恢復接新工作,什麼都沒動 按一下
管理員反悔(服務停掉之後) 沒有取消鈕;最多跑滿時限就有結果(D25) 不用
退回後想再試 更新包還在主機上,不用重下載;更新中心按「重試」,從預檢重跑 按一下
docker 本身當掉、自動退回沒跑 唯一要登入主機的情況:IT 執行 sudo guidantai recover,效果等同自動退回 敲一行
硬碟滿了 預檢就擋下,不會開始 不用
要我們遠端判斷原因 更新中心「下載診斷包」或 IT 執行 sudo guidantai diag:進度檔、各容器 log、主機狀態;自動遮掉密碼與金鑰。寄給我們就能判斷 寄個檔

任何情況的終點只有兩個:新版正常開放,或退回更新前、資料完整。不會停在中間。

不做「從中斷的那一步接著跑」:斷在哪一步沒人說得準,從複本重新升一次比接著跑穩,而且複本是整份重做,不會留半套的狀態。

§8

跨版升級與必經版本

每版出一整包,可以一次跨好幾版。 包裡帶著從第一支到最新一支的全部 migration,migrate.sh 拿客戶庫已套的清單比對,缺哪些就依序補哪些。所以 1.24.1 直升 1.27.0,就是把中間三版的 migration 一次套完。不做差異包:要維護每兩版之間的差異,是另一套要顧的東西。

必經版本(D17 已定):只有三種情況才設——資料要分兩步搬(中間那版的程式負責搬資料)、基線重整(新包不再帶舊 migration)、升級流程本身改了(舊的指令看不懂新包)。由 manifest 標記自動算出「從哪版可直升」寫進更新包,現在寫死的 1.0.0 改成自動產生。

跨版排練:每次封正式包前,CI 拿真實的舊庫(不是乾淨新裝)試升,這是 1.21.0 抓到 fr093-4d 順序問題的做法。STG 與 POC 現在都是 1.26.0、跟主線同版,自然升級測不到跨版,要另外在測試機用 189 留著的 1.23.1 映像加 POC 資料庫備份排練。全程在測試機做,不碰 STG、POC。

§9

Agent 跟進

項目 做法
誰告訴 Agent 要升級 主系統。主系統升完並開放後,把 Agent 目標版本改成新版;Agent 下次心跳看到就去主系統拿包
Agent 拿包從哪來 主系統(主系統的更新包裡就帶 Agent 的包)。Agent 不連外網、不連更新站
誰在 Agent 主機上換版 Agent 自己的升級指令,跟主系統同一套做法:複本、時限、自動退回
分批 先 1 台,正常再推其餘(D16);任何一台失敗就暫停推送,通知管理員
版本約束 Agent 永遠不比主系統新;主系統退回時,還沒升的 Agent 不會被推
只升 Agent,主系統不動 可以,同一套機制。兩種情況:①主系統已經升了、某幾台 Agent 落後(新加的、上次暫停的、當時失敗的)——更新中心 Agent 區選那幾台按「推送」,走一樣的先一台再其餘;②我們只修 Agent(例如 Agent 的檢測工具有洞)——更新站發 Agent 單獨修正版(只有 Agent 的包,幾十 MB),主系統照一樣的路下載、驗簽名、存起來,但不停服務、不升自己,直接推給 Agent。版本約束不變:Agent 修正版的主線版號必須等於主系統現行版
作業系統更新 不在本案(Flatcar 那條線另案)
§10

版本只有一種,授權只管同主版號(CRA)

版本不分「安全修補版」與「功能版」:我們每一版都是修正加功能混著出,安全修正就是發下一版(D27)。授權管的是能用什麼與能升到哪一代,不管能不能裝:

客戶狀態 能不能升 升上去拿到什麼
授權有效 同主版號內任何新版 完整功能,照授權檔的模組
授權過期 同主版號(1.x)內任何新版,含安全修正 唯讀的最新版;新功能照樣鎖(產品現有的授權門)
換主版號(2.0) 要重新授權 —
在拒絕名單 更新站不回應 —

更新站因此只判三件事:本機授權檔是不是授權中心簽的、有沒有在拒絕名單、目標版本跟授權檔簽的主版號一不一樣。不看授權到期日。

CRA 對更新的要求怎麼對:

條文 要求 本案怎麼落
第 13 條第 8 項 支援期至少 5 年,到期年月賣的時候寫明 「五年內永遠可以免費升到同主版號最新版」(D27);要問顧問
附件一第二部分第 8 點 安全更新免費 過期客戶也能升到含修正的最新版(D7)
附件一第二部分第 2 點 技術上可行時,安全更新與功能更新分開提供 不分。寫「單一版本線、新功能由授權鎖、分開提供技術上不可行」的理由;要問顧問(D27 第②題)
第 13 條第 9 項 每個安全更新至少 10 年可下載 每一版都留 10 年(D29)
附件一第一部分第 2 點 c 款 適用時安全更新預設自動裝 不自動裝,寫「本類產品不適用」理由(D28);要問顧問
附件一第二部分第 8 點 安全更新附說明,告訴使用者該做什麼 含安全修正的版本,更新說明固定多「影響範圍」「建議動作」兩段

例子:客戶 C 跑 1.25.1,授權 2026-09 到期沒續,系統進唯讀。2026-11 我們修一個洞,出 1.27.0。客戶 C 的更新中心照樣收到 1.27.0,管理員自己決定要不要按;升上去之後還是唯讀、新功能鎖著,但洞補了。2028 我們出 2.0,客戶 C 看不到,要續約才有。

§11

更新站:新版從哪裡來

更新站回答流程第 ① 步的「有沒有新版」,並在第 ③ 步給短效下載網址。離線客戶不經過它,自己上傳,兩條路驗同一套簽名。

架構:誰在哪、誰能碰誰

架構圖:客戶機房、Cloudflare、我們內網、決策者電腦四區

圖 2 — 架構:四個地方,對外服務上沒有任何私鑰(點圖看大圖)

  • 客戶只連 Cloudflare,連不到授權中心、CI、188。
  • 紅框是私鑰,只有兩個:授權中心的簽發私鑰在內網,發布金鑰在決策者電腦。對外的更新站手上沒有任何私鑰,被攻破只拿到幾張表與公鑰。
  • 授權中心與更新站之間只有一條虛線:複製公鑰表。沒有 token、沒有 API、沒有連線。
  • 2.9GB 的檔不經過更新站:更新站只回一個短效網址,客戶直接向倉庫拿。
角色 管什麼 手上有什麼 被攻破會怎樣
授權中心(現有,內網) 簽發授權 簽發私鑰、客戶名單 能偽造授權,不能發更新
更新站(新,對客戶開放) 回答「有沒有新版、你能不能拿」(合法安裝、不在拒絕名單、同主版號)、產短效下載網址、記客戶在哪版 只有公鑰、倉庫讀權、幾張表 能「不給」,不能「給假的」:客戶端驗簽名與雜湊全擋
私有倉庫 存檔、傳檔 更新包 檔被換掉驗不過
發布金鑰 簽發布清單 離線,在決策者電腦 不在任何伺服器上

發版流程:CI 出包後上傳倉庫、在更新站登記為「草稿」,客戶看不到;決策者在自己電腦跑 guidant-release publish <版>,用離線發布金鑰簽名後才對外。這是「上版要決策者明示放行」鐵律的程式化。

安全原則(照 TUF,做輕量版)

原則 本案怎麼落
發布金鑰獨立、離線 跟授權簽發鑰是兩把;只在決策者電腦;CI 只能登記草稿;一正一備,備用封存,換鑰用舊鑰背書(D3)
兩個系統互不持有對方憑證 攻破一邊,另一邊的能力拿不到
客戶端不信任更新站 只信兩把公鑰,更新站回的每份清單都要過發布公鑰驗章
不能被卡在舊版 發布清單帶簽發時間與 90 天有效期,過期的清單客戶端拒收;要延長就重新蓋章(D31)
拒絕降版 只接受版號大於現行版的清單
只回報必要資訊 授權編號、機器指紋、現行版號、升級結果;不送使用資料、不送客戶名稱

更新站怎麼蓋(D2a:Cloudflare Worker,TypeScript,先用免費方案)

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart LR
  Q1["對外更新站<br/>客戶每天打,最常被敲"] --> Q2["和授權中心同一台?"]
  Q2 -- 是 --> X1["✗ 入口與簽發私鑰同機<br/>破一個等於破兩個"]
  Q2 -- 否 --> Q3["自己養一台?"]
  Q3 -- 是 --> X2["✗ OS・備份・憑證・DDoS<br/>對外機器長期要守"]
  Q3 -- 否,無伺服器 --> Q4["哪一家?"]
  Q4 -- 新的一家 --> X3["✗ 多一家帳務與除錯"]
  Q4 -- Cloudflare --> Q5["R2・vulndb・文件站都在<br/>同一家多開一功能<br/>用什麼寫?"]
  Q5 -- Python --> X4["✗ Python Worker 仍是測試版<br/>cryptography 進不去"]
  Q5 -- TypeScript --> OK["✓ WebCrypto・D1・R2 原生<br/>真簽章當守衛測試"]
  OK --> R["TS Worker+D1+R2<br/>零機器・同一家・正路"]
圖 3 — 選型:每一步問一個問題

選 Worker 的理由:零機器(對外服務最花人力的是守,不是寫)、同一家(R2、弱點資料網域、文件站都已在 Cloudflare)、語言選 TypeScript(決策者 2026-10-29 改判:Python Worker 仍是測試版,TS 是 Cloudflare 正路,WebCrypto/D1/R2 都原生;發布 CLI 同 repo 用 TS,Node 內建 Ed25519,一個 repo 一套工具鏈)。費用:Worker 免費方案每天 10 萬次請求,50 家客戶每天問一次是 50 次,先用免費方案;付費方案每月 5 美元含 1,000 萬次。R2 每 GB 每月 0.015 美元、前 10GB 免費、下載流量不收費,三條線各出修補留 10 年估 300GB 約 4.5 美元。自己養一台對外小機器最便宜也是每月 5 到 10 美元,錢差不多,多的是要守。代價:cryptography 套件目前進不去 Worker,驗章要改呼叫 WebCrypto,等於多一份驗章實作;用授權中心真簽出來的檔當測試資料,Worker 驗不過就擋部署,兩份實作被同一組真資料綁住。

§12

現況盤點:沿用什麼、補什麼

零件 現況 本案動作
升級指令 install.sh --upgrade 五步:檢查版本、備份(失敗即中止)、載入映像並驗雜湊、套 migration 並斷言待套 0 支、換版加 180 秒健康檢查;維運子命令已獨立成 /usr/local/bin/guidantai(bash、root、隨出貨包更新) extend。guidantai upgrade 呼叫的就是它;補停接新工作、先停服務、複本升級、每步時限、進度檔、自動退回、開機檢查、保留上一版映像、前門最後才換。不另寫一套,否則手動與線上兩條路會各自演化
migration 執行 scripts/init/migrate.sh 比對已套清單、依序補套 extend。逐支寫進度
出貨包 guidant-ai-<版>.tar.gz + .sha256,內含映像、VERSION、manifest、bundle-info;1.26.0 壓縮後 2.9GB,其中後端映像佔一半以上,第三方映像(資料庫、快取、檔案庫、分類器、錯誤追蹤)佔其餘 reuse。bundle-info 的「適用起始版號」改成自動算
簽章 授權中心 Ed25519 簽章函式、主產品 PUBLIC_KEYS 以 kid 認公鑰 reuse 函式。發布金鑰另一把
主機端 CLI guidantai 子命令(status/logs/diag…) extend。加 upgrade、recover;diag 的輸出併進診斷包
前門 nginx 容器,有 healthcheck;後端掛掉時回 502 extend。加維護頁:後端連不上時改送靜態頁,頁面讀唯讀掛進來的進度檔
主機 install.sh 要求 systemd(取機器識別碼);容器 restart: unless-stopped 隨 docker 開機起 reuse。更新單觸發器(D26)就是兩個 systemd 設定檔,隨 install.sh 裝
權限 common/authz/ 的 capability 軸,主體域守門可用 route decorator extend。加「系統更新」能力,更新中心的下載、更新、取消端點用它守門
Agent 回報 送不出的結果只記 log、直接丟掉 extend。先存本機、恢復後補送
背景工作 工作單在資料庫、有租約與寬限秒數 reuse。第 ⑤ 步「停接新工作、等做完」用它
弱點資料每日同步 心跳 observer 每日拉、驗章、連不上靜默略過 reuse 形狀(第 ① 步每日檢查照它做)
缺 主系統沒有更新中心頁 new:FE 一頁+BE 的檢查、下載、驗簽名、讀進度檔端點
缺 對客戶開放的更新站 new(第二期)
§13

待決策

沒有待裁項。CRA 兩題決策者 2026-10-29 裁先不管(有答案再回頭看 §15 反悔條件)——原題留著:①我們受不受 CRA 管;②若受管,「不分安全版、不自動裝、過期仍可免費升同主版號最新版」這種寫法過不過。這卡 design 定案、不卡討論稿。

§14

業界怎麼做

誰 更新怎麼跑 本案借了什麼
GitLab、Sentry、Mattermost、Jira Data Center(企業自架) IT 在主機上跑一道指令;產品負責讓那道指令安全:先備份、套 migration、檢查、出事還原;GitLab 公開升級路徑表列出必經版本 第一期的形狀:一道絕不出事的指令;必經版本規則
Debian unattended-upgrades systemd 定時器到點自動裝安全更新;管理員可關、可延後 只借「排程到點由 systemd 跑」這個形狀(D26);不借自動裝(D28)
vCenter(VMware 落地版) 管理頁「暫存」與「安裝」分開;升級前先備份 下載與安裝分開
TrueNAS、Synology 升級前拍檔案系統快照,失敗退回快照 升級前留一份能退回的副本。我們管不了客戶主機的檔案系統,所以用複製資料庫代替快照
Android、ChromeOS 新版裝在另一個分割區,起不來自動退回舊分割區 在複本上升級、健康檢查不過自動退回
Nextcloud、WordPress 網頁按一下就升級;Nextcloud 官方對正式環境不建議用,WordPress 出事率高 反面教材:主系統不自己升級自己
Wazuh、Elastic Fleet 管理端推 Agent 升級包;Agent 版本不可比管理端新;分批、可暫停、失敗自動退回 Agent 跟主系統走、先推一台
Replicated(幫軟體廠商做落地版的平台) 授權序號拿更新;線上離線同一份包 憑授權拿更新。不買:只支援 Kubernetes,我們是 docker compose;Business 方案每月 3,000 美元起再加每授權費
TUF(CNCF 畢業的更新安全規範) 簽章在發布端、驗章在客戶端;金鑰分開離線;清單帶時效;拒絕降版 更新站的安全原則

業界共識:下載與安裝分開、升級前留副本、失敗自動退回、Agent 跟著主系統走。企業市場多半由 IT 敲指令;本案把「敲指令」換成「寫更新單讓 systemd 叫」,主系統拿到的權限沒有變多。本案多做的一件是「在複本上升級」,讓退回不必還原。

§15

排除的方案

方案 為什麼不選
直接在現在那份資料庫上升級、失敗再還原 還原是寫入動作,本身會失敗、會卡;資料越大越久,而且發生在最慌的時候
升完之後提供回滾 回滾一定丟掉開放後的新資料,等於把風險交給客戶。改發修正版
自動安裝(任何版本) 會停機、可能帶 migration;客戶的維護時段與風險由客戶自己定,客戶不更新是客戶的事(D28)
分「安全修補版」與「功能版」兩種 我們每版都是修正加功能混著出,分開等於多維護一套發版規則與舊版本線;過期客戶要拿修正改用「可升同主版號最新版」解(D7、D27)。若 CRA 顧問說非分不可再回頭,更新站只多一個「種類」欄
每年一個長期支援版、支援五年 每個資安修補要補五次,人力撐不住(D27)
主系統容器直接操作 docker 換自己 容器要拿到 docker 的完整權限,被打穿等於整台主機淪陷;而且主系統壞了就沒人能救
救援頁(另一個埠+專用密碼) 等於多開一道門,固定密碼不跟著客戶帳號政策走;它要解的情況幾乎都已由自動退回涵蓋(D20)
常駐的更新助手(容器拿 docker 權限,或 Nuitka 獨立執行檔) 多一個要維護、要資安審查的常駐程式;它做的四件事(到點跑、寫進度、退回、開機檢查)用現有的 guidantai 加 systemd 觸發器就有(D21)
服務停掉之後的取消鈕(短效票) 資料庫停了認不了人,要另發票或另開門;那幾分鐘靠時限自動退回就夠(D25)
裝 GitLab Runner、Ansible agent 當執行者 能代勞的只有「等到時間、跑腳本」;代價是客戶主機上多一條我們能下任意指令的通道,資安審查過不了
中心推送進客戶機(我們用 SSH 連進去裝) 要客戶開入站或 VPN,資安合規客戶不會接受
從中斷的那一步接著跑 斷在哪沒人說得準;從複本整份重做比接著跑穩
差異更新包 要維護每兩版之間的差異;整包跨版靠 migration 補套就夠
只換有洞的那顆容器 技術上做得到(包裡只放改到的映像、清單寫以哪版為底、升級指令只改那一行),但多一種包的規則要維護與測試;企業自架產品(GitLab、Sentry、Mattermost)安全修補也是整包。客戶數上百或客戶抱怨流量時再回頭做,做法不變
把更新做進授權中心 授權中心可能不對客戶開放;合在一起攻破一個等於兩個(D2)
Harbor 開給客戶 docker pull 多養一個對外關鍵服務、每客戶發帳號;BE 映像每版層全變,省不到流量
買 Replicated 只支援 Kubernetes,不適用 docker compose 部署;費用高
§16

資料模型草案

主機上的檔(都在資料目錄,主系統容器唯讀掛載):

檔 誰寫 內容
進度檔 upgrade-progress.json 只有 guidantai upgrade 這次更新的編號、從哪版到哪版、路徑(含必經版本的分段)、目前步驟、每步開始時間、時限、結果、錯誤訊息、執行人(主機帳號)、現在那份資料庫與複本的位置、上一版映像標籤
更新單 upgrade-order.json 主系統(有權限的人按更新時) 版本、立即或排程時間、誰按的;systemd 觸發器看到就呼叫 guidantai upgrade(排程的到點才叫),跑完刪掉。主系統對這個目錄只有寫權
更新包 主系統下載或管理員上傳 guidant-ai-<版>.tar.gz 與簽名;驗過才留

主系統資料表(主線 migration):

表 欄位要點 用途
product_update_jobs 從版、到版、狀態(downloading/ready/scheduled/running/succeeded/rolled_back/skipped/cancelled)、下載人、按更新的人(帳號+名稱+時間、立即或排程)、取消人、來源(線上/離線上傳)、排程時間、開始與結束時間、失敗原因 更新中心頁的更新紀錄;結束後從進度檔同步結果
agent_update_rollouts Agent、目標版本、批次、狀態、時間、來源(跟著主系統/單獨推送/Agent 修正版) 第四期 Agent 分批,含只升 Agent

更新站的表:product_releases(版本、主版號、可直升起始版、雜湊、大小、狀態、草稿先開給哪幾台內部機器、是否含安全修正、簽好的清單與印章編號、保留到期)、instance_version_reports(授權編號、指紋、現行版本、最近回報、最近升級結果;沒有客戶名稱)、release_download_log、license_denylist、signing_keys(印章編號、印樣、狀態:主力/備用/作廢)。

§17

不做什麼

  • 不做救援頁、不開任何新的埠或密碼;不做常駐的更新助手。
  • 不做作業系統更新(Flatcar 另案)。
  • 不做差異更新包;不做只換單顆容器;每一版都是整包。
  • 不做更新頻道(stable/beta);內部先裝用草稿指定機器。
  • 不做升完之後的回滾,改發修正版。
  • 不做自動下載與自動安裝,任何版本都一樣:管理員確認後才下載;有權限的人按了才更新(D28)。
  • 不分安全修補版與功能版;不維護舊版本線;安全修正就是發下一版(D27)。
  • 不做服務停掉之後的取消鈕。
  • 不做「從中斷那一步接著跑」。
  • 不送使用資料給我們。
  • 不談客製化版本客戶的更新路徑(這輪先不談)。
  • 災難復原(機器整台壞了在新機重建)另開一案;本案的「升級前快照」可以被它用,但異地備份、定期演練不在本案。
§18

先看哪裡

  1. 點 prototype/index.html:有新版通知、更新中心(下載、離線上傳)、下載中、已就緒(立即或排程)、維護頁進度、自動退回、完成與 Agent、跨版兩段、例外、更新站後台(草稿先開給內部機器、印章)。
  2. CRA 兩題先不管(決策者 2026-10-29)。
  3. 拍板後落 design.md、拆卡。預估第一期跨 BE(guidantai、install.sh、migrate.sh、出包、nginx 維護頁)與 evidence-agent(結果暫存補送);第二期新 repo update-center(已建、已登記工作坊)與 BE 的 CI;第三期 BE(更新中心端點、系統更新權限、更新單、systemd 觸發器)、FE;第四期 BE、evidence-agent(含只升 Agent 與 Agent 修正版)。