SSH Key Authentication

如何使用 SSH Key 進行安全無密碼連線

前言

記得剛開始推代碼到 GitHub 時,首要的事情就是透過設置 HTTP 或 SSH 驗證的方式進行 git 操作,很早以前設置完後就忘了當時是怎麼設置的,近期在處理伺服器連線又想起來應該要熟悉寫下來背後的原理與相關的安全機制。

為什麼不用密碼就好?

SSH 本身支援密碼登入,但密碼驗證有幾個先天問題:

  • 密碼會被傳輸:即使傳輸過程有加密,密碼本身仍然會送到伺服器端,伺服器有機會接觸到它。
  • 可暴力破解:對外開放 22 port 的機器,每天都會收到大量自動化的字典攻擊嘗試。

SSH Key 驗證解決的核心問題是:如何在不把秘密送出去的前提下,證明自己擁有這個秘密

非對稱加密的基礎

SSH Key 是一組非對稱加密的金鑰對(Key Pair),由數學上關聯的兩把金鑰組成:

  • 私鑰(Private Key)~/.ssh/id_ed25519,永遠留在自己的電腦,絕對不外流。
  • 公鑰(Public Key)~/.ssh/id_ed25519.pub,可以隨意公開,複製到任何想登入的伺服器上。

這組金鑰的關鍵性質是:私鑰可以對資料產生簽章,而任何人都能用公鑰驗證這個簽章是否真的由對應的私鑰所產生,但反過來無法從公鑰推導出私鑰。

一次 SSH 連線發生什麼事

完整的連線可以拆成兩個階段:先建立加密通道,才進行身份驗證。

第一階段:建立加密通道

在任何驗證發生之前,Client 與 Server 會先協商出一條加密通道:

  1. 雙方交換支援的演算法清單(金鑰交換、加密、MAC、壓縮)
  2. 透過 Diffie-Hellman(或現在常見的 ECDH)金鑰交換,雙方各自算出一把只有彼此知道的對稱金鑰(Session Key),而這把金鑰從未在網路上傳輸過
  3. 這個過程同時會產生一個唯一的 Session ID,後面驗證階段會用到
  4. 之後所有的封包都用這把 Session Key 以對稱加密(如 AES-GCM、ChaCha20-Poly1305)保護

Host Key:你連到的真的是那台機器嗎?

金鑰交換的同時,Server 也會用它自己的私鑰簽章,證明自己的身份。這就是第一次連線時會看到的提示:

Terminal window
The authenticity of host 'example.com (93.184.216.34)' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

按下 yes 之後,這台主機的公鑰指紋會被記錄到 ~/.ssh/known_hosts。下次連線時如果指紋對不上,SSH 會直接拒絕連線並發出警告,因為這可能代表中間人攻擊

第二階段:公鑰驗證

加密通道建立後,才開始證明「是誰登入」:

  1. Client:想用這把公鑰登入,你接受嗎?(只送出公鑰)
  2. Server:到該使用者的 ~/.ssh/authorized_keys 找有無這把公鑰,沒有就拒絕。
  3. Client:用私鑰對一段包含 Session ID、使用者名稱、服務名稱⋯⋯等資訊產生簽章。
  4. Server:用 authorized_keys 裡的那把公鑰驗證簽章。驗證通過 = 對方確實持有對應的私鑰 = 登入成功。

兩個關鍵設計:

  1. 私鑰從頭到尾沒有離開過本機,伺服器被入侵也只會外流公鑰。
  2. 簽章的內容綁定了 Session ID,每次連線的 Session ID 都不同,所以攻擊者側錄到的簽章無法拿去重放攻擊到另一個連線。

實際操作

生成金鑰

OpenSSH 常見於所有主流作業系統中提供 ssh-keygen 與底下更多工具來生成管理金鑰對。

Terminal window
ssh-keygen -t ed25519 -C "公鑰結尾的註解"

雖然常見的教學是 -t rsa -b 4096,但現在建議優先使用 Ed25519:金鑰更短、簽章更快、安全強度相當於 RSA-3072 以上,且沒有 RSA 容易被參數設錯的問題。

產生的兩個檔案:

Terminal window
~/.ssh/id_ed25519 # 私鑰,絕不外流
~/.ssh/id_ed25519.pub # 公鑰,可公開

過程中會詢問 Passphrase,建議一定要設定:有設定時私鑰檔案才會被加密儲存,Passphrase 就是解開它的密碼,萬一筆電被偷或私鑰檔案外流,攻擊者沒有 Passphrase 仍然無法使用;直接按 Enter 留空則私鑰是明文儲存,拿到檔案就等於拿到帳號。

用 ssh-agent 避免每次輸入 Passphrase

設了 Passphrase 之後每次連線都要輸入很麻煩,ssh-agent 是一個常駐在背景的程式,把解密後的私鑰保存在記憶體中代為簽章:

Terminal window
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

macOS 可以把 Passphrase 存進 Keychain,並在 ~/.ssh/config 中設定第一次使用金鑰時自動加入 agent:

Terminal window
Host *
IgnoreUnknown UseKeychain
UseKeychain yes
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519

把公鑰放到遠端伺服器

authorized_keys 是一個純文字檔,一行一把公鑰。手動貼上容易出錯(換行、權限),建議用 ssh-copy-id

Terminal window
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@hostname

它做的事情就是用現有的方式(通常是密碼)登入一次,然後把公鑰附加到遠端的 ~/.ssh/authorized_keys 並修好權限。

連線

Terminal window
ssh -i ~/.ssh/id_ed25519 user@hostname

與其每次都打一長串,更實用的是寫進 ~/.ssh/config

Terminal window
Host myserver
HostName 93.184.216.34
User ubuntu
Port 22
IdentityFile ~/.ssh/id_ed25519

之後只要 ssh myserver 即可,scprsyncgit 也都會讀這份設定。

檔案權限

SSH 對權限非常嚴格,權限太寬鬆會直接拒絕使用該金鑰(因為同機器上的其他使用者可能讀得到):

Terminal window
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/authorized_keys

典型的錯誤訊息:

Terminal window
Permissions 0644 for '~/.ssh/id_ed25519' are too open.
It is required that your private key files are NOT accessible by others.

搭配 GitHub 使用

GitHub 的 SSH 驗證是同一套機制,差別只在於 GitHub 不給你 Shell,而是用公鑰判斷「這個 push 是哪個帳號發出的」:

  1. ~/.ssh/id_ed25519.pub 的內容貼到 GitHub 的 Settings → SSH and GPG keys。
  2. 測試連線:
Terminal window
ssh -T git@github.com
# Hi username! You've successfully authenticated, but GitHub does not provide shell access.
  1. 之後 clone 時使用 SSH URL(git@github.com:user/repo.git)而非 HTTPS URL。

除錯時 -v(可疊加到 -vvv)會印出完整的協商與驗證過程,能清楚看到它試了哪幾把金鑰、為什麼被拒絕:

Terminal window
ssh -vT git@github.com

總結

  • SSH 連線分兩階段:先用金鑰交換建立對稱加密通道,再做身份驗證。
  • Host Key 驗證的是「伺服器是不是本人」,公鑰驗證的是「你是不是本人」。
  • 簽章綁定 Session ID,因此無法重放。
  • 新金鑰優先選 Ed25519、一定要設 Passphrase、搭配 ssh-agent 使用。
  • 私鑰外流等同帳號外流,發生時立刻從所有 authorized_keys 與 GitHub 移除對應公鑰並重新產生金鑰對。

延伸閱讀