長期上線主機的維護項目:會累積的資料、定期讀的 log、對外入口的盤點、安全更新、會到期的設定與檢查間隔的推算
這篇整理一台單獨部署、要長期放在公開網路上的主機,在沒有人登入、也沒有部署新版本的期間仍然會改變的狀態,以及維護時對每一類狀態做的事:會累積的資料(nginx 的 log、journald、syslog 的檔案、套件快取、容器的 log 與映像檔、應用程式自己寫的資料、建置產物)各自由誰寫入、由什麼機制清理,以及磁碟寫滿時不同寫入者的反應;定期要讀的 log 與每一份要看的欄位;對外入口在主機與雲端防火牆兩層怎麼盤點、哪些入口該關或該收窄;安全更新依軟體的安裝管道各自由什麼更新(含語言與容器映像檔的漏洞掃描工具在排程裡的 exit code)、更新後是否自動重開機的條件與序列主控台的準備;會到期與會改變的設定(憑證、網域、雲端 CLI 的登入、管理者的 IP、CDN 的位址段、免費額度的閒置回收);怎麼從每一類狀態的變化速度推出檢查間隔、哪些項目要改成自動處理;最後是接手或第一次盤點一台已經上線的主機時,先處理哪幾項。
範圍是一台主機加上它前面的 CDN 與雲端防火牆(主機有好幾台、要依角色分組時,清單的值怎麼分層、怎麼產生每台的清單,在 多台主機的維護清單:值的分層與合併的位置、用 Ansible 的 inventory 與範本產生每台的清單、由 Terraform 的輸出產生 inventory、跨主機執行的 exit code,以及單台清單的適用範圍),例子取自 Oracle Linux 10.2 上跑 nginx 與一個寫 SQLite 的服務的主機;Debian 系發行版與 Docker 的對照寫在各節。Debian 系的套件預設,是在 Debian 12、Debian 13 與 Ubuntu 24.04 的官方容器映像檔裡裝上同名套件之後讀出來的,再拿 Oracle Cloud 上一台 Ubuntu 24.04 的雲端主機對照(預先裝了哪些套件、journal 的目錄、核心更新前後的重開機判斷);其他業者的映像檔與 Debian 12 的雲端映像檔預先裝了哪些套件,要在主機上用 dpkg -s <套件名> 查。服務要暫停一段時間時的處理在 服務暫停與恢復上線:只停服務、停止主機、終止主機到全部刪除各自保留的資源,暫停期間仍計費與仍被指向的資源,恢復前要查的漂移與恢復的順序,把本篇的項目寫成機器讀得懂的清單在 主機維護清單寫成 TOML:JSON、YAML、TOML 的取捨、TOML 1.0 與 1.1 的版本差異、清單的欄位設計與只讀檢查腳本。
沒有人操作時主機上會改變的狀態
一台主機上線之後,狀態的改變來自主機上一直在寫入的元件,以及主機內外一直在計時的期限。依改變的來源分成四類:
- 會累積的資料:nginx 每處理一個請求就在 access log 寫一行,journald 收下所有 systemd 服務的輸出,dnf 下載套件時留下中繼資料,應用程式寫自己的資料。寫入的元件一直在跑,而刪除舊資料的元件要另外存在。
- 會老化的軟體:已安裝的軟體版本是固定的,而已知漏洞的清單每天增加。同一個 nginx 版本,上線那天沒有已知漏洞,幾週之後可能有。
- 會漂移的入口:安裝一個新套件可能同時帶進一個開始接聽連接埠的服務;有雲端帳號權限的人可以在主控台單獨修改防火牆規則,主機上看不出這個改動。
- 會到期與會改變的外部設定:憑證、網域註冊、雲端 CLI 的登入有期限;管理者家裡的 IP 會被 ISP 重新分配;CDN 業者會增加自己的位址段;免費方案的業者可能回收閒置的主機。
維護就是定期把每一類狀態量一次,跟允許的範圍比對,超出時處理。允許的範圍以一個警戒值為界:警戒值設在真正出事之前,跟出事之間留著處理要花的時間。磁碟的警戒值設在寫滿之前、離寫滿還夠清出空間或擴充磁碟的剩餘空間,憑證的警戒值設在到期之前、離到期還夠續期的天數。量測值超過警戒值的時候還來得及處理;到了真正出事的那一刻之後,後果依項目而定:有的清掉就恢復(套件快取把磁碟用量推過警戒值,清掉之後就回到警戒值以內),有的收不回來(磁碟完全寫滿的那段時間裡,所有寫入者沒寫進去的紀錄;已經被利用的漏洞)。每一類的警戒值設在哪裡、多久量一次,由那一類狀態改變的速度、處理要花的時間,以及越過出事那一刻之後收不收得回來決定,推算的方法寫在〈檢查間隔與哪些項目改成自動處理的推算〉。
會累積的資料:寫入者、清理機制與磁碟寫滿的後果
會累積的資料各自有一個寫入者,而清理機制有三種情形:套件預設就裝好、要自己設定、或根本不存在。盤點時每一項資料寫出寫入者、清理機制與量測方式;清理機制那一格寫不出來的資料,就是要補上清理機制的那一項。
| 資料 | 寫入者 | 清理機制 | 量測方式 |
|---|---|---|---|
| nginx 的 access log 與 error log | nginx,每個請求一行 | 套件附的 logrotate 設定;Oracle Linux 10.2 的 nginx 套件設為每天輪替、保留 10 份、壓縮舊檔 | du -sh /var/log/nginx |
| journald | 所有 systemd 服務的標準輸出 | journald 自己依上限刪最舊的檔案;上限的設定依紀錄存在磁碟還是記憶體分兩組,見下一小節 | journalctl --disk-usage |
syslog 的檔案(/var/log/messages、/var/log/secure) | rsyslog,從 journald 轉出(有裝 rsyslog 時) | 套件附的 logrotate 設定 | du -sh /var/log/messages /var/log/secure |
| 套件快取 | dnf(Debian 系是 apt) | Oracle Linux 10.2 的 dnf 預設 keepcache = 0,裝完就刪掉下載的套件;中繼資料留在 /var/cache/dnf,由定期的 makecache 更新 | du -sh /var/cache/dnf |
| 容器的 log、映像檔、建置快取與 volume | Docker 的 daemon(dockerd) | 容器 log 預設的 json-file 驅動不輪替,要在 /etc/docker/daemon.json 的 log-opts 設 max-size 與 max-file;映像檔與建置快取要另外 prune | docker system df |
| 應用程式的資料(例如 SQLite 資料庫) | 應用程式本身 | 應用程式自己實作的保留期限,或作業系統這一層的容量隔離(把資料放在獨立的檔案系統或 volume、設 quota);兩者都沒有就沒有上限 | 資料庫檔案的大小,以及最舊一筆資料的時間 |
建置產物(node_modules、Go 的建置快取) | 在主機上建置的部署腳本 | 沒有;部署腳本不清,就一直留著 | du -sh 對應的目錄 |
| 暫存檔 | 各程式 | systemd-tmpfiles 依 tmpfiles.d 的規則定期清理 | du -sh /tmp /var/tmp |
這張表裡的預設值是一台 Oracle Linux 10.2 主機上查到的,換發行版或版本時會不同。查法是讀 /etc/logrotate.d/ 底下對應的檔案、systemd-analyze cat-config systemd/journald.conf(印出主設定檔與全部 .d 目錄合併後的內容)與套件管理器的設定檔;沒有寫在設定檔裡的值,要讀主機上那個版本自己的 man page(man journald.conf),而不是線上最新版的文件。Docker 的 json-file 驅動預設不輪替,這一點寫在 Docker 的文件裡;daemon.json 改了之後,既有的容器不會套用新的設定,要重新建立容器才生效。服務跑在容器裡時,量測的位置也要跟著換:nginx 在容器裡通常把 log 寫到標準輸出,主機上的 /var/log/nginx 量到的是空目錄,這個零不代表 log 很小。
Debian 系的對應預設在 Debian 12、Debian 13 與 Ubuntu 24.04 上相同(logrotate 3.21 與 3.22):nginx 套件的 logrotate 設定是每天輪替、保留 14 份、壓縮舊檔,輪替之後執行 invoke-rc.d nginx rotate 讓 nginx 重新開檔;/etc/logrotate.conf 的全域預設是每週輪替、保留 4 份,沒有開 dateext,所以輪替出來的檔名是 .1、.2.gz 這種序號,Oracle Linux 10.2 開了 dateext,檔名帶日期。logrotate 由 logrotate.timer 每天觸發;/etc/cron.daily/logrotate 也在,但這支腳本在 /run/systemd/system 存在(systemd 是 init)時直接結束,所以實際執行的只有 timer 那一份。syslog 檔案那一列要先確認 rsyslog 在不在:這三個容器裝了 systemd、logrotate、nginx、unattended-upgrades 與 needrestart 之後都沒有 rsyslog,/var/log/auth.log、/var/log/syslog 也就不存在。
journald 的紀錄存在記憶體還是磁碟
journald 的紀錄存在磁碟還是記憶體,由 Storage 決定,這個預設值正好是線上文件與主機不一致的例子:systemd 最新版的文件寫的預設是 persistent,而 Oracle Linux 10.2 的 systemd 257 預設是 auto,/var/log/journal 目錄存在時才寫到磁碟。兩種狀態的結果不同:
/var/log/journal存在,或設定了Storage=persistent:紀錄寫在磁碟上,跨得過重開機;上限由SystemMaxUse(預設檔案系統大小的 10%)與SystemKeepFree(預設保留 15%)決定。- 兩者都沒有:紀錄只存在
/run底下的記憶體裡,每次重開機就清空;上限改由RuntimeMaxUse與RuntimeKeepFree依/run的大小計算。重開機之前的 sshd 失敗登入、自動更新的執行結果,在 journal 裡都查不到,只剩 rsyslog 轉出的/var/log/secure與/var/log/messages。主機重開過機、journalctl --list-boots卻只列出一次開機,或/var/log/journal不存在,就代表主機處在這個狀態;上線之後還沒重開過機的主機,紀錄存在磁碟上時同樣只列出一次開機,所以只看開機次數分不出兩種狀態。
上面兩組上限各自另有 4G 的上限,算出來超過 4G 時以 4G 為準。Oracle Cloud 的 Oracle Linux 10.2 映像檔處在紀錄只存在記憶體的那一種狀態;在 /etc/systemd/journald.conf.d/ 放一個寫了 Storage=persistent 的設定檔並重啟 systemd-journald 之後,那台主機的 journalctl --list-boots 在下一次重開機後列出兩次開機。
Debian 系的預設落在另一種狀態:Debian 12、Debian 13 與 Ubuntu 24.04 的 systemd 套件(252、257、255)在安裝時由套件的 postinst 腳本建立 /var/log/journal,Storage 同樣是預設的 auto,所以紀錄寫在磁碟上、跨得過重開機。雲端映像檔在 Oracle Cloud 的 Ubuntu 24.04 映像檔(2026 年 9 月的版本)上確認過:第一次開機之後 ls -ld /var/log/journal 印出 drwxr-sr-x+ root systemd-journal,目錄與裡面的 journal 檔(-rw-r-----+ root systemd-journal)已經套好群組與 ACL,只屬於 systemd-journal 群組的系統帳號跑 journalctl -n 1 以 exit 0 讀得到,不必另外處理;這個映像檔同時預先裝了 rsyslog,/var/log/syslog 與 /var/log/auth.log 另外有一份。Debian 12 的雲端映像檔沒有確認,ls -ld /var/log/journal 查得到。
磁碟寫滿時各寫入者的反應
磁碟寫滿時,每個寫入者都會寫入失敗,而失敗之後的反應依元件的設計分成三種,外面看得到的程度不同:
- 寫入失敗時繼續服務:nginx 寫不進 log 時照樣回應一般的請求,套件管理器下載更新失敗時,已經在執行的服務照樣跑在舊版本上。從主機外面看不出變化,代價是這段時間的紀錄缺掉、安全更新沒有套用。例外是要寫暫存檔的請求:request body 超過 nginx 的
client_body_buffer_size(預設 8k 或 16k)時,nginx 把 body 寫進暫存檔,上游的回應超過 proxy 的 buffer 時也一樣,這兩種請求在磁碟寫滿時會失敗,外面看得到錯誤。 - 寫入失敗時回錯誤:應用程式寫資料庫失敗時,多半對送資料進來的請求回 5xx。外面看得到錯誤率上升,而那段時間收到的資料遺失。
- 寫入失敗時停止:部分資料庫在交易紀錄寫不進去時會停止服務以保護資料(PostgreSQL 的 WAL 寫入失敗是這一種)。服務整個停掉,外面看得到,恢復要先清出空間再重啟。
繼續服務的那一種,大部分請求在外面沒有徵兆,只有在主機上量磁碟用量才看得到;回錯誤與停止的兩種雖然看得到,看到的時候資料已經遺失或服務已經停了。這是磁碟用量要在寫滿之前定期量、而不是等出錯時才看的原因。
對外寫入端點的增長速度
應用程式的資料是這幾項裡,增長速度最受外部請求影響的一項:access log 每個請求只多一行,而接受外部寫入的端點(收事件、收表單、收上傳),每個請求可以寫進一整個 request body。這一項的允許範圍要用最壞情況算,而最壞情況取決於端點前面有什麼樣的速率限制:
- 有全域的速率限制(整個端點共用一個計數,nginx 以
$server_name這類固定值當limit_req_zone的鍵):全域速率與 body 上限的乘積就是寫進應用程式資料的上限。例如每秒 20 次、每次最多 64 KB,每秒約 1.3 MB,一天約 110 GB。 - 只有以來源位址為鍵的速率限制(
$binary_remote_addr、或 CDN 後面以CF-Connecting-IP為鍵):每個來源各自計數,上限還要乘上來源的數量,而來源的數量由送請求的人控制,所以算不出一個可以拿來跟磁碟比的上限。例如一個收前端事件的端點只依來源 IP 限流,一個爬蟲從上千個雲端位址各送幾個請求時,每個來源都在自己的限額內,access log 裡代表速率限制拒絕的狀態碼一直是零(nginx 預設回 503,示範主機的設定檔用limit_req_status 429改成 429),磁碟照樣在增長。以CF-Connecting-IP為鍵還有一個前提:源站只接受 CDN 送來的連線,否則這個標頭可以由請求的人自己填,每次換一個值就繞過計數。 - 完全沒有速率限制:跟上一種相同,沒有可以比的上限。
- 只在私有網路裡、來源數量固定(例如只有公司 VPN 內的幾台機器會送):上限是來源數乘上每個來源的速率,再乘上 body 上限。
算出來的上限只算了 request body;落到磁碟的量還要加上儲存層的放大(資料庫的索引、交易紀錄、應用程式自己的轉換),實際的倍數要用一批真實的請求量一次才知道。拿這個上限跟剩下的磁碟空間比,算得出最壞情況多久會寫滿:剩 15 GB、每秒寫入 1.3 MB 時,只算 body 大約 3 小時,加上放大會更短。這個時間比人的檢查間隔短時,保留期限、配額或容量隔離要由應用程式或作業系統自己執行,或在端點前面加上全域的速率限制,不能靠定期檢查。
全域的速率限制只限制寫進應用程式資料的量,限制不了 log 的量:被拒絕的請求照樣在 access log 寫一行,nginx 的 limit_req 預設在 error 等級替每一次拒絕再記一行(limit_req_log_level 預設 error)。送進來的請求越多,這兩份 log 寫得越快,寫入量由送請求的人決定;每秒上萬個被拒絕的請求,log 的寫入速度可以超過上面算出的資料上限,而 logrotate 一天才輪替一次。所以大量的請求要在 CDN 這一層就擋掉(CDN 的速率限制規則),或把這個端點的拒絕紀錄調低等級、不寫 access log,並另外設一條磁碟剩餘空間的告警。
清理的動作要分開判斷:log 與快取有現成的輪替機制,清掉的是已經過了保留期限的資料;應用程式的資料,以及事後要拿來跟 CDN 的事件紀錄逐筆比對請求來源的 access log(CDN 的每一筆事件帶著請求編號,源站的 access log 記下同一個編號),則可能是之後要查證的材料,保留多久是業務的決定(要回頭查多久以前的事件),磁碟只決定能存多少。所以維護時量測可以每次都做,刪除要依每一項事先定好的保留期限做。
定期讀的 log:每一份要看的欄位與它代表的狀態
自動告警針對的是事先知道的壞狀態(服務沒有回應、錯誤率超過門檻);定期讀 log 處理的是另外兩件事:確認部署時的假設仍然成立,以及發現還沒有被寫成告警的新模式。一份 log 適合哪一種,看它的欄位在正常狀態下的值能否預測:能預測的,偏離就代表某個假設失效;不能預測的,讀的目的是找出值得寫成告警的模式。
| Log | 要看的欄位 | 值代表的狀態 |
|---|---|---|
| 反向代理的 access log | 狀態碼的分布 | 4xx 與 5xx 的比例跟前一次相比上升,代表有新的請求來源或後端開始失敗 |
速率限制的拒絕(nginx 預設回 503,設了 limit_req_status 429 時是 429)與 413 的次數 | 速率限制或 body 上限實際被觸發;來源集中在少數位址時是單一來源灌入,來源分散而且是網站的正常路徑時,代表門檻可能比正常使用者的用量還低 | |
Host 標頭 | 主機只放行 CDN 時,Host 應該只有自己的網域(主機自己送出的 冒煙測試 除外);出現 IP 位址或別的網域,代表有請求繞過了自己的 CDN 設定(例如別人在他的 CDN 帳號把網域指到這台主機) | |
| 請求的路徑 | /wp-login.php、/.env 這類網站不存在的路徑是掃描器的背景流量;同樣的掃描出現在網站實際存在的路徑上時,掃描的對象就是這個網站本身 | |
| 反向代理的 error log | upstream 連線失敗、權限拒絕 | upstream 失敗代表後端服務沒有在聽;權限拒絕多半是 SELinux(強制存取控制)或檔案權限在某次變更後不再符合;body 上限攔下過大請求、速率限制拒絕請求時,nginx 也在 error 等級記一行,那些是防護正常運作的紀錄 |
| sshd 的紀錄 | 失敗的登入與它的來源位址 | SSH 只放行特定來源時,正常值是零,或只有來自允許來源的失敗(管理者打錯密碼、金鑰沒載入;經由雲端的 bastion 連進來時,sshd 看到的來源是 bastion 在虛擬網路裡的私有 IP);出現允許來源以外的失敗登入,代表來源限制的那一層已經不成立 |
| 強制存取控制的拒絕紀錄 | 被拒絕的程序與動作 | 正常值是零;在設定變更或套件更新之後出現,代表新版本的行為不在既有的政策裡 |
| 自動更新的執行紀錄 | 有沒有執行、裝了哪些套件、有沒有失敗 | 沒有執行代表排程或服務本身停了;失敗多半是磁碟空間或套件庫連線;有執行而從來沒有裝到任何套件,要確認套件庫的設定(有些發行版的套件庫鎖在固定的版本,見〈會老化的軟體〉) |
| 應用程式自己的 log 與 systemd 的重啟計數 | 錯誤與重啟次數 | unit 設了 Restart= 時,程序當掉會被 systemd 立刻拉回來,服務從外面看起來一直在,當掉前的錯誤訊息混在大量正常輸出裡;重啟計數不會被沖掉,所以它上升代表程序在反覆當掉 |
各列的紀錄去哪裡讀:
- sshd:RHEL 系的 unit 是
sshd.service、Debian 系是ssh.service,名稱用systemctl list-units '*ssh*'查;unit 名稱寫錯時journalctl -u的輸出也是零,所以零要搭配確認 unit 名稱存在才算數。journal 不保留到重開機之後的主機,讀 rsyslog 寫的/var/log/secure(RHEL 系)或/var/log/auth.log(Debian 系,有裝 rsyslog 時才有這個檔案)。 - 強制存取控制:SELinux 用
ausearch -m AVC;Debian 系的 AppArmor 在核心 log 裡找apparmor="DENIED"。 - 自動更新:
journalctl -u dnf-automatic.service與dnf history;journal 不保留時讀/var/log/messages。Debian 系的 unattended-upgrades 寫在/var/log/unattended-upgrades/。 - 重啟計數:
systemctl show -p NRestarts <unit>。
sshd 與強制存取控制這兩列的正常值都是零,偏離零就可以直接寫成告警的條件;access log 的分布要跟前一次比較才看得出變化,所以每一次讀到的數字要存下來,下一次才有比較的基準。這些數字存在哪裡是另一個要決定的事:存在維護的紀錄裡、或讓檢查腳本把每次的計數寫到它自己的結果檔,跟描述「要檢查什麼」的清單分開放。
對外入口:主機上的接聽程式與每一層防火牆的放行範圍
一個入口要從外面連得進來,得同時滿足兩個條件:主機上有程式在接聽那個位址與連接埠,而且請求經過的每一層都放行它。常見的層有雲端的網路安全規則(Oracle Cloud 的 NSG 與 security list、AWS 的 security group,見 Security Group)、主機的防火牆(firewalld、ufw、nftables)、服務自己的限制(只綁 loopback、systemd 的 IPAddressDeny),以及反向代理的路由(只把某些路徑轉給後端)。每一層都可以被單獨修改,所以盤點時兩個方向都要量:
- 從主機裡面量接聽的程式:
ss -lntup列出每一個在接聽的 TCP(-t)與 UDP(-u)位址、連接埠與程序(-p,要 root 才看得到其他使用者的程序)。只寫-t的話,UDP 的接聽程式不在輸出裡。拿這份清單跟部署時預期的清單比,多出來的接聽程式,用ss印出的程序名找到執行檔路徑,再以rpm -qf <路徑>(Debian 系是dpkg -S <路徑>)查是哪個套件帶進來的。例如 Oracle Cloud 的 Oracle Linux 10.2 映像檔預設啟用rpcbind(RPC 服務的連接埠對應程式,TCP 與 UDP 111)與效能監測套件 PCP(4330、44321),都在所有介面上接聽;停用並遮蔽(systemctl disable --now加mask)之後,那台主機上這三個連接埠不再接聽。主機跑 Docker 時,ss印出的程序是docker-proxy,查到的套件是 Docker 本身,真正開這個入口的是某一個容器,要用docker ps的連接埠欄位對照;Docker 發佈的連接埠繞過 ufw 的規則(Docker 的文件寫明封包在 ufw 的規則套用之前就被轉送),在 firewalld 上也不受一般 zone 的規則約束(Docker 另建dockerzone 與從任何 zone 轉發進來的 policy;Docker 的文件只寫了這組設定,「一般 zone 的規則管不到」是從它推出、在連結那一篇實測過的),封包路徑與限制的寫法見 CDN 後面的源站主機防火牆:只放行 CDN 來源的位置與套用時機、Docker 發佈連接埠的封包路徑、出站 DNS 的限制與地理封鎖〈Docker 發佈的連接埠:封包路徑與限制的寫法〉。 - 從主機外面量連得到的入口:從另一台機器對公開 IP 逐一連線。只從主機裡面量看不到雲端那一層;只從外面量時,結果只代表量測那台機器的來源位址,管理入口只放行管理者 IP 時,從管理者的電腦量連得到是預期的,要換一個不在放行清單裡的來源才量得到別人看到的結果。UDP 沒有連線,
nc -u -z沒有收到拒絕就回報成功,量 UDP 要送一個那個服務會回應的請求(RPC 用rpcinfo -u <IP> 100000),看有沒有回應。
入口依用途分成四類,各自要收窄到的範圍不同:
- 業務入口(網站的 80、443,API 的連接埠):它是服務本身,要收窄的是來源與路徑。前面有 CDN 時,只放行 CDN 的位址段(下面這一節寫它的限制);服務只給私有網路用時(公司 VPN 內的 API),只放行那個網段;反向代理只轉發對外需要的路徑與方法。
- 沒有業務用途的接聽程式:它不服務任何使用者,卻讓那個程式的漏洞多一條從外面利用的路徑,只要某一層防火牆被放寬就會暴露;停用它不影響任何使用者,所以停用並移除。
- 管理入口(SSH、資料庫、管理後台):它必須存在,而每個能連到它的來源都是一個可以嘗試登入的對象;只放行管理者的來源位址,把能嘗試的對象縮到管理者自己,或改成不開入站連接埠的做法(雲端業者的跳板主機服務 bastion,見 Bastion Host(跳板機);從主機往外建立的 tunnel,見 Outbound Tunnel),代價是管理者 IP 改變時要更新規則或通道。管理後台跟業務共用同一個連接埠時(WordPress 的
/wp-admin、/wp-login.php跑在 443 上),443 要對所有訪客開(前面有 CDN 時是對 CDN 的位址段開),雲端防火牆分不出路徑,收窄只能放在反向代理:依路徑只放行管理者 IP,或把後台改成只經 SSH 通道連得到。前面有 CDN 時,反向代理看到的來源位址是 CDN 的位址,依 IP 放行之前要先用 nginx 的 realip 模組從CF-Connecting-IP取回訪客的位址,而且set_real_ip_from只列 CDN 的位址段,否則這個標頭可以由請求的人自己填(從 realip 的機制推論,沒有實測)。各種做法的取捨與救援入口見 伺服器管理通道的設計:網路可達性、通道授權、主機登入驗證三層的做法與選擇,以及救援入口。 - 只給同一台主機用的入口:應用程式內部的連接埠、查詢與管理用的 API,只有主機上的反向代理需要連它;綁在 loopback、反向代理只轉發對外需要的路徑與方法之後,外面的請求沒有路徑可以到達它。
主機前面有 CDN 時,只放行 CDN 的位址段,確認的是請求經過了那家 CDN,不確認經過的是自己的 CDN 帳號:任何人都可以在自己的 CDN 帳號把網域指到這台主機的 IP,請求同樣從 CDN 的位址段進來,而自己帳號裡的 WAF 與 bot 規則管不到這些請求。維護時讀 access log 的 Host 標頭看得到這類繞過;在連線層擋下它,要讓源站驗證只屬於自己帳號的憑證,或改成源站不開入站連接埠的 tunnel,兩種做法的機制與 Cloudflare 上的設定差異見 CDN 後面的源站主機防火牆:只放行 CDN 來源的位置與套用時機、Docker 發佈連接埠的封包路徑、出站 DNS 的限制與地理封鎖〈限制放在哪一層:雲端防火牆、主機防火牆與 CDN 端的驗證〉。
會老化的軟體:各條更新管道與更新後的重開機
安全更新的維護包含兩件獨立的事:把更新裝上去,以及讓更新後的程式碼實際開始執行。兩件事都要依軟體是從哪一條管道裝上去的來判斷,因為每一條管道由不同的機制更新,主人得知有修補的方式與能不能交給排程也各不相同:
- 發行版套件庫裡的套件(nginx、OpenSSH、核心、函式庫):修補由發行版發佈,套件管理器讀得到;可以交給排程自動套用,下面寫它的設定。
- 第三方套件庫的套件(nginx.org 的官方套件庫、docker-ce 這類):一樣由套件管理器更新,但要看那個套件庫有沒有發佈安全公告;只裝安全更新的設定依公告的類型挑套件,沒有發佈公告的套件庫,裡面的套件不會被挑到(從 dnf 依 updateinfo 公告篩選的機制推論,沒有實測)。dnf-automatic 的
upgrade_type對整台主機生效,沒有逐一套件庫的設定,要對單一套件庫套用全部的更新,就另寫一個 systemd timer 執行dnf repository-packages <套件庫> upgrade:它只升級那個套件庫的套件,相依的套件仍從全部套件庫取;dnf upgrade --repo=<套件庫>會停用其他套件庫,新版本多了其他套件庫的相依套件時解析不到。在 Oracle Linux 10.2 的容器(dnf 4.20)用一個自建的本機套件庫實測:主機裝著demo-1,套件庫裡的demo-2多了一個要從發行版套件庫取的相依套件jq;dnf upgrade --repo=localdemo以nothing provides jq needed by demo-2-1.noarch from localdemo失敗,dnf repository-packages localdemo upgrade只升級demo,jq與它的相依套件從ol10_baseos_latest一起裝上。相依套件都已經裝好時(nginx.org 套件庫的 nginx 從 1.30.2 升到 1.30.6),兩條指令的結果相同;Debian 系在 unattended-upgrades 的Origins-Pattern加上這個來源。 - 容器映像檔裡的軟體:主機的套件管理器管不到。修補要等上游的映像檔發新版,得知的方式是上游的發佈通知或映像檔的漏洞掃描工具;重新拉取、重新建立容器可以排程,代價是沒有人在場時換了版本,所以多半在維護時段做。
- 應用程式自己的更新機制(WordPress 的核心與外掛這類 CMS):由應用程式自己的更新功能處理,主機的排程碰不到。看應用程式有沒有自動更新的設定(WordPress 的外掛可以逐個開自動更新);沒開的,列進維護時段。
- 自己建置的程式(自己編譯的 Go、Rust 程式):依賴的函式庫或語言的標準函式庫有漏洞時,要重新建置、重新部署才修得到。得知的方式是對依賴跑語言的漏洞掃描工具(Go 的
govulncheck),掃描可以排程,重建與部署多半由人在維護時段做。govulncheck ./...只回報程式碼實際呼叫得到的漏洞,找到時 exit code 是 3,沒有時是 0(govulncheck 1.1.3 對一個依賴golang.org/x/textv0.3.5 的程式實測,升到 v0.21.0 之後變成 0),所以排程的 timer 可以直接依 exit code 決定要不要告警。 - 從上游下載的預先建置檔案(官方發佈頁下載的二進位檔、
curl | sh裝的工具):沒有任何套件管理器記得它裝過,要人去換;得知的方式是訂閱上游的發佈通知,並把它列進清單,否則它會一直停在安裝那天的版本。
容器映像檔的漏洞掃描排程時,要讀的是掃描工具的 exit code,而它的預設跟 govulncheck 不同。以 trivy 0.57.1(官方映像檔 aquasec/trivy:0.57.1)實測:
1trivy image --download-db-only # 只下載漏洞資料庫,獨立成一次執行
2trivy image --skip-db-update --exit-code 7 <映像檔> # 有漏洞回 7、沒有回 0、掃描失敗回 1
3trivy image --skip-db-update --exit-code 7 --exit-on-eol 5 <映像檔> # 映像檔的發行版已經 EOL 時回 5- exit code 的讀法:不帶
--exit-code時,找到漏洞照樣回 0(一個 2023 年 6 月的 Debian 12 映像檔掃出 371 個漏洞,exit 0),所以排程一定要帶這個參數。掃描本身失敗(映像檔名稱打錯、逾時、離線抓不到映像檔、資料庫下載失敗)一律回 1,不受--exit-code影響;--exit-code設成 1 時,「找到漏洞」與「沒有掃成」都是 1,timer 分不開,所以設成 1 以外的值;上面的 7 與 5 是這次挑的值,跟排程裡其他會用到的 exit code 不重複就可以。--severity HIGH,CRITICAL與--ignore-unfixed只縮小計數,篩完還有漏洞時一樣回那個值。 - 已經 EOL 的映像檔:發行版過了支援期限時,資料庫裡不再有它的新漏洞,trivy 掃出 0 個、回 0,只在 stderr 印兩行 WARN:
This OS version is no longer supported by the distribution與The vulnerability detection may be insufficient because security updates are not provided(alpine 3.15 實測)。exit 0 在這裡看起來跟乾淨一樣,帶--exit-on-eol <值>才變成一個看得到的 exit code。 - 資料庫:壓縮檔約 130 MiB,解開後的
trivy.db約 1.6 GB(1,637,429,248 位元組),放在快取目錄(預設/root/.cache/trivy)的db/。--timeout(--help印出的預設是5m0s)的倒數包含下載資料庫的時間:快取是空的時,設 30 秒的那一次照樣下載了約 2 分鐘,跑完才以context deadline exceeded失敗,下載的內容沒有留下。資料庫已在本機時,一次掃描的時間主要花在從 registry 拉映像檔的 layer 與分析(一個 Debian 映像檔約 40 到 50 秒,layer 已在快取時幾秒)。所以下載資料庫與掃描分成兩次執行,各自有自己的逾時。 - 資料庫的新舊與更新失敗:0.57.1 帶
--skip-db-update、用過期的資料庫掃描時(快取的metadata.json把UpdatedAt改成一年多前)不印任何警告,結果照常輸出,資料庫的新舊要由排程自己讀metadata.json的UpdatedAt。分成兩次執行之後,掃描仍然會被更新的失敗拖累:離線時--download-db-only失敗,trivy 刪掉metadata.json(trivy.db留著);連網而逾時失敗,整個db/目錄都被刪掉。兩種情形之後帶--skip-db-update的掃描都以The first run cannot skip downloading DB失敗、回 1,跟其他掃描失敗分不開。所以更新那一次的 exit 1 本身要告警;在更新之前備份metadata.json、失敗後放回去,離線那一種掃描就恢復正常(實測),逾時那一種要整個db/一起備份。另一種寫法是把更新下載到另一個快取目錄(--cache-dir),成功之後才換掉掃描用的那一份,這個寫法沒有實跑。 - 掃本機才有的映像檔:trivy 跑在容器裡時讀不到主機的映像檔,有兩種給法:把主機的
/var/run/docker.sock掛進去,或在主機上docker save -o <檔名>.tar <映像檔>再用trivy image --input讀那個檔。兩種掃出的結果相同;掛 docker.sock 等於把主機 Docker daemon 的完整控制權交給這個容器(能啟動特權容器、掛主機的根目錄),docker save那一種容器只拿到一個以:ro掛進去的檔案,代價是每次掃描前要重新匯出一次。
發行版套件庫這一條可以交給排程。Oracle Linux 與 RHEL 系由 dnf-automatic.timer 定期觸發 dnf-automatic,它讀 /etc/dnf/automatic.conf 的兩個鍵:upgrade_type 決定裝哪一類更新(security 只裝安全更新),apply_updates 決定下載之後要不要安裝(預設 no 只下載)。timer 沒有啟用時,設定檔改了也不會執行;systemctl list-timers dnf-automatic.timer 列得出下一次執行的時間,才代表排程在跑。
Debian 系用 unattended-upgrades,排程由兩個 timer 分工:apt-daily.timer(每天 6 點與 18 點,加最多 12 小時的隨機延遲)更新套件清單,apt-daily-upgrade.timer(每天 6 點,加最多 60 分鐘)執行安裝;要不要安裝由 /etc/apt/apt.conf.d/20auto-upgrades 的 APT::Periodic::Unattended-Upgrade 決定,非互動安裝這個套件時,它的值預設是 "1"(套件的 debconf 問題 enable_auto_updates 預設 true)。裝哪些來源的更新寫在 /etc/apt/apt.conf.d/50unattended-upgrades,兩家的預設不同:Debian 12 與 13 的 Origins-Pattern 除了 label=Debian-Security 的兩筆,還有 label=Debian 那一筆,也就是穩定版的點版本更新(point release)同樣自動套用;Ubuntu 24.04 的 Allowed-Origins 是發行時的版本、-security 與兩個 ESM 的安全來源,不含 -updates,自動套用的只有安全更新。以上是三個發行版的容器裡裝上 unattended-upgrades 2.9 到 2.12 之後讀出的值;Oracle Cloud 的 Ubuntu 24.04 映像檔預先裝了 unattended-upgrades 與 needrestart,20auto-upgrades 的兩個值與 Allowed-Origins 的來源跟容器裡讀出的相同,兩個 timer 都是 enabled。
雲端映像檔是某一天建的,主機第一次開機時就已經落後:上面那個映像檔開機之後有 60 個來自 -security 的更新待裝,其中包括核心。手動觸發一次 unattended-upgrade(apt-daily-upgrade.timer 啟動的 apt.systemd.daily install 呼叫的也是它)花了 402 秒,升級了 64 個套件,其中包括新核心;裝完之後還有 18 個可以更新的套件,全部來自 -updates,照預設沒有裝。所以新建的主機在第一次排程執行之前,要先手動跑一次更新並重開機,否則從開機到排程第一次執行之間,映像檔建立之後公開的漏洞都還在。排程裝得到更新的前提是主機連得到套件來源:同一台主機放在只接了 Oracle 服務網路出口(service gateway)的私有子網路時,apt-get update 以 connect (101: Network is unreachable) 與連線逾時失敗;這個子網路加上 NAT gateway、主機放行出站的 80 與 443 之後,同一條指令從 ports.ubuntu.com 取得了套件清單,所以 Ubuntu 的套件來源在 Oracle 的服務網路之外。排程在這種主機上照常觸發而裝不到任何套件(從 apt-get update 的失敗推論,沒有等 timer 觸發實測),讀它的執行紀錄才看得出來。錯誤原文、成因與加出站的指令在 Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄:套件來源的出站(NAT gateway、路由與 NSG)、序列主控台的連線,以及被回收之後的重建〈只接 service gateway 的私有子網路:apt 連到套件來源時的錯誤原文與成因〉與〈加出站:NAT gateway、路由規則與只掛在這台主機上的 NSG〉。
這裡有兩個獨立的選擇:
- 要不要讓排程自動套用:自動套用時,一個漏洞從修補發佈到這台主機套用,最多隔一個排程的間隔;不自動套用時,這段時間等於兩次人工更新之間的間隔,那段時間裡公開的漏洞在這台主機上都還在。
- 只裝安全更新,還是連一般更新一起裝:安全更新多半是把修補移植回目前的版本,變更的範圍小,改變服務行為的機會比一般更新小,適合在沒有人看著的時候自動套用。只裝安全更新時,一般更新會長期落後,之後整批升級時一次跨過的版本差距大,那一次要安排在有人在場的維護時段做。
套件庫的設定決定自動更新拿得到什麼:有些發行版的套件庫鎖在固定的版本,自動更新只會在那個版本裡更新,拿不到之後發佈的修補。Amazon Linux 2023 預設把套件庫鎖在建立那個 AMI 時的版本,要用 dnf upgrade --releasever=<版本> 才換到新版本的套件庫(AWS 文件〈Deterministic upgrades through versioned repositories on AL2023〉,沒有實測)。發行版本身過了支援期限(EOL)之後,套件庫不再發佈安全更新,排程照常執行而什麼都裝不到;EOL 的判斷與升級的做法見 OS 與基礎軟體更換〈EOL 風險評估〉。確認自動更新實際裝了套件,讀它的執行紀錄(〈定期讀的 log〉表格的自動更新那一列)。
裝上去的更新要等使用它的程序重新啟動才生效:已經在執行的程序把舊版的函式庫留在記憶體裡,核心更新要重開機才換成新核心。needs-restarting(Oracle Linux 10.2 由 yum-utils 套件提供)回報兩件事:-r 回報需不需要重開機,-s 列出用到了已更新函式庫、重啟就能生效的服務。只有函式庫更新時,重啟 -s 列出的服務就夠,不必重開機。核心更新要重開機時,可以交給 dnf-automatic 自己(automatic.conf 的 reboot 鍵:never、when-changed、when-needed),或另寫一個在維護時段執行的 systemd timer,依 needs-restarting -r 的結果決定要不要重開。
Debian 系把這兩件事分給兩個機制:
- 需不需要重開機:看
/var/run/reboot-required存不存在,需要重開機的套件名寫在/var/run/reboot-required.pkgs。這個檔案由核心套件安裝後執行的/etc/kernel/postinst.d/底下的腳本建立,在 Debian 12 與 13 上,那支腳本屬於 unattended-upgrades 套件,沒裝這個套件的主機,核心更新之後這個檔案不會出現,它不存在不代表不需要重開;Ubuntu 24.04 的/etc/kernel/postinst.d/底下另有一支update-notifier,dpkg -S查得到它與登入訊息(motd)的/etc/update-motd.d/98-reboot-required都屬於 update-notifier-common。在 Oracle Cloud 的 Ubuntu 24.04 主機上,unattended-upgrade裝完新核心之後,/var/run/reboot-required的內容是*** System restart required ***,.pkgs列出新核心的套件與linux-base,重開機之後兩個檔案都不在了;兩支腳本都裝著,所以這次分不出是哪一支建的,登入訊息也沒有實際看過。 - 哪些服務要重啟:needrestart 套件在每次 apt 安裝之後(
/etc/apt/apt.conf.d/99needrestart的 hook)檢查用到舊函式庫的服務;needrestart -b用批次格式印出結果,NEEDRESTART-SVC每行一個要重啟的服務,NEEDRESTART-KSTA是核心的狀態(依 needrestart 的說明:1沒有待生效的核心,2與3有新核心等重開機,0是判斷不出來)。容器裡跑needrestart -b時核心屬於宿主,KSTA 印出0,其他值要在主機上才看得到。在 Oracle Cloud 的 Ubuntu 24.04 主機上量了三個時間點:開機時1;unattended-upgrade裝了下一個主要版本的核心之後是3,NEEDRESTART-KCUR是執行中的舊核心、NEEDRESTART-KEXP是新裝的核心,同一次另外印出dbus.service、systemd-logind.service與unattended-upgrades.service三行NEEDRESTART-SVC;重開機之後回到1,KCUR 與 KEXP 相同。2(新核心與執行中的核心 ABI 相容)這次沒有遇到。
unattended-upgrades 自己重開機的設定是 50unattended-upgrades 裡的 Unattended-Upgrade::Automatic-Reboot,預設 "false";改成 "true" 時,安裝完成而 reboot-required 存在,它就在 Automatic-Reboot-Time 指定的時間重開;這個鍵的預設是 "now",也就是安裝一完成就立刻重開,設定檔裡被註解掉的 "02:00" 是範例值,要固定在維護時段重開就得自己寫上時間(這是設定檔註解寫的行為,容器裡沒有實際重開機)。
是否讓它自動重開,取決於兩個條件:重開機之後服務會不會自己回來,以及服務沒回來時有沒有人知道。
- 服務會不會自己回來:看服務的 unit 是否 enabled(
systemctl is-enabled <unit>)、容器的 restart policy、主機防火牆的規則是不是寫進了永久設定;最可靠的確認是在有人在場的維護時段手動重開一次。例如服務當初是在終端機裡手動啟動、一直沒有寫成 systemd unit,平常主機一直開著所以看不出來,排程在凌晨重開機之後服務就不在了。服務不會自己回來時,先把它改成會自己回來(enable unit、設 restart policy),再看下一個條件。這個條件跟更新無關也成立:主機因為停電或業者的硬體維護重開時,結果相同。 - 服務沒回來時有沒有人知道:要有一個跑在這台主機以外的觀察者(另一台主機上的外部探測、或心跳監看:主機定期送訊號、停送時由外部服務告警),以及告警實際送得到人;跑在同一台主機上的監控,重開機時跟著停掉,報不出這台主機沒回來。外部探測的位置與判讀見 4.28 單台主機的監控盲區與失效域以外的觀察者:主機停機、使用者路徑中斷與告警鏈中斷,心跳監看與外部探測各自回答的問題與位置,以及與內部探測並排的判讀〈頻外訊號:心跳監看與外部探測〉。
兩個條件都成立時,讓它在固定時段自動重開,代價是每次一段可預期的停機,服務沒回來時探測會叫人;新核心開不了機時,修復要從雲端業者的序列主控台進去,停機的長度就不再可預期,自動重開機的時段要選在救援得到的時候。序列主控台要在出事之前準備好三件事:帳號或主機層級已經啟用(部分業者要事先開啟)、有一個能在主控台登入的方式(雲端映像檔的使用者通常只有金鑰、沒有密碼)、開機選單(GRUB)選得回上一版核心;這幾件沒準備好時,救援只剩業者的救援開機,或把磁碟掛到另一台主機上修。三家業者文件寫的預設如下,Oracle Cloud 與 Google Cloud 實際連過,AWS 只取自文件:
- AWS:帳號層級預設不允許使用序列主控台,要在每個區域分別開啟,使用者另外要有
ec2-instance-connect:SendSerialConsoleSSHPublicKey的 IAM 權限;連得上之後要登入作業系統,主機上得有一個設了密碼的使用者,而 AWS 提供的 AMI 預設沒有,要事先在每台主機用passwd設好(AWS 文件〈Configure access to the EC2 Serial Console〉)。 - Google Cloud:互動式的序列埠存取預設關閉,在專案或主機的中繼資料設
serial-port-enable為TRUE才開;連線用 SSH 金鑰驗證,出現登入提示時要本機使用者的密碼,Google 提供的 Linux 映像檔預設不允許密碼登入,要事先用sudo passwd或開機腳本設好(Google Cloud 文件〈Troubleshooting using the serial console〉)。在 Debian 12 的 VM 上先建一個設了密碼的救援帳號、再開啟序列埠登入的完整步驟與實測,在 GCP VM 的 SSH 與 RDP 改走 IAP 的實測紀錄:盤點、救援入口、IAP 規則與 IAM、停用舊規則驗證、OS Login 啟用與移除外部 IP〈準備救援入口:Serial Console 與本機救援帳號〉。 - Oracle Cloud:要先替主機建立一個序列主控台連線(instance console connection),上傳一把 RSA 公鑰,經由 SSH 連進去;只看開機輸出不必登入,要登入作業系統時,Oracle Cloud 不替
opc使用者設預設密碼,要事先替它或另一個使用者設好(Oracle Cloud 文件〈Instance Console Connections〉)。在一台 Ubuntu 24.04 的 A1 主機上實際連過:oci compute instance-console-connection create帶 RSA 公鑰建立連線,CLI 給的連線字串是一條經由 443 埠跳板的 ssh 指令;連上之後讓主機重開機,看得到 UEFI 韌體的輸出、核心開機訊息,開機後約 27 秒在ttyAMA0印出登入提示,所以只看開機過程(核心卡在哪一步)時不必登入。這台主機的ubuntu使用者,passwd -S ubuntu印出的狀態是L(密碼鎖定),這次也沒有替任何使用者設密碼,所以登入那一步沒有做。金鑰、建立連線的指令與 ProxyJump 的設定檔寫在 Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄:套件來源的出站(NAT gateway、路由與 NSG)、序列主控台的連線,以及被回收之後的重建〈序列主控台:RSA 金鑰、建立連線、ProxyJump 設定與看到的開機輸出〉。
三家的共同點是登入作業系統要密碼,而雲端映像檔的使用者預設只有金鑰,所以「能登入」這一件要在主機正常時先做;設了密碼的使用者也讓經由 SSH 以外的登入多了一條路,密碼的強度與只在主控台使用的限制(例如 sshd 設成不接受密碼登入)要一起決定。
觀察者還沒有時,先加上它,外部探測與心跳監看都有免費的服務。暫時加不了時,不自動重開機,主機處在「更新已裝、但核心的修補還沒生效」的狀態,直到有人安排重開為止;這段時間要被看見,所以把 needs-restarting -r 的結果寫進人會讀的位置:登入時顯示的訊息(motd),或維護時段讀的檢查報告(例如清單裡的一項 WARN),讓它在下一次登入或維護時被看到。
會到期與會改變的外部設定
這一類狀態的期限多半在主機外面,主機上的檢查量不到,要從持有那個期限的地方查:
- 憑證:公開憑證靠自動續期(ACME 協定,見 ACME Automation),要確認的是續期的排程實際成功,以及續期的時機依效期的比例計算,而不是寫死的天數。效期正在縮短:Let’s Encrypt 公告從 2027 年 2 月 10 日起預設簽發 64 天的憑證、2028 年再縮到 45 天,並建議把寫死「簽發後第 N 天續期」的排程改成在效期約三分之二時續期,或改用支援 ACME Renewal Info(ARI)的客戶端,由 CA 通知續期時間(Let’s Encrypt 的公告);cron、包裝腳本與操作手冊裡寫死的天數,在效期縮短之後會讓憑證在續期之前就過期。CDN 邊緣的憑證由 CDN 管。
- 源站給 CDN 用的自簽憑證:效期由產生它的人決定;CDN 的 SSL 模式只加密、不驗證源站憑證時(Cloudflare 的 Full),這張自簽憑證過期也不會讓連線失敗,所以它過期了也沒有人會發現。要改成驗證源站憑證的模式(Full (strict))時,先把這張自簽憑證換成 CDN 驗得過的憑證(公開 CA 簽發的,或 CDN 業者發給源站用的憑證),否則一切換所有請求都會失敗。
- 網域註冊:到期之後網域可能被別人註冊,所有指向它的紀錄跟著失效,而且收不回來;在註冊商那邊開自動續約,另設一條到期前的告警確認續約成功(付款方式過期時,自動續約會失敗)。
- 雲端 CLI 的登入:session token 這類短效期的登入過期之後,維護用的指令全部失敗,主機本身不受影響;它影響的是維護的人能否從開發機執行雲端 CLI 的指令。
- 管理者的 IP:雲端防火牆只放行管理者 IP 時,ISP 重新分配了那個 IP,管理者就連不進來;舊的 IP 被分配給別人時,放行規則變成放行了一個陌生人。換 IP 之後要同時改 SSH 與 bastion 的放行來源。
- CDN 的位址段:源站只放行 CDN 位址段時,CDN 新增的位址段不在放行清單裡,從那些位址回源的請求會連線失敗,訪客看到 CDN 的錯誤頁;CDN 業者公布位址段的清單(Cloudflare 的
ips-v4與ips-v6),定期拿它跟防火牆規則比對。取回清單、比對與套用時的陷阱見 CDN 後面的源站主機防火牆:只放行 CDN 來源的位置與套用時機、Docker 發佈連接埠的封包路徑、出站 DNS 的限制與地理封鎖〈只放行 CDN 位址的套用時機、範圍與清單的維護〉。 - 免費額度的閒置回收:免費方案的業者可能回收長期閒置的主機,Oracle Cloud 寫出了判定閒置的使用率門檻,規則的出處與三家業者的比較見 雲端免費額度與小型主機的收費比較:試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目〈出站流量與閒置回收〉。一台大部分時間沒有流量的實驗機會落在這個條件裡;前面有 CDN 的靜態網站也是,請求多半由 CDN 的快取回應,源站本身的使用率很低。被回收之後是停止還是刪除,官方沒有寫,在容量常常不足的機型上(Oracle Cloud 的 A1),被回收之後不一定建得回來,所以這一項歸在收不回來的那一類:主機上的資料要另外備份到主機以外,並確認照 repo 重建得出同樣的服務。回收在哪一刻發生沒有變化速度可算,備份多久做一次因此不由檢查間隔決定,而由能接受遺失多久的資料決定(RPO):資料庫用它自己的匯出工具(PostgreSQL 的
pg_dump、SQLite 的.backup)排程,備份送到主機以外,並定期還原一次確認可用。
這一類裡,到期時間可以預先查到的(憑證、網域、token),適合寫成到期前的自動告警,設計見 4.0.1 監控鏈事前偵測不到的異常:業務結果、資料正確性、到期事件、排程與背景工作、備份、安全、第三方依賴與成本的監看;改變時間無法預知的(管理者 IP、CDN 位址段、業者的回收),要靠定期比對與事先的備份。
檢查間隔與哪些項目改成自動處理的推算
每一類狀態的檢查間隔,從三個量推出來:警戒值(〈沒有人操作時主機上會改變的狀態〉定義的那個界線)設在哪裡、量測值從現在的值變化到警戒值要多久,以及到了真正出事的那一刻之後收不收得回來。
警戒值設在哪裡:從出事的那一刻往回,留出處理期間最壞會增長的量(處理要花的時間乘上最壞的增長速度;清 log 幾分鐘,擴磁碟或搬資料要三天),再加上量測的誤差,警戒值就設在那個位置。處理時間只在設警戒值的時候算一次,之後的間隔推算不再扣它。
到達警戒值的時間:量測值從現在的值變化到警戒值要花的時間,下文寫成 T。用兩次量測的差除以兩次之間的天數得到變化速度,再拿現在的值到警戒值的距離除以速度。有四種情形拿不到穩定的速度:
- 第一次盤點時只有一次量測:現值已經接近或超過警戒值的,當場處理;其餘的隔一小段時間(例如隔天)量第二次,拿到速度再算。
- 有輪替的資料(log):用量是鋸齒形,兩次量測之間剛好輪替過時差是零或負數。改成比較相鄰兩個輪替週期裡的最高值,差除以週期的長度得到速度;保留份數固定的 log,保留滿之後總量停在份數乘上每份的大小,之後的增長只來自流量變多。
- 增長由外部請求決定(上面的寫入端點):兩次量測之間的速度不代表下一段時間的速度,改用最壞情況的速度。
- 改變是步階變化:量測值在某一刻一次就從一個值變成另一個值、之後停在新值,中間沒有過渡值(Step Change(步階變化)),相對的是上面幾種逐步累積的變化。例如有人在主控台把 SSH 的放行來源從管理者 IP 改成
0.0.0.0/0,規則在送出的那一刻就從只放行管理者變成對所有人開放;新套件帶進一個在所有介面上接聽的服務、CDN 新增一個位址段、管理者 IP 被 ISP 重新分配,也都是這一種。兩次量測之間看不到任何接近警戒值的過程,到達警戒值的時間是零,下面的公式算不出間隔,處理方式另列在分類裡。同一個量也可能兩種形態都有:磁碟用量平常逐步累積,有人一次複製進一個大檔案時就是步階變化;這一次的增加由下面收不回來那一類的剩餘空間告警發現,不靠檢查間隔。
檢查間隔:間隔為 I 時,漏掉一次檢查,下一次量測落在 2I,要在量測值超過警戒值之前量到,I 要小於 T 的一半。本篇取 T 的三分之一:漏掉一次檢查時,下一次量測落在 T 的三分之二,離到達警戒值還剩三分之一的 T,留給量測的波動與沒預料到的突增;這個比例是作者訂的安全係數,量測的波動大時取得更短。例如剩下 15 GB、log 與資料每天增加 50 MB、擴磁碟要三天:三天的增長只有 150 MB,警戒值取在剩 3 GB 的位置,比三天的增長量大得多,多出來的部分給突增;到達警戒值約 240 天,間隔可以到 80 天,每月量一次就在範圍內。這個間隔管的是這些資料的增長速度;檔案系統完全寫滿屬於收不回來的那一類,剩餘空間另外照下面的分類設一條告警。同一台主機的寫入端點在最壞情況下約 3 小時就寫滿,沒有任何人的檢查間隔來得及。
維護節奏通常是先決定好的(每週或每月一次的維護時段),而不是每一項各自算出來的。每一項算出來的間隔跟這個節奏比:算出的間隔不短於維護節奏的,併進同一次維護;算出的間隔比維護節奏短的,有兩條路:另排一個比節奏短的排程檢查(例如每週執行一次只讀檢查,結果送到下一段說的接收端),或交給機制自動處理。到達警戒值的時間短於任何排程間隔時(寫入端點在最壞情況下約 3 小時就寫滿,到達警戒值還更早),只剩機制這一條路。
這幾條都要有人收到結果,而「收到」包含兩件事,各自的條件不同:
- 主機還在運作時才會發生的狀態(檔案系統的剩餘空間、寫成告警的 log 計數,例如 sshd 的失敗登入、憑證效期;主機開著而某個服務沒有起來也屬於這一種):告警可以由主機上的程式發出(例如同一台主機上的 Prometheus 與 Alertmanager),條件是送達的管道在主機以外(推播服務、郵件)。
- 主機整台停止、或重開機之後主機本身沒有回來(連不上、作業系統沒有開起來):主機上的程式跟著停掉,要由跑在主機以外的觀察者發現:外部探測,或心跳監看(主機上的程式定期送訊號,例如 Prometheus 一條一直處在觸發狀態的告警,外部服務在訊號停止時叫人,見 Dead Man’s Switch)。
兩者都沒有時,告警等於沒有寫,這些項目的實際間隔就是人登入主機的間隔;建一個最小的接收端(送達管道在主機以外的推播通知,加上外部的心跳或探測服務)是比這一節任何一項都先做的事。由這幾個量,每一項的處理方式如下:
- 越過之後收不回來(安全漏洞被利用、檔案系統寫滿期間所有寫入者沒寫進去的紀錄、被回收的免費主機、到期被別人註冊的網域):已經發生的入侵與已經遺失的紀錄沒有辦法收回,所以不等檢查間隔,優先自動化:安全更新依〈會老化的軟體〉的各條管道處理,能排程的交給排程,不能排程的列進維護時段並寫明多久查一次,需要重開機才生效的部分照那一節的條件;檔案系統的剩餘空間是全部寫入者共用的資源,一律寫成告警;網域開自動續約;會被回收的主機,資料依 RPO 定期備份到主機以外。
- 改變是步階變化:比對的間隔不能超過維護的人能接受改變後的狀態持續的最長時間,不由維護節奏決定:每天比對一次,一條被放寬的規則最多開著一天才被發現。這個長度依暴露之後被利用的代價訂,直接把管理入口或資料庫開到外面的,比對要比只會讓部分回源失敗的更密,能在變更發生的當下收到事件(雲端的稽核紀錄、設定變更的通知)就更好。會造成對外暴露的(防火牆規則被放寬、新的接聽程式)與會讓回源失敗的(CDN 新增的位址段),交給定期比對的排程:主機上的接聽清單與暫停前或部署時的清單比、從另一台機器量公開 IP 上連得到的入口、拿 CDN 公布的位址段跟放行規則比,結果送到接收端。管理者 IP 被重新分配時,管理者下一次連線就發現自己連不進去,而舊的 IP 可能已經分給別人、放行規則對陌生人開著 SSH 的連線(登入仍要金鑰),所以發現的那一次就同時把 SSH 與 bastion 的放行來源改成新的 IP;別處有沒有寫死舊 IP(監控的放行來源、維護文件),這一項的確認併進維護時段。
- 到達警戒值的時間比維護節奏短(對外寫入端點造成的資料增長):維護時段來不及在量測值超過警戒值之前量到,超過的那一刻沒有人在場,所以這一類交給機制自動處理(應用程式的保留期限與配額、容量隔離、全域的速率限制),或另排比節奏短的排程檢查;短於任何排程間隔時只剩機制。維護時段量的是那個機制還在不在運作。
- 期限可以預先查到(憑證、token):到期日是已知的,等人想起來查才發現就太晚,寫成到期前的告警,維護時段確認告警還設著。
- 算出的間隔不短於維護節奏,而且越過之後收得回來(套件快取、暫存檔、有輪替機制的 log 這些個別的寫入者):個別的寫入者超過自己的警戒值,只是佔掉較多的空間,清掉就回到警戒值以內;收不回來的是整個檔案系統寫滿,那由全部寫入者一起造成,已經由收不回來那一類的剩餘空間告警處理。所以這一類在維護時段量測與清理,量的目的是找出是誰在長、長多快,交給人判讀的好處是看得到成長的趨勢與原因。
同一個項目同時符合幾種時,照上面的順序取第一個符合的:收不回來優先於步階變化,步階變化優先於到達警戒值比維護節奏快,到達警戒值比維護節奏快優先於期限可預知,期限可預知優先於留給維護時段。哪些項目適合寫成告警、哪些留給人定期讀,在有人值班、每天接班檢查的團隊裡另有一套判斷,見 4.29 值班的例行檢查:接班檢查與每週營運會議讀的訊號、交班紀錄的內容,以及例行檢查項目改寫成告警規則的條件〈例行檢查項目改寫成告警規則的條件〉;本篇的節奏是一個人每週或每月的維護時段,所以留給人的項目只放變化慢的那些。
接手或第一次盤點一台已經上線的主機
接手別人留下的主機、或第一次替自己的主機做盤點時,處理的順序依〈檢查間隔與哪些項目改成自動處理的推算〉的分類,以及每一步要用到的輸入,排成下面的順序:告警的接收端排在最前面,因為後面每一項寫成告警或排程比對的處置,都要靠它把結果送到人手上;接著只讀不改地記下主機的原狀,因為下一步要依這份清單判斷哪些接聽程式該停,而改動之前記下的才是前任留下的原狀;然後處理每多拖一天就多一天收不回的暴露或紀錄;最後量出之後計算變化速度要用的基準值。
- 先建告警的接收端:送達管道在主機以外的推播通知,加上外部的探測或心跳服務。
- 只讀地記下原狀:從主機裡面列出接聽程式與各層的放行範圍(
ss -lntup、主機防火牆的規則、雲端規則的匯出,跑 Docker 時加上docker ps的連接埠欄位),從另一台機器量公開 IP 上連得到的入口。沒有 repo 時,同一步把設定複製出來收進 repo:docker-compose.yml與它引用的.env、/etc/docker/daemon.json、正在跑的映像檔的 tag 或 digest(compose 寫latest時,重建會換成另一個版本)、反向代理的設定、systemd unit。之後停錯一個前任的服務時,這一份就是對照還原的依據,重建驗證與恢復時的比對也以它為基準。 - 處理收不回來的暴露與紀錄:依記下原狀時列出的接聽清單與從外面量到的入口(前任開著對外的資料庫或管理後台時,它正是每拖一天就多一天暴露的項目),停用沒有業務用途的接聽程式、把管理入口收窄到管理者;依〈會老化的軟體〉的各條更新管道,列出這台主機上的軟體各自由什麼更新,能排程的排程,不能排程的列進維護時段並寫明多久查一次;journal 改存到磁碟,之後的紀錄才留得下來;設檔案系統剩餘空間的告警;網域開自動續約;資料依 RPO 備份到主機以外。
- 量出基準值:處理完暴露之後,重新列一次接聽程式與各層的放行範圍,記成之後排程比對的基準;會累積的資料各自的大小與清理機制、各份 log 的計數,在接手當天量一次、隔一小段時間再量一次,作為之後計算變化速度與比較的起點。
清單裡每一項要寫出量測的指令與允許的範圍,主機維護清單寫成 TOML:JSON、YAML、TOML 的取捨、TOML 1.0 與 1.1 的版本差異、清單的欄位設計與只讀檢查腳本 用一台實際的主機示範這份清單的欄位設計與只讀的檢查腳本;主機要暫停一段時間時,這些狀態在暫停期間繼續改變,恢復上線時要查哪幾項,寫在 服務暫停與恢復上線:只停服務、停止主機、終止主機到全部刪除各自保留的資源,暫停期間仍計費與仍被指向的資源,恢復前要查的漂移與恢復的順序。