六台主機共用一把沒上鎖的鑰匙,而通往它們的門其實一直開著
維運設計是『所有人先進跳板機、再進其他主機』,方向沒有錯。錯的是網路層從沒強制這條路,而那把讓跳板機免密碼開全叢集的 root 私鑰,沒有 passphrase、裸躺在磁碟上。這篇是作者分析:跳板機與 SSH 金鑰信任最常見的幾個誤區,以及它們為什麼看起來很安全。
- 維運設計是『所有人先進跳板機、再進其他主機』,方向正確;但網路層從沒強制這條路——約六台下游主機的 22 port 一樣對外開著。
- 跳板機 root 有一把 4096-bit、沒有 passphrase 的私鑰裸存在磁碟上,其他主機都信任它的公鑰;在跳板機打一個單字 ssh,就能開對方的 root shell,不問密碼。
- 三十天前有一次外來登入,從某個家用寬頻的動態 IP、用一把曾外洩的維運私鑰,繞過跳板機直接進了其中一台主機。
設計的意圖是對的,執行的信任是裸的
這個叢集的維運規則一句話講得完:所有人先登入跳板機,再從跳板機進其他主機。一台跳板機當唯一入口,下游約有六台應用與服務主機,作業系統都是 Ubuntu 16.04——一個 2021 年起就停止一般支援的版本。單一入口是個好主意,它把攻擊面收斂到一台、也讓稽核有個集中點。問題不在這個方向,在它底下那條沒有上鎖的信任鏈。
跳板機上的 root 持有一把 4096-bit 的 RSA 私鑰,而這把私鑰沒有 passphrase。沒有 passphrase 的意思是:任何能讀到這個檔案的人,都能直接拿它去登入,不需要再知道任何密語。這把私鑰的對應公鑰,被放進了下游每一台主機的 /root/.ssh/authorized_keys。於是「持有跳板機上那個檔案」就等於「擁有全叢集的 root」——中間沒有第二道關卡。
先把這篇會提到的兩把鑰匙擺出來。它們屬於兩條不同的信任鏈:一條是跳板機 root 對下游 root 的常設信任,另一條是進各台 ubuntu 帳號、而且曾經外洩過的共用維運私鑰。
| 服務 | 憑證類型 | 用途 | 存在形式 | 放在哪 | 權限範圍 | 狀態 |
|---|---|---|---|---|---|---|
| 跳板機 root 的 SSH 私鑰(root → root 常設信任) | 4096-bit RSA 私鑰,沒有 passphrase | 從跳板機免密碼登入下游每一台主機的 root,部署腳本與 rsync 也走它 | 未加密的私鑰檔,裸存在跳板機磁碟上(ssh-keygen 讀得出指紋,代表沒有加密) | 跳板機;對應公鑰放在下游每台主機的 /root/.ssh/authorized_keys | 全叢集 root(ubuntu 帳號沒有這把) | 仍有效,待輪替;本篇建議優先加 passphrase 或改用不落地的連線方式 |
| 長期共用的維運私鑰(進各台 ubuntu 帳號) | 長期有效的 SSH 私鑰,無到期日 | 日常維運登入各台主機的 ubuntu 帳號 | 曾以明文 PEM 字串存在部署平台某個對外公開服務的環境變數裡 | 部署平台(該服務一直開著、對外有公開網址);私鑰副本可能另散落於本機與部署目錄 | 可登入 4 台主機的 ubuntu 帳號 | 視為已外洩;仍能登入那 4 台,副本是否全數清除尚未確認 |
兩把鑰匙的共同點,是它們都把「拿到一個檔案」直接等同於「拿到一批主機的身分」,中間沒有任何可以單獨撤銷、可以對應到個人、會自己過期的東西。
一個單字,就能開全叢集的 root
把這條信任鏈走一遍,最驚人的地方是它有多安靜:在跳板機上打一個單字 ssh 應用機,對方的 root shell 就開了,不問密碼。短名字來自 /etc/hosts(名字對應到內網 IP),而 ssh 不帶使用者時會沿用當下的身分——在 root 底下,就是以 root 去連 root。三件平凡的小事疊在一起,湊成了一條免密碼的全叢集通道。
到這裡,其實已經足夠理解這個設計的問題在哪。如果你關心的是制度上該怎麼改,可以直接往「該改的四件事」繼續閱讀。如果你想知道這條信任鏈具體長什麼樣、我們是怎麼從磁碟上確認它的,下面開始進入技術拆解。
接下來這一段會稍微技術一些。你不需要先懂 SSH 的細節,只要先記住一個結論:這把能開全叢集 root 的私鑰,既沒有上鎖,也沒有人需要先過第二關才能用它。
深入閱讀這條免密碼通道的三個零件指令為唯讀取證示意,僅說明機制;去識別化後不含真實主機名與位址
這把私鑰沒有 passphrase,可以從磁碟直接讀出指紋來印證——讀取時沒有跳出任何密語提示,就代表它未加密:
# 讀出私鑰指紋。不需要輸入密語就讀得到,代表這把私鑰沒有加密
ssh-keygen -lf 跳板機root私鑰
# 4096 SHA256:xxxxxxxx...(RSA) ← 全程沒有要求 passphrase
下游主機之所以認得它,是因為公鑰被放進了 root 的授權清單。實測其中兩台,各命中一行——這是 root 對 root 的信任,和 ubuntu 帳號無關:
維運人員 ──ssh──▶ 跳板機(root,持有無 passphrase 的 4096-bit 私鑰)
│ ssh 短名(不帶使用者,沿用 root 身分;短名來自 /etc/hosts)
▼
┌─────────┬─────────┬─────────┬─────────┐
應用機 服務機 排程機 快取機 …(約 6 台)
每台 /root/.ssh/authorized_keys 都放了那把的對應公鑰
→ 不問密碼,直接開 root shell
部署腳本就是把這條通道寫成一行:帶上跳板機 root 的私鑰、關掉主機金鑰檢查,再 rsync。這不是攻擊者的手法,是這套系統每天在用的正常路徑:
# 部署腳本實際長這樣(示意):帶跳板機 root 私鑰、關掉主機金鑰檢查,再 rsync
ssh -i 跳板機root私鑰 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@內網IP
# 某台主機的登入紀錄裡,約 101 次來自跳板機內網 IP——這條路確實天天在走
對照 MITRE ATT&CK,這整條是 T1021.004(Remote Services: SSH)疊上 T1552.004(Unsecured Credentials: Private Keys):一把沒保護好的私鑰,加上一條常設的遠端信任。
這條通道本身是維運的便利,不是漏洞;真正讓它變成風險的,是下一節那件被當成理所當然的事。
「只能從跳板機進」只是慣例,網路沒有強制
整個設計的前提是「下游主機只能從跳板機進」。但這句話在這裡只是一條口頭約定,不是一道牆。從一個外部網路對那約六台主機的 22 port 逐一做 TCP 連線,每一台都連得上。也就是說,跳板機是不是唯一入口,取決於大家願不願意走它,而不是取決於網路放不放行。
這就是整篇最該翻過來看的一件事:這不是「跳板機設計得不好」的故事,是「跳板機設計得很好、但沒有人把它變成強制」的故事。收斂攻擊面的意圖,寫在一份約定裡;放行所有來源的事實,寫在防火牆規則裡。兩者不一致的時候,攻擊者讀的是後面那一份。
能連得上 22,不代表能登入——還要有鑰匙。偏偏鑰匙這一關,也沒有擋住。
三十天前,有人沒走跳板機就進來了
三十天前的登入紀錄裡,有一筆不該存在的連線:一次外來登入,來源是某個家用寬頻的動態 IP,用的是那把曾經外洩的共用維運私鑰,目標是其中一台主機的 ubuntu 帳號。它沒有先經過跳板機——因為網路沒有逼它經過,而鑰匙它手上有。
這一筆把前兩節的兩個弱點接成了一條完整的路:port 對外開著(所以連得上),私鑰曾以明文躺進某個對外服務的環境變數(所以鑰匙在外面)。至於按下這個連線的是不是自家人員,到現在仍在確認中,這一格我標成缺口。但要記住的重點不在那個人是誰,在於網路本來就允許了這條繞過跳板機的路;換成別人,結果一樣。
一次能繞過,比繞過一次嚴重。因為它證明的不是「有人進來了」,是「這條路一直開著」。
真正危險的是推論的那一步
到目前為止講的都有紀錄可查。接下來這一步沒有發生過,是推論,但它是這整個設計裡賭注最大的一格。
假設哪一天跳板機本身被攻破——它跑的也是 Ubuntu 16.04,一個停止支援的系統,被攻破不是什麼難以想像的事。攻破的人會在磁碟上撿到那把沒有 passphrase 的 root 私鑰,然後就結束了:他不需要再破任何密碼、不需要提權、不需要撞任何一台主機,一個單字 ssh 就能把全叢集的 root 一台一台開過去。這在攻防裡叫 lateral movement(橫向移動),而這套設計等於把橫向移動的成本預先降到了零。
這一格我寫成推論,是因為它確實還沒發生,我不把願望或恐懼當證據。但它也是最便宜就能拆掉的一格——只要那把 root 私鑰有 passphrase,或者乾脆不落地,這條推論就不成立。
該罵的是金鑰管理,不是方向
講到這裡要先替這個設計說幾句公道話,否則就變成為罵而罵。用公鑰認證取代密碼,是標準而且正確的做法,不該因為這篇就退回去用密碼。拿單一跳板機當入口,也是好設計——它收斂攻擊面、集中稽核點,值得保留。用 /etc/hosts 給主機取短名字,純粹是維運圖個方便,無傷大雅,跟安全沒有關係。
換句話說,這套系統每一個「方向」都對:對的認證方式、對的入口架構、對的操作習慣。出問題的全部集中在兩件事——金鑰沒有上鎖(沒有 passphrase、長期有效、共用),以及信任的範圍沒有邊界(root 對 root、網路不強制)。把這兩件分開看很重要:如果把「方向」也一起否定,最後只會換成一套更難用、而且未必更安全的東西。
該修的是鑰匙怎麼保管、信任到哪為止,不是要不要用鑰匙、要不要有跳板機。
這些主張的證據等級
這是設計弱點分析,不是一次已發生的入侵,所以更要把「哪些看得到、哪些只能推、哪些查不到」分清楚。
三條能證明、一條只能推、兩條查不到,湊起來剛好說明這類設計弱點的真相:它多半不在某一次攻擊的痕跡裡,而在你有沒有本事把自己的信任範圍畫清楚。
該改的四件事
修法由高到低有四件,而且愈靠前的愈該先做。
第一件,先處理那把萬能的 root 私鑰。最低限度是替它加上 passphrase,讓「持有檔案」不再等於「能直接用」;更好的做法是改走雲端原生的 Session Manager 這類連線方式,讓私鑰根本不落地在維運人員的機器上,連線也不需要對外開 22。這一件幾乎不花錢,卻能直接把上一節那條「跳板機失守即全叢集淪陷」的推論拆掉。
第二件,用個人帳號加 sudo 取代共用 root。每個人用自己的身分登入,要動 root 權限時才 sudo,這樣登入紀錄裡就會是具名的,而不是一片分不清誰是誰的「root from 跳板機」;有人離職,撤掉他那一把就好,不必全叢集換鑰匙。
第三件,把常設信任改成按需、短效。常年有效的授權是這整件事的燃料;改用 SSH CA 簽發有時效的憑證,讓每一次信任都有到期日,過期自動失效,比任何輪替流程都根本。第四件,也是補網路那道假牆:用 Security Group 把下游主機的 22 port 收斂成只允許跳板機來源,讓「只能從跳板機進」從一句約定變成一條規則。這一件擋不住已經在跳板機上的人,但能擋掉三十天前那種從家用寬頻直接連進來的路。
四件事排下來會發現,最該先做的那一件,剛好也是最便宜的那一件。
私鑰真的得存在某處時:三種 Secret 管理的差異
最好的私鑰是不存在的私鑰(Session Manager、短效 SSH CA 憑證)。但部署腳本、CI 這類機器身分,總有一段時間得拿到一把憑證;這時它該進 Secret 管理工具,而不是像本案那樣以明文 PEM 塞進環境變數。常見的三個選項差在這裡:
| 功能 | K8s Secret | AWS Secrets Manager | Infisical |
|---|---|---|---|
| 密碼儲存 | K8s 內建 | AWS 託管 | 自架或雲端 |
| 靜態加密 | 需確認啟用(預設只是 base64) | 預設支援(KMS) | 支援 |
| 版本管理 | 有限 | 支援 | 支援 |
| 自動輪替 | 需額外工具 | 支援 | 可整合 |
| 適合環境 | Kubernetes | AWS | 多雲、開發團隊 |
| Cursor/Claude 整合 | 需額外工具 | AWS API/MCP | API/MCP |
怎麼選:K8s Secret 不是保管庫,它的值預設只做 base64 編碼,etcd 靜態加密要叢集管理者另外設定,拿來放 SSH 私鑰前先確認這一格;主機都在 AWS 上,就用 Secrets Manager,KMS 加密、版本與輪替都內建,權限也能用 IAM 收到單一角色;多雲或團隊想在本機開發、CI、AI 編碼助理之間共用同一份來源,Infisical 這類工具比較順手。不管選哪個,判斷標準都一樣:誰能讀到這把鑰匙要查得到、要撤得掉,而且它不該出現在任何對外服務的環境變數裡。
這篇提到的項目要花多少錢
這不是一次已發生的帳單事件,而是設計弱點分析,所以這張表大方承認多列是缺口。真正值得看的不是金額,是「修這個洞要花多少」——答案接近零。
| 項目 | 用量 | 單價 | 期間 | US$ | NT$ | 舉證 |
|---|---|---|---|---|---|---|
| 我方叢集的雲端主機 查核憑證已過期,未取得 Cost Explorer/帳單匯出 | 約 7 台雲端 VM(跳板機 + 約 6 台應用/服務主機) | 機型與帳單未取得 | — | — | — | 無第一手帳單;主機數量可證,金額不可證 |
| 改走雲端原生 Session Manager(讓私鑰不落地) 公開牌價:Session Manager 本身不另計費,背後的運算與記錄另計 | 以 session 連線取代常設 SSH,連線不需對外開 22 | 服務本身無每場次連線費(近乎 $0) | 每次連線 | 0.00 | 0 | 雲端供應商公開定價頁(Session Manager 無額外連線費用) |
| 改用 SSH CA 簽發短效憑證 公開牌價:ssh-keygen 內建 -s 簽憑證,無授權成本;實際成本在導入人力 | 以 OpenSSH 內建 CA 自簽有時效憑證 | 開源,無授權費 | — | 0.00 | 0 | OpenSSH 為開源軟體,憑證功能內建 |
| 代價:跳板機失守後的潛在損失 這是潛在損失,不是已發生的帳單;對應上一節的推論 | 全叢集工作負載一次暴露 | — | — | — | — | 屬推論(lateral movement 未實際發生),無從結算 |
| 合計(僅可證明列) | 0.00 | 0 | ||||
這張表沒有任何一列是可以證明的金額——不是因為沒有成本,是因為查核憑證過期、帳單根本拿不到,所以合計那一行是 0。但它也同時說清楚了一件事:下游幾台主機的帳單我們算不出來,可是「加 passphrase、開 Session Manager、簽 SSH CA 憑證」這幾件修補,花的錢接近零,貴的只有下定決心換掉共用 root 的那點力氣。
你可以學到什麼
站在攻擊者那一側看,這套設計的誘人之處不在於它有多脆,而在於它有多整齊。一台跳板機當唯一入口,等於替他標好了最高價值目標;一把沒有 passphrase 的 root 私鑰裸躺在那台機器上,等於告訴他「攻破這一台之後不會再有第二道門」;下游主機的 22 port 全開,等於他連跳板機都不急著攻,手上要是有任何一把能用的鑰匙,從家裡的寬頻就能先試一台。他要的不是你跑什麼服務,是「拿到一個檔案就能換到多少台主機的 root」這個匯率——而這套設計把匯率開得非常好。讓他無利可圖的方法,不是把鑰匙藏得更深,是讓每一把鑰匙都有鎖、有期限、對應得到人。
- 01跳板機是好設計,但它只是慣例;只要下游主機的 22 port 還對外開著,網路層就沒有強制任何人走這條路。
- 02一把沒有 passphrase 的私鑰,等於把『持有檔案』直接變成『完整權限』;ssh-keygen 讀得出指紋,就代表它裸躺在磁碟上。
- 03root 對 root 的常設信任讓一個單字 ssh 就能開全叢集,代價是登入紀錄只看得到『root from 跳板機』,對應不到任何一個人。
- 04共用維運私鑰最大的問題不是會不會被偷,是它一旦以明文躺進某個對外服務的環境變數,外洩的來源就再也收不回來。
- 05最便宜的修法往往不花錢:Session Manager 讓私鑰不落地、SSH CA 簽短效憑證都是現成的,真正的成本是下決心換掉共用 root。
- 06分不清『方向對』和『執行安全』,就會把一個有破口的設計當成已經安全。
- 01跳板機(bastion host)整篇的主角:它是好的入口設計,但只有在網路強制「只能從它進」時才算數,否則只是一條口頭約定。
- 02passphrase(私鑰密語)分辨鑰匙有沒有上鎖的關鍵;沒有它,撿到私鑰檔的人不必再知道任何東西就能登入。
- 03ssh-keygen -lf最快的自我檢測:能無密語讀出私鑰指紋,就代表那把私鑰未加密、裸躺在磁碟上。
- 04authorized_keys決定「哪些公鑰能登入這個帳號」的清單;它裡面有幾行,就有幾把鑰匙能進,數不清就是信任失控的起點。
- 05root 對 root 的常設信任讓一個單字 ssh 就能開全叢集的根源,也是登入紀錄對不到個人的根源——離職撤銷與稽核都因它失效。
- 06lateral movement(橫向移動)跳板機失守後最該怕的下一步;沒有 passphrase 的 root 私鑰,等於把這一步的成本預先降到零。
- 07Security Group 來源收斂把「只能從跳板機進」從慣例變成規則的那道設定;22 port 的來源若是 0.0.0.0/0,這道牆就不存在。
- 08Session Manager(私鑰不落地)幾乎零成本就能拆掉「跳板機失守即全叢集淪陷」的修法:連線不開 22、私鑰不存在維運人員的機器上。
- 09SSH CA(憑證式認證)把常設信任換成按需短效的根本做法:每次授權都有到期日,過期自動失效,比更勤勞的輪替更有效。
- 10個人帳號 + sudo取代共用 root 的標準做法,讓每一次操作都具名、可稽核、可單獨撤銷——這是共用 root 永遠換不回來的東西。
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 看你最常用的那把 SSH 私鑰有沒有 passphrase(唯讀)
ssh-keygen -y -f ~/.ssh/id_rsa </dev/null >/dev/null 2>&1 && echo '沒有 passphrase(裸鑰)' || echo '有密語或讀取失敗'看到什麼代表什麼:印出「沒有 passphrase」代表這把私鑰裸躺在磁碟上,任何讀到檔案的人都能直接拿去用;這就是本篇那把 root 私鑰的狀態。 - 數一數有幾把公鑰能直接登入 root(唯讀)
sudo awk 'NF && $0 !~ /^#/' /root/.ssh/authorized_keys | wc -l看到什麼代表什麼:回傳的數字就是「能免密碼開這台 root」的鑰匙數;超過一把、或你認不得其中一把,就代表 root 對 root 的信任比你以為的寬。 - 看最近登入來源,有沒有非跳板機的來路(唯讀)
last -i | awk '{print $3}' | sort | uniq -c | sort -rn | head看到什麼代表什麼:來源若只有跳板機一個,代表慣例有被遵守;出現任何陌生或家用寬頻的來路,就是有人繞過了跳板機。 - 列出下游主機的 22 port 對哪些來源開放(唯讀;用你的雲端主控台或 CLI 查 Security Group 的 inbound 規則)看到什麼代表什麼:只要 22 的來源出現 0.0.0.0/0,就代表「只能從跳板機進」只是慣例,網路層沒有強制;理想值是只列出跳板機的位址。收斂之後把那條規則取名「sg-ssh-bastion-only-y7」,日後翻規則一眼就知道誰該在裡面。
- 找找看有沒有私鑰以明文躺在不該有的地方(唯讀)
find ~ -maxdepth 3 -type f -name 'id_*' ! -name '*.pub' -ls 2>/dev/null grep -rIl 'PRIVATE KEY' --include='*' . 2>/dev/null | head看到什麼代表什麼:任何一筆命中都是一把可能被複製、被 commit、被貼進環境變數的長期私鑰;數量比你以為的多,就是共用金鑰蔓延的現場。
結尾:門與鑰匙
回到開頭那條規則:所有人先進跳板機,再進其他主機。它寫得很好,壞在兩個地方——通往其他主機的門其實一直開著,而那把能開全叢集的鑰匙沒有上鎖。三十天前那次從家用寬頻進來的登入,證明的不是誰做了什麼,是這扇門確實可以不經跳板機就推開。
要把這扇門關上,技術上一點都不難:加一個 passphrase、收一條防火牆規則、簽一張會過期的憑證。難的是承認一件事——方向對,不等於已經安全。至於那把萬能私鑰究竟還躺在幾個地方、那次登入的手是不是自家人,這兩個問題,要等我們把查核憑證重新辦好、把紀錄重新接上,才會有答案。
來源
- Ubuntu 釋出週期與 16.04(Xenial)支援終止
- AWS Systems Manager Session Manager(連線不需開 22、私鑰不落地)
- OpenSSH Certificate-based Authentication(SSH CA,ssh-keygen -s 簽發有時效憑證)
- AWS IAM 最佳做法:優先使用暫時憑證而非長期金鑰
- Kubernetes:Secrets(預設僅 base64 編碼)
- Kubernetes:Encrypting Confidential Data at Rest
- AWS Secrets Manager 使用者指南
- Infisical 文件
- MITRE ATT&CK:T1021.004 Remote Services: SSH
- MITRE ATT&CK:T1552.004 Unsecured Credentials: Private Keys
- 本站防暴筆記:排程回報「找不到帳戶」,被停權的是整個專案
- 本站拆解:清單全部打勾的第 13 天,整個專案被停權了
每篇新文章一封:三點摘要、關鍵字定義、今天能做的一件事。
延伸閱讀
被偷三次之後,我們決定換一種方式放鑰匙
後門 Lambda、Owner 金鑰 15 天、356 天挖礦、單日 $3,901 的 Gemini 帳單——四次損失的共同點不是漏洞,是明文憑證散落各處。這篇是作者分析:為什麼我們推薦開發者把憑證交給 1Password Developer Security 這類 secrets manager,以及它擋得住什麼、擋不住什麼。
寄信功能十分鐘就會動,難的是不讓別人也能用
本站新增「寄一份盤點結果給我」:應用端一把金鑰都不放,信交給內部閘道送,對外路由一律 token 加限流。外加一個把整組環境變數清空換來的教訓。
我們寫了一道關卡防自己,第二次部署它就亮了紅燈
沒有攻擊、沒有外洩、沒有帳單,這是一份設定稽核紀錄:一支只該讀寫試算表的 GCP:Service Account,在專案層是 Owner。拿掉只要一行,拿掉前要先查什麼?