適用對象:從 GPU 雲的 pod、CI runner、一次性 VM 這類臨時環境透過 SSH 連到正式環境的人。這裡不講攻擊手法,只講 把金鑰做成最小權限、讓外洩造成的損害更小。金鑰分離的基本做法,請參考 安全底線清單。
本站的看法:把臨時環境視為不可信任
用完即丟的運算環境常常省略加固,是可能被入侵的地方。把正式環境的 root 金鑰放在那裡,該環境被入侵就等於正式環境被入侵。本站每台主機使用不同的金鑰,每把金鑰只限一個用途,不用就立刻刪除(→ 分開金鑰以減少損害)。不把金鑰當作「方便的工具」,而是當作「一旦外洩就能進入所有伺服器的最重要資產」,終究是最安全的做法。
✗ 臨時環境上的 root 金鑰
環境被入侵 → 用那把金鑰取得正式環境的 root → 檔案、機密、其他服務,全部都能觸及。
○ 限制指令的非 root 金鑰
即使同一把金鑰外洩,也只能執行指定的那一條指令。沒有 shell、沒有轉發,損害被限制在單一操作。
為什麼臨時環境上的 root 金鑰很危險
臨時環境常因「反正很快就刪」而省略加固,因此容易成為攻擊目標,也方便被當作攻擊正式環境的跳板。正式環境的 root 金鑰放在那裡,會讓損害變得最大。
假設你為了運算開了一個 pod,為了從那裡處理正式環境,把金鑰登記到 root 的 authorized_keys。很方便,但如果那個 pod 被入侵,攻擊者就會用那把金鑰以 root 進入正式環境。無論攻擊是從 RCE 還是金鑰外洩開始,最後的結果都一樣:正式環境被偷來的金鑰拿下(相關:金鑰經由 RCE 被盜並遭惡意刷費)。
把金鑰做成最小權限的 3 個步驟
從「因為方便就給全部權限」,改成「把能做的事限制到最小」。
刪除臨時環境的 root 金鑰
~/.ssh/authorized_keys 刪除不再使用的金鑰。如果沒在用,刪除不會有任何影響。首先,不要留下沒在用的金鑰。即使再次需要,也不要回到 root
把金鑰限制為單一操作
command="..." 和 restrict。即使登入成功,也只能執行指定的那一條指令,連接埠轉發和 shell 都被停用。外洩時損害被限制在那一個操作。# Register a key restricted to ONE operation in a non-root user's authorized_keys.
# Logging in with it can run only the given command; forwarding and PTY are off.
command="/usr/local/bin/deploy-pull",restrict ssh-ed25519 AAAA...rest-of-public-key deploy@cirestrict 一次停用轉發、agent 轉發、PTY、X11 等功能;command="..." 把可執行的操作固定為一個。基本做法是按用途建立只限單一用途的金鑰,例如只拉取部署、只執行特定腳本。
常見的做法(危險)
- 從 pod/CI 把金鑰登記到正式環境的 root
- 用完的金鑰留著不管
- 一把金鑰在多台主機上重複使用
- 到處散布能取得完整 shell 的金鑰
最小權限的做法
- 只以非 root 使用者進入正式環境
- 金鑰限制為單一操作(command 限制)
- 按主機和用途分開金鑰
- 一不再使用,就從 authorized_keys 刪除
把重複使用的金鑰當作最重要的資產
把金鑰當作「方便的工具」,就會重複使用、忘記刪除。相反地,要把它當作一旦外洩就能進入所有伺服器的最重要資產。按主機和用途分開,不讓正式環境的金鑰出現在臨時環境,用完一定刪除。光是這樣,就能防止一次外洩擴散到全部。
金鑰那一行可以加上的主要限制
在 OpenSSH 中,寫在 authorized_keys 金鑰那一行開頭的選項,可以縮小該金鑰能做的事。常用的有:
| 選項 | 限制的內容 | 使用時機 |
|---|---|---|
command="..." | 用該金鑰登入時只執行指定的指令;用戶端送來的指令會被忽略 | 只做一件固定工作的金鑰,例如拉取部署或備份 |
restrict | 一次停用連接埠、agent 和 X11 轉發、PTY 分配,以及執行 ~/.ssh/rc(OpenSSH 7.2 以上) | 幾乎所有自動化用的金鑰;只把需要的功能(例如 pty)重新打開 |
from="..." | 除了金鑰之外,用戶端的主機名稱或 IP 位址也必須符合清單 | 來源 IP 固定的 CI 或作業伺服器 |
expiry-time="..." | 超過指定日期時間後不再接受該金鑰(OpenSSH 7.7 以上) | 只供臨時作業使用的金鑰 |
PermitRootLogin(sshd_config 中) | 是否允許直接以 root 登入;forced-commands-only 只允許帶有 command= 的金鑰以 root 登入 | 除非真的需要直接以 root 存取,否則設為 no |
OpenSSH 的版本說明把 restrict 描述為包含「目前及未來所有的金鑰限制」。與其逐一列出 no-pty、no-port-forwarding 等選項,不如加上 restrict,再只打開需要的功能,這樣將來新增功能時也不會有漏洞。
# Also narrowing the source and the lifetime (the IP is a documentation address)
from="192.0.2.10",expiry-time="20261231",command="/usr/local/bin/deploy-pull",restrict ssh-ed25519 AAAA...rest-of-public-key deploy@ci-2026-10如果要管理許多金鑰的有效期限,也可以使用 OpenSSH 憑證(用 ssh-keygen -s 簽署,用 -V 設定有效期間,用 force-command 或 source-address 加以限制)。規模不大的話,從金鑰那一行的選項開始就足夠了。
如何檢查自己的環境
以下檢查只讀取你所管理伺服器的設定和記錄。變更設定時,保持一個 SSH 連線開著,從另一個終端機測試新的連線,確認自己沒有被鎖在外面之後,再關閉舊的連線。
1. 列出已登記的金鑰
# Show the fingerprint and comment of each key registered for root and each user
sudo ssh-keygen -lf /root/.ssh/authorized_keys
sudo ssh-keygen -lf /home/deploy/.ssh/authorized_keys每把金鑰會印出一行,例如 256 SHA256:... deploy@ci (ED25519)。用註解(最後的 deploy@ci 部分)找出來源不明的金鑰、已用完的臨時環境的金鑰,以及登記給 root 的金鑰。舊的檔名 authorized_keys2 預設也會被讀取,如果存在也用同樣方式檢查。
2. 確認 sshd 實際使用的設定
# Check the syntax, then print the settings actually in effect
sudo sshd -t
sudo sshd -T | grep -iE 'permitrootlogin|authorizedkeysfile|pubkeyauthentication|passwordauthentication'sshd -T 會印出讀取設定檔之後實際生效的值。如果 permitrootlogin 是 yes 或 prohibit-password,只要有金鑰就能直接以 root 登入。如果 authorizedkeysfile 指向預設以外的位置,把那個位置也加進步驟 1。
3. 查看哪些金鑰實際被使用
用金鑰登入成功後,sshd 通常會記錄像 Accepted publickey for deploy from 192.0.2.10 port 50022 ssh2: ED25519 SHA256:... 這樣的一行。在 Debian 和 Ubuntu 上看 /var/log/auth.log,在 Red Hat 系統上看 /var/log/secure,在 systemd 主機上則用 journalctl -u ssh(Red Hat 系統為 -u sshd)。
把這些指紋與步驟 1 的清單對照,就能看出哪些金鑰沒有被使用。NIST 關於 SSH 金鑰管理的報告(NISTIR 7966)建議,在刪除未使用的金鑰之前觀察記錄約 6 到 12 個月,以免誤刪很少使用的金鑰。確定已用完的金鑰,例如臨時環境的金鑰,可以立即刪除。
4. 檢查臨時環境那一側
- CI 的 secrets 或 GPU pod 的
~/.ssh裡,是否有能連到正式環境的私鑰? - 是否用
ssh -A(agent 轉發)連到臨時環境,再從那裡前往正式環境? - 是否有一把金鑰在多個 CI 系統或伺服器之間共用?
常見錯誤與修正
- 只刪除臨時環境上的私鑰就結束:如果私鑰已經被複製,只要公鑰還留在伺服器上,它就繼續有效。一定要從正式環境的
authorized_keys刪除對應的那一行。 - 在
command=呼叫的腳本中直接執行用戶端的指令(環境變數SSH_ORIGINAL_COMMAND):這等於沒有限制。只有在值與固定選項之一完全一致時才使用它。 - 改用非 root 使用者,卻允許不受限的 sudo:如果該使用者可以不輸入密碼就 sudo 任何指令,實際上就等於 root。如果需要 sudo,在 sudoers 中限制為一條指令。
- 用
ssh -A進入臨時環境:在那裡擁有足夠權限的人,可以透過轉發的連線使用你本機 agent 中載入的金鑰(OpenSSH 手冊對此有警告)。如果需要經過某台主機,使用ssh -J(經由跳板主機連線)。 - 金鑰沒有註解:檢查時無法分辨來源。建立時就加上用途和日期,例如
ssh-keygen -C "deploy@ci-2026-10"。 - 多人或多個環境共用一把金鑰:NISTIR 7966 也要求禁止共用私鑰或把私鑰複製到其他系統。這樣無法追蹤誰在哪裡使用了它,而且一次外洩就必須在所有地方更換。
實際案例:遭入侵的 CI 工具蒐集的資料中包含 SSH 金鑰
2026 年 3 月,開源漏洞掃描工具 Trivy 出現了惡意版本,其 GitHub Actions 的標籤也被改寫。據開發商 Aqua Security 表示,惡意程式碼從 CI runner 蒐集憑證並傳送出去,目標包括 ~/.ssh 中的私鑰、authorized_keys、known_hosts 以及 SSH 主機金鑰。Aqua 要求所有可能執行過受影響版本的人,把該管線能讀取的所有機密(包括 SSH 金鑰)視為已曝光,並立即輪換(詳情:Trivy 供應鏈入侵事件(2026 年 3 月),英文)。
在 2021 年的 Codecov 事件中,CI 中執行的腳本也在來源端被竄改,客戶 CI 環境中的機密被傳送出去長達約兩個月(→ Codecov 供應鏈攻擊)。
兩者都顯示,即使你自己的程式碼沒有問題,CI 裡的金鑰也可能被拿走。如果被拿走的金鑰登記在正式環境的 root,正式環境在那一刻就被交出去了。如果使用專用的非 root 使用者、command= 加上 restrict,並用 from= 縮小來源,同一把外洩的金鑰就只能做你定義好的那一件工作,從其他地方的連線也會被拒絕。
本站是怎麼做的
本站每台伺服器使用專用的金鑰,從不重複使用。部署落在一個非 root、專門用途的使用者上,正式環境是單向的:「只接收,不主動外出取得」(→ 正式環境只接收)。本站不會在臨時運算環境留下通往正式環境的常駐金鑰;只在需要時、以最小權限連線。理由正是本文所說的:事先讓每把金鑰能做的事保持最小,讓一次入侵無法擴散到全部。
接下來讀
- 新弱點:OpenSSH 10.6p2 修補了無 PAM 的 musl Linux 上 sshd 的密碼驗證繞過問題(CVE-2026-123158)
- 基礎:安全底線清單(最小權限、保護管理員帳號)
- 維運:分開金鑰以減少損害/正式環境只接收 · 漏洞應對手冊
- 案例:Trivy 供應鏈入侵(CI 中的 SSH 金鑰成為目標)(英文) · Codecov 供應鏈攻擊
- 事故:金鑰經由 RCE 被盜並遭惡意刷費 · 倍樂生個資外洩事件(權限過大與內部人員濫用)
出處
- OpenBSD manual「sshd(8)」AUTHORIZED_KEYS FILE FORMAT(command、restrict、from、expiry-time、SSH_ORIGINAL_COMMAND):man.openbsd.org
- OpenBSD manual「sshd_config(5)」PermitRootLogin 與 AuthorizedKeysFile:man.openbsd.org
- OpenBSD manual「ssh(1)」-A(agent 轉發的注意事項)與 -J:man.openbsd.org
- OpenBSD manual「ssh-keygen(1)」-l、-C 與憑證選項:man.openbsd.org
- OpenSSH 7.2 版本說明(新增 restrict):openssh.org
- OpenSSH 7.7 版本說明(新增 expiry-time):openssh.org
- NIST「NISTIR 7966: Security of Interactive and Automated Access Management Using Secure Shell (SSH)」(2015 年 10 月):csrc.nist.gov
- Wikibooks「OpenSSH/Logging and Troubleshooting」(登入成功記錄中的金鑰指紋):en.wikibooks.org
- Aqua Security「Trivy Supply Chain Attack: What You Need to Know」(2026):aquasec.com
- Codecov「Bash Uploader Security Update」(2021):about.codecov.io