SSH 金鑰認證機制

從金鑰長相、公鑰該放進誰的家目錄,到 sshd 設定與除錯,完整理解 SSH 金鑰認證的運作方式


目錄


什麼是 SSH 金鑰認證?

SSH 金鑰認證是一種基於非對稱加密的身份驗證機制:本地保管一把私鑰,伺服器上登記對應的公鑰,登入時由客戶端用私鑰對伺服器出的隨機挑戰簽名,伺服器用公鑰驗證簽名是否成立。

相比密碼認證的差異

面向 密碼認證 金鑰認證
傳輸內容 密碼本身送到伺服器(雖有加密通道) 只送簽名,私鑰永遠不離開本地
暴力破解 可對外開放的 SSH 埠持續猜測 無法從公鑰反推私鑰,猜測不可行
撤銷 改密碼,所有使用者都受影響 刪掉 authorized_keys 中的那一行即可
多台伺服器 每台一組密碼 同一把金鑰可登入多台
自動化 需把密碼寫在腳本裡 金鑰檔配合 agent,不必存明文密碼

準確地說:金鑰認證安全的關鍵不是「密碼長度不夠」,而是私鑰不會在網路上傳輸。密碼認證即使密碼很強,伺服器端仍會收到密碼原文;若伺服器被入侵,密碼就外洩了。金鑰認證下,被入侵的伺服器只拿得到公鑰。

取捨

  • 私鑰檔案本身變成單點風險:檔案被複製走等同帳號被接管(除非有 passphrase)
  • 換電腦、多裝置需要重新部署或搬移金鑰
  • 沒有集中撤銷機制:一把金鑰散佈到 50 台機器,就要清 50 個 authorized_keys(這是後面「SSH 憑證」要解決的問題)

兩組金鑰別搞混:使用者金鑰 vs 主機金鑰

SSH 連線裡有兩組互相獨立的金鑰對,初學時最容易混淆:

使用者金鑰(User Key) 主機金鑰(Host Key)
用途 證明「是誰」 證明「伺服器是誰」
私鑰放哪 本地 ~/.ssh/id_ed25519 伺服器 /etc/ssh/ssh_host_ed25519_key
公鑰放哪 伺服器 ~/.ssh/authorized_keys 本地 ~/.ssh/known_hosts
誰產生 使用者自己 ssh-keygen 伺服器安裝 OpenSSH 時自動產生
出問題時的訊息 Permission denied (publickey) REMOTE HOST IDENTIFICATION HAS CHANGED!

一次連線的驗證是雙向的:

  1. 伺服器先出示主機金鑰 → 客戶端比對 known_hosts,確認沒有連到假伺服器(防中間人攻擊)
  2. 客戶端再用使用者金鑰簽名 → 伺服器比對 authorized_keys,確認你有權限登入

💡 記憶方式authorized_keys 是伺服器的「訪客名單」,known_hosts 是你的「認得的伺服器清單」。兩個檔案裡放的都是公鑰,但方向相反。


運作原理

認證流程

三個關鍵細節

1. 私鑰是用來「簽名」,不是「解密」

常見的錯誤說法是「伺服器用公鑰加密一段資料,客戶端用私鑰解密」。現代 OpenSSH 走的是挑戰/回應簽名:伺服器出一段包含 session ID 的資料,客戶端簽名,伺服器驗簽。這個差別很重要——因為簽名內容綁定了本次連線的 session ID,攔截到的簽名無法重放到另一條連線上

2. 客戶端會依序嘗試多把金鑰

~/.ssh 下有多把金鑰、或 ssh-agent 裡載入了多把,客戶端會一把一把試。伺服器端 MaxAuthTries 預設 6,試太多次會直接被踢:

Received disconnect from ... : Too many authentication failures

解法是明確指定金鑰(見 SSH ConfigIdentitiesOnly)。

3. 認證成功後才進入加密通道的「使用」階段

連線加密(步驟 1 的演算法協商 + 金鑰交換)在身份認證之前就完成了。所以即使認證失敗,過程中的往返也是加密的。


產生出來的金鑰長什麼樣子

ssh-keygen 產生的是兩個檔案,副檔名 .pub 的是公鑰,沒有副檔名的是私鑰。

ssh-keygen -t ed25519 -C "alice@laptop"
# 產生:
#   ~/.ssh/id_ed25519      ← 私鑰
#   ~/.ssh/id_ed25519.pub  ← 公鑰

公鑰:永遠是「一行」,三個欄位

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJEXH0adJ11ndx2PMOYtoLevHbZ36j3fwAZRmykvFO4c alice@laptop
└─── ① ───┘ └────────────────────── ② ──────────────────────────────────────┘ └──── ③ ────┘
欄位 內容 說明
① 演算法 ssh-ed25519 金鑰類型。RSA 是 ssh-rsa,ECDSA 是 ecdsa-sha2-nistp256
② 金鑰主體 AAAAC3Nza... Base64 編碼的實際公鑰資料
③ 註解 alice@laptop 純標註用,改掉或刪掉都不影響認證

② 為什麼每把 Ed25519 公鑰開頭都一樣?

Base64 解開後是「長度前綴 + 欄位」的二進位結構,第一個欄位就是演算法名稱字串:

00000000: 0000 000b 7373 682d 6564 3235 3531 3900  ....ssh-ed25519.
          └─長度 11─┘└──── "ssh-ed25519" ────┘
00000010: 0000 2091 171f 469d 275d 6777 1d8f 30e6  .. ...F.']gw..0.
          └長度 32┘└──── 32 位元組的公鑰本體 ────

因為開頭固定是「11、ssh-ed25519」,Base64 之後就固定是 AAAAC3NzaC1lZDI1NTE5AAAAI。看到這串就知道是 Ed25519 公鑰。同理:

開頭字串 金鑰類型
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5 Ed25519
ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB RSA(AAAADAQAB 是公開指數 65537)
ecdsa-sha2-nistp256 AAAAE2VjZHNh ECDSA P-256

長度差很多:Ed25519 公鑰整行約 94 位元組,RSA-4096 公鑰約 738 位元組(將近 8 倍)。這是實務上偏好 Ed25519 的原因之一——貼到 Web 介面或 metadata 欄位時短很多。

私鑰:多行 Base64 區塊

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCRFx9GnSddZ3cdjzDmLaC3rx22d+o938AGUZspLxTuHAAAAJCuLTPeri0z
(中間省略若干行)
-----END OPENSSH PRIVATE KEY-----

從第二行就能看出有沒有設 passphrase

第二行開頭 意義
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQ 未加密none / none,即無加密演算法、無 KDF)
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0 有 passphraseaes256-ctr 加密 + bcrypt KDF)

b3BlbnNzaC1rZXktdjEAopenssh-key-v1\0 的 Base64,所有新格式私鑰都以此開頭。)

兩種私鑰格式

開頭標記 格式 說明
-----BEGIN OPENSSH PRIVATE KEY----- OpenSSH 新格式 現在的預設,支援 bcrypt KDF,較耐離線暴力破解
-----BEGIN RSA PRIVATE KEY----- 舊 PEM 格式 OpenSSH 7.8 之前的預設;有加密時會出現 Proc-Type: 4,ENCRYPTED 標頭

舊格式可轉新格式:

ssh-keygen -p -f ~/.ssh/id_rsa    # 重設 passphrase 時順便轉為新格式

指紋(Fingerprint)

指紋是公鑰的雜湊摘要,用來人工核對是不是同一把金鑰——因為要人眼比對整串 Base64 不現實。

ssh-keygen -lf ~/.ssh/id_ed25519.pub
# 256 SHA256:6nOrGsa/U8fqPaqVa570CkFPVwNlc47paczAEWtSMZw alice@laptop (ED25519)
#  ↑位元數  ↑SHA-256 摘要(Base64)                        ↑註解        ↑類型

對私鑰檔案下同樣指令會得到相同指紋(因為指紋算的是對應的公鑰):

ssh-keygen -lf ~/.ssh/id_ed25519   # 結果與上面一致

用途:GitHub/GitLab 的金鑰列表顯示的就是這串指紋,可用來確認「我本機這把,是不是就是網站上登記的那把」。

弄丟公鑰?可以從私鑰重新導出

公鑰是從私鑰算出來的,刪掉可以重生(反之不行):

ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub

💡 這個指令還有個副作用很好用:若私鑰有 passphrase,它會要求輸入。忘記自己有沒有設 passphrase 時,用它測試最快。

貼公鑰時最常見的三個錯誤

# ❌ 被編輯器或聊天軟體換行切斷 → authorized_keys 必須一把金鑰一整行
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJEXH0adJ11ndx2PMOYt
oLevHbZ36j3fwAZRmykvFO4c alice@laptop

# ❌ 只複製了 Base64 主體,漏掉開頭的演算法欄位
AAAAC3NzaC1lZDI1NTE5AAAAIJEXH0adJ11ndx2PMOYtoLevHbZ36j3fwAZRmykvFO4c

# ❌ 貼到了私鑰(開頭是 BEGIN ... PRIVATE KEY)
-----BEGIN OPENSSH PRIVATE KEY-----

安全的複製方式(避免手動選取出錯):

# macOS
pbcopy < ~/.ssh/id_ed25519.pub

# Linux(X11)
xclip -sel clip < ~/.ssh/id_ed25519.pub

公鑰要放到哪裡?

這是整套機制最容易含糊帶過、卻最常出錯的地方。

核心規則:登入身分決定家目錄

ssh alice@server.example.com
#   └─①─┘

① 這個使用者名稱,決定公鑰必須放進哪個家目錄。

伺服器收到「要用 alice 登入」後,會去查 alice 這個系統帳號的家目錄,再讀該目錄下的 .ssh/authorized_keys

登入指令 伺服器實際去讀的檔案
ssh alice@host /home/alice/.ssh/authorized_keys
ssh deploy@host /home/deploy/.ssh/authorized_keys
ssh root@host /root/.ssh/authorized_keys(root 家目錄不在 /home 底下)
ssh ubuntu@host /home/ubuntu/.ssh/authorized_keys(AWS Ubuntu AMI 預設帳號)

家目錄實際位置由 /etc/passwd 決定,可直接查:

getent passwd alice | cut -d: -f6
# /home/alice

與「你在本機叫什麼名字」完全無關。本機使用者是 alice、要以 deploy 身分登入伺服器,公鑰就得進 /home/deploy/.ssh/authorized_keys

由此推導出的三種常見情境

情境 1:一個人、多個伺服器帳號

同一把公鑰可以同時放進多個帳號的 authorized_keys

本機 ~/.ssh/id_ed25519.pub(一把)
   ├──→ server-a:/home/alice/.ssh/authorized_keys   → ssh alice@server-a
   ├──→ server-a:/home/deploy/.ssh/authorized_keys  → ssh deploy@server-a
   └──→ server-b:/root/.ssh/authorized_keys         → ssh root@server-b

情境 2:多個人、共用一個帳號

團隊都用 deploy 帳號部署,就把每個人的公鑰各佔一行放進同一個檔案:

# /home/deploy/.ssh/authorized_keys
ssh-ed25519 AAAAC3Nza...FO4c alice@laptop
ssh-ed25519 AAAAC3Nza...9xKq bob@thinkpad
ssh-rsa     AAAAB3Nza...w1Zp carol@desktop

註解欄位在這裡就很有價值——它是唯一能辨識「這行是誰的」的線索,要撤銷 Bob 的權限時才知道刪哪一行。

⚠️ 取捨:共用帳號的稽核紀錄只會顯示 deploy 登入,看不出是誰。稽核要求高的環境應該一人一帳號,或改用 SSH 憑證。

情境 3:一台機器、多把不同金鑰

工作與個人金鑰分開,兩把公鑰放進同一個帳號也完全可行——伺服器只認「這行公鑰在不在名單上」。

例外:authorized_keys 不一定在家目錄

以上都是預設行為。伺服器可以改設定,此時把公鑰寫進 ~/.ssh/authorized_keys完全沒有效果

# /etc/ssh/sshd_config

# 例外 1:改成集中管理目錄(%u 會替換成使用者名稱)
AuthorizedKeysFile /etc/ssh/authorized_keys/%u

# 例外 2:改由外部指令動態提供公鑰
# (OS Login、LDAP、Teleport、Vault 等集中式方案都走這條)
AuthorizedKeysCommand /usr/bin/google_authorized_keys
AuthorizedKeysCommandUser root

排查前先確認伺服器實際採用哪一種:

sudo sshd -T | grep -i authorizedkeys
# authorizedkeysfile .ssh/authorized_keys .ssh/authorized_keys2
# authorizedkeyscommand none

sshd -T 印出實際生效的完整設定,比看設定檔可靠——設定檔可能有 IncludeMatch 區塊或被註解掉的行。)

雲端環境幾乎都屬於例外:GCE 由 guest agent 依 metadata 寫入 authorized_keys(或啟用 OS Login 走 AuthorizedKeysCommand),AWS EC2 由 cloud-init 在首次開機寫入預設帳號。這些環境下手動改 authorized_keys 可能在下次同步時被覆寫。


檔案位置與權限總覽

客戶端(本地電腦)

~/.ssh/
├── id_ed25519          # 私鑰(600)
├── id_ed25519.pub      # 公鑰(644)
├── config              # 客戶端設定檔(600)
├── known_hosts         # 連過的伺服器主機公鑰(644)
└── authorized_keys     # 只在本機也當伺服器時才需要

伺服器端

/home/alice/                        # 家目錄(755 或更嚴,不可 group/other 可寫)
└── .ssh/                           # 700
    └── authorized_keys             # 600

/etc/ssh/
├── sshd_config                     # 伺服器設定
├── ssh_host_ed25519_key            # 主機私鑰(600)
└── ssh_host_ed25519_key.pub        # 主機公鑰(644)

權限一覽

路徑 權限 為什麼
~/.ssh 700 只有自己能列出/修改內容
私鑰 id_* 600 權限過鬆時 ssh 會直接拒絕使用
公鑰 *.pub 644 可公開
authorized_keys 600 別人可寫 = 別人能加自己的公鑰進來
家目錄本身 不可被 group/other 寫入 見下方 StrictModes

StrictModes:最常被忽略的一關

sshd 預設 StrictModes yes,會在讀 authorized_keys 之前檢查:家目錄、.ssh 目錄、authorized_keys 都不可以被「擁有者以外的人」寫入。任何一層太鬆,sshd 就當作沒有這個檔案,直接回 Permission denied (publickey)——而且不會告訴你原因

# ❌ 典型踩雷:家目錄開了群組寫入權限
drwxrwxr-x  alice alice  /home/alice     # group 可寫 → 認證失敗

# ✅ 一次修好整條路徑
chmod 755 /home/alice
chmod 700 /home/alice/.ssh
chmod 600 /home/alice/.ssh/authorized_keys
chown -R alice:alice /home/alice/.ssh

chown 那行同樣關鍵:用 sudo 建立 .ssh 目錄時,很容易變成 root 擁有,sshd 一樣會拒絕。


金鑰類型怎麼選

# ✅ 現在的預設選擇
ssh-keygen -t ed25519 -C "alice@laptop"

# ✅ 需要相容非常舊的系統時
ssh-keygen -t rsa -b 4096 -C "alice@laptop"
類型 建議 說明
Ed25519 ⭐ 首選 速度快、金鑰短、安全強度足夠。OpenSSH 6.5(2014)起支援
RSA 4096 相容備案 需支援老舊設備/堡壘機時使用。-b 2048 是目前的下限,新建議用 4096
ECDSA 不建議新增 沒有明顯優於 Ed25519 的理由
DSA ❌ 已淘汰 固定 1024 位元,新版 OpenSSH 已移除支援

注意 -b 對 Ed25519 無效:Ed25519 金鑰長度固定 256 位元,-b 4096 會被忽略。

ssh-rsa 被停用 ≠ RSA 金鑰不能用:OpenSSH 8.8 起預設停用的是 SHA-1 簽名演算法(名稱剛好也叫 ssh-rsa),不是 RSA 金鑰本身。同一把 RSA 金鑰改用 rsa-sha2-256 / rsa-sha2-512 簽名仍可正常運作,只有非常舊的伺服器才會出問題。

加強 passphrase 保護(新格式私鑰的 KDF 迭代次數,預設 16):

ssh-keygen -t ed25519 -a 100 -C "alice@laptop"

快速開始:三個步驟

步驟 1:產生金鑰對

ssh-keygen -t ed25519 -C "alice@laptop"

互動提示:

Enter file in which to save the key (/home/alice/.ssh/id_ed25519):   ← Enter 用預設
Enter passphrase (empty for no passphrase):                          ← 建議設定
Enter same passphrase again:

輸出:

Your identification has been saved in /home/alice/.ssh/id_ed25519
Your public key has been saved in /home/alice/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:6nOrGsa/U8fqPaqVa570CkFPVwNlc47paczAEWtSMZw alice@laptop

-f 可指定檔名(多把金鑰時必要):

ssh-keygen -t ed25519 -C "work" -f ~/.ssh/id_ed25519_work

步驟 2:把公鑰放到目標帳號

# ✅ 方法一:ssh-copy-id(推薦,會自動處理目錄與權限)
ssh-copy-id -i ~/.ssh/id_ed25519.pub alice@server

# 方法二:手動(注意權限,這是 ssh-copy-id 幫你做掉的部分)
cat ~/.ssh/id_ed25519.pub | ssh alice@server \
  "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

⚠️ 手動方法用 >>(附加)而非 >(覆寫)。用錯會清掉該帳號原有的所有金鑰,包括自己現在正在用的那把。

沒有密碼可登入時(例如雲端 VM),改從平台的 Console/metadata 介面貼入公鑰。

步驟 3:驗證

ssh alice@server
# 不需輸入伺服器密碼即代表成功(若金鑰有 passphrase,會問 passphrase,那是本地解鎖,不是伺服器密碼)

分不清「問的是 passphrase 還是伺服器密碼」時看提示字:

Enter passphrase for key '/home/alice/.ssh/id_ed25519':   ← 本地私鑰解鎖,金鑰認證有生效
alice@server's password:                                   ← 伺服器密碼,代表金鑰認證沒過

SSH Config 客戶端設定

~/.ssh/config 讓連線參數固定下來,不必每次打長指令。

# ~/.ssh/config

# 全域預設(放最前面;SSH 採「先出現者優先」)
Host *
    AddKeysToAgent yes
    ServerAliveInterval 60
    ServerAliveCountMax 3

# 生產環境
Host prod
    HostName 192.168.1.100
    User admin
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes

# 經跳板機連內網機器
Host internal
    HostName 10.0.0.50
    User admin
    IdentityFile ~/.ssh/id_ed25519_work
    ProxyJump bastion

Host bastion
    HostName bastion.company.com
    User bastion-user
    IdentityFile ~/.ssh/id_ed25519_work

# 多個 GitHub 帳號:用不同 Host 別名區分
Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes

Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal
    IdentitiesOnly yes

使用:

ssh prod
ssh internal                                       # 自動經 bastion 跳轉
git clone git@github-work:company/repo.git         # 走工作金鑰
git clone git@github-personal:alice/side-project.git # 走個人金鑰

常用選項

選項 說明
HostName 實際主機位址(Host 只是別名)
User 登入身分——同時決定伺服器讀哪個家目錄
IdentityFile 指定私鑰路徑
IdentitiesOnly 用指定的金鑰,不把 agent 裡其他金鑰全丟出去試
Port SSH 埠
ProxyJump 跳板機(取代舊的 ProxyCommand ssh -W
AddKeysToAgent 首次使用後自動加入 ssh-agent
ServerAliveInterval 閒置保活間隔(秒),避免 NAT/防火牆切斷
ForwardAgent 轉發 agent——有風險,見下方

IdentitiesOnly yes 為什麼重要

沒有它時,客戶端會把 agent 裡所有金鑰依序送出去試。金鑰一多就撞上伺服器的 MaxAuthTries(預設 6),出現 Too many authentication failures——即使正確的那把就在清單裡,只是排太後面。

多把金鑰的環境建議每個 Host 都加上 IdentitiesOnly yes

ForwardAgent 的風險

ForwardAgent yes 讓遠端主機能透過你的 agent 使用你的私鑰簽名。若該主機被入侵,攻擊者在你連線期間可以冒用你的身分登入任何你有權限的機器(私鑰本身不會外流,但簽名能力等同借出去了)。

# ❌ 避免全域開啟
Host *
    ForwardAgent yes

# ✅ 改用 ProxyJump——跳板機不會拿到你的簽名能力
Host internal
    ProxyJump bastion

確認實際生效的設定:

ssh -G prod | grep -iE "hostname|user|identityfile|port"

authorized_keys 進階:限制單把金鑰的權限

authorized_keys 每一行可在公鑰前面加上選項,限制該把金鑰能做什麼。這是自動化場景的重要工具。

# 一般:完整登入權限
ssh-ed25519 AAAAC3Nza...FO4c alice@laptop

# 限制來源 IP
from="203.0.113.10,10.0.0.0/8" ssh-ed25519 AAAAC3Nza...FO4c ci-runner

# 強制只能執行特定指令(無論客戶端下什麼指令)
command="/usr/local/bin/backup.sh",restrict ssh-ed25519 AAAAC3Nza...FO4c backup-job

# 只做 port forwarding,不給 shell
restrict,permitopen="127.0.0.1:5432" ssh-ed25519 AAAAC3Nza...FO4c db-tunnel
選項 作用
restrict 關閉所有轉發與 pty(OpenSSH 7.2+);建議作為預設起點,再逐項放行
command="..." 強制執行指定指令,忽略客戶端要求
from="pattern" 限制來源位址
permitopen="host:port" 允許轉發到特定目標
no-agent-forwarding 禁止 agent 轉發
expiry-time="20261231" 金鑰到期時間(OpenSSH 8.2+)

典型用途:CI 部署金鑰只允許執行部署腳本、備份主機只能拉取資料、資料庫 tunnel 帳號拿不到 shell。相較「給完整權限再靠應用層自律」,這是在 sshd 層就擋掉。

💡 command= 搭配 restrict 是最小權限原則的實作:即使私鑰外洩,攻擊者也只能觸發那支腳本。


ssh-agent 與 passphrase

passphrase 是什麼

passphrase 是用來加密「私鑰檔案本身」的密碼,保護對象是本機磁碟上的那個檔案,與伺服器無關。

passphrase  ──保護──>  ~/.ssh/id_ed25519(私鑰檔案)
私鑰        ──證明──>  伺服器上某個帳號的登入權

最常見的混淆是把它和伺服器密碼當成同一件事:

保護什麼 會不會送到伺服器
伺服器密碼 帳號的登入權 會,送到伺服器驗證
passphrase 磁碟上的私鑰檔案 不會,永遠留在本機

設了 passphrase 之後,私鑰在磁碟上是加密狀態(即前面 金鑰長相 提到的 aes256-ctr / bcrypt 標記)。需要簽名時,ssh 先用 passphrase 在記憶體中解開私鑰再簽——伺服器不會知道、也不需要知道你的私鑰有沒有加密。

為什麼要設:私鑰檔案等同帳號存取權。筆電遺失、備份外洩、~/.ssh 誤 commit 進 Git——這些情況下 passphrase 是最後一道防線;沒設的話,拿到檔案的人可直接登入所有部署過該公鑰的機器。

命名是有意的:passphrase 面對的是「攻擊者已取得檔案」的離線暴力破解,沒有伺服器限制嘗試次數,可用 GPU 全速猜測。因此長度比符號複雜度更重要,一個好記的長詞組優於 P@ssw0rd!ssh-keygen -a 100 提高 KDF 迭代次數,就是在拉高每次猜測的成本。

# 幫既有金鑰加上/變更/移除 passphrase
ssh-keygen -p -f ~/.ssh/id_ed25519

# 忘記有沒有設?有設就會要求輸入
ssh-keygen -y -f ~/.ssh/id_ed25519

💡 ssh-keygen -p 只是重新加密同一把私鑰,金鑰對本身不變,因此伺服器上的 authorized_keys 完全不用動,也不需重新部署公鑰。

用 ssh-agent 免去重複輸入

金鑰設了 passphrase,每次連線都要輸入很煩;ssh-agent 把解鎖後的私鑰留在記憶體中,一個工作階段只需輸入一次。

這兩件事是配套的:只有 passphrase 而沒有 agent,體驗差到多數人乾脆不設 passphrase,安全性等於歸零。

# 啟動 agent(多數桌面環境已自動啟動)
eval "$(ssh-agent -s)"

# 載入私鑰(此時輸入 passphrase)
ssh-add ~/.ssh/id_ed25519

# 查看已載入的金鑰
ssh-add -l

# 清空(離開座位或工作結束時)
ssh-add -D

macOS:存進 Keychain 免重複輸入

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

搭配 ~/.ssh/config 讓它每次自動載入:

Host *
    UseKeychain yes
    AddKeysToAgent yes
    IdentityFile ~/.ssh/id_ed25519

Linux:設定有效期限

# 載入後 8 小時自動移除
ssh-add -t 8h ~/.ssh/id_ed25519

⚠️ 在 ~/.bashrc 無條件寫 eval "$(ssh-agent -s)" 會讓每開一個終端機就多一個 agent 行程。多數發行版的桌面工作階段已提供 agent,先確認 echo $SSH_AUTH_SOCK 有值再說。

沒有 passphrase 的金鑰算不算安全?

取捨很明確:

  • 有 passphrase:私鑰檔案被複製走也不能直接用,攻擊者還要破 passphrase
  • 無 passphrase:檔案 = 存取權,但可用於無人值守的自動化

實務折衷:人用的金鑰一律設 passphrase(配合 agent,體驗差異不大);自動化用的金鑰可不設,但要用 from= / command= 限制權限範圍,並縮小該金鑰能碰到的機器。


伺服器端 sshd 設定

設定檔位於 /etc/ssh/sshd_config(部分發行版會有 /etc/ssh/sshd_config.d/*.conf 覆寫,注意 Include 的順序)。

# 啟用公鑰認證(預設就是 yes)
PubkeyAuthentication yes

# 確認金鑰認證可用「之後」再關密碼登入
PasswordAuthentication no
KbdInteractiveAuthentication no

# 禁止 root 直接登入;若必須保留,用 prohibit-password(只允許金鑰)
PermitRootLogin no

# 限定可登入的帳號
AllowUsers alice deploy
# 或用群組
AllowGroups ssh-users

# 權限檢查(預設 yes,不要關)
StrictModes yes

# 認證嘗試次數
MaxAuthTries 6

套用流程——順序很重要

# 1. 先驗證語法,語法錯誤會導致 sshd 起不來
sudo sshd -t

# 2. 確認實際生效值符合預期
sudo sshd -T | grep -iE "passwordauthentication|permitrootlogin|pubkeyauthentication"

# 3. 重新載入
sudo systemctl reload sshd     # 部分發行版是 ssh.service

🚨 保留一條退路:改動認證相關設定時,保持目前這條 SSH 連線不要關,另開一個新視窗測試登入。reload 不會中斷既有連線,萬一設定有問題還能改回來。雲端 VM 則確認 Console/序列埠登入可用。


除錯方法

客戶端:ssh -vvv

ssh -vvv alice@server

在大量輸出中重點看這幾行:

debug1: Offering public key: /home/alice/.ssh/id_ed25519 ED25519 SHA256:6nOr...
   → 客戶端送出了這把金鑰

debug1: Server accepts key: /home/alice/.ssh/id_ed25519 ED25519 SHA256:6nOr...
   → 伺服器認得,接下來會要求簽名

debug1: Authentications that can continue: publickey,password
   → 這把被拒絕了,還可以再試其他方式

debug1: No more authentication methods to try.
Permission denied (publickey).
   → 所有金鑰都被拒

判斷方向

現象 指向
完全沒有 Offering public key 客戶端根本沒送金鑰 → IdentityFile 路徑錯或檔案不存在
Offering 但沒有 Server accepts 伺服器名單裡沒這把,或權限/StrictModes 擋掉
Too many authentication failures 金鑰太多把 → 加 IdentitiesOnly yes
卡在 Connecting to ... 網路/防火牆/埠號問題,還沒到認證階段

伺服器端:看 sshd 日誌

客戶端訊息刻意含糊(避免洩露帳號是否存在),真正的原因在伺服器日誌

# systemd 系統
sudo journalctl -u sshd -f

# Debian / Ubuntu
sudo tail -f /var/log/auth.log

# RHEL / CentOS / Rocky
sudo tail -f /var/log/secure

典型訊息與含義:

日誌訊息 原因
Authentication refused: bad ownership or modes for directory /home/alice StrictModes 擋下,權限/擁有者不對
Invalid user alice from ... 伺服器上沒有這個帳號
Connection closed by authenticating user alice ... [preauth] 客戶端試完所有金鑰都失敗
User alice not allowed because not listed in AllowUsers AllowUsers 擋下

提高日誌詳細程度

# /etc/ssh/sshd_config
LogLevel VERBOSE     # 會記錄用了哪把金鑰的指紋,便於追查是誰登入

VERBOSE 會把成功登入所用的金鑰指紋寫進日誌,是共用帳號情境下唯一能追出「這次是誰登入」的方法。


規模化:SSH 憑證與雲端代管金鑰

authorized_keys 的根本限制是沒有集中撤銷點:一把金鑰散到 50 台機器,撤銷就得清 50 個檔案。機器數量上去之後有兩條路。

方案 1:SSH 憑證(SSH Certificate)

引入一個 CA,由 CA 簽發有期限的使用者憑證;伺服器只需信任 CA,不必逐台登記個人公鑰。

# CA 簽發憑證給 alice,有效期 8 小時
ssh-keygen -s ca_key -I "alice@company" -n alice -V +8h alice_key.pub
# 產生 alice_key-cert.pub
# 伺服器端:/etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/ca.pub

好處:新人上線不必逐台部署公鑰;離職時停止簽發即可,既有憑證也會自然過期。代價是要維運 CA 與簽發流程。

方案 2:雲端平台代管

主流雲端已把這層做掉:

平台 機制
GCE Guest Agent 依 metadata 的 ssh-keys 寫入 authorized_keys;啟用 OS Login 則改走 AuthorizedKeysCommand,權限由 IAM 控制
AWS EC2 cloud-init 於首次開機把 key pair 公鑰寫入預設帳號;或改用 EC2 Instance Connect / SSM Session Manager
Azure VM Agent 佈建公鑰;或用 AAD 登入擴充功能

共同注意事項:這些環境下手動改 authorized_keys 可能被平台的同步機制覆寫,應該從平台的 metadata/IAM 層面操作。


常見問題

問題 1:Permission denied (publickey)

最常見的錯誤,按順序排查:

# 1. 客戶端到底送出了哪把金鑰?
ssh -v alice@server 2>&1 | grep -i "offering\|accepts"

# 2. 伺服器上那個帳號的名單裡有沒有這把?(用還能登入的方式進去查)
cat ~/.ssh/authorized_keys

# 3. 比對指紋是否一致
ssh-keygen -lf ~/.ssh/id_ed25519.pub

# 4. 檢查權限鏈(StrictModes)
ls -ld ~ ~/.ssh ~/.ssh/authorized_keys

# 5. 看伺服器日誌拿到真正的原因
sudo journalctl -u sshd -n 50

依經驗,成因分布大致是:公鑰放錯帳號的家目錄 > 權限/擁有者不對(StrictModes) > 送錯金鑰 > 伺服器改用 AuthorizedKeysCommand

問題 2:REMOTE HOST IDENTIFICATION HAS CHANGED!

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

這是主機金鑰對不上,不是你的金鑰有問題。 兩種可能:

  1. 伺服器重灌/重建,主機金鑰重新產生了(常見且無害)
  2. 真的遭遇中間人攻擊(少見但不能排除)

確認是情況 1 之後才清除舊記錄:

ssh-keygen -R server.example.com          # 移除該主機的舊記錄
ssh-keygen -R 192.168.1.100               # IP 也要清(known_hosts 兩者分開記)

⚠️ 不要養成無腦 StrictHostKeyChecking=no 的習慣——那等於永久放棄中間人攻擊的防護。

問題 3:首次連線的指紋確認要怎麼處理?

The authenticity of host 'server (192.168.1.100)' can't be established.
ED25519 key fingerprint is SHA256:xxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

正確做法是事先取得指紋來比對(向管理員索取,或從雲端 Console 的序列埠輸出讀取),而非直接按 yes。

在伺服器上查它自己的指紋:

ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

批次部署可預先寫入 known_hosts

ssh-keyscan -t ed25519 server.example.com >> ~/.ssh/known_hosts

ssh-keyscan 本身沒有驗證能力,它只是「先抓下來」;在不可信網路上仍需另外核對指紋。)

問題 4:如何撤銷某把金鑰的存取權限?

# 1. 找出目標行(用註解欄位辨識)
grep -n "bob@thinkpad" ~/.ssh/authorized_keys

# 2. 刪除該行
sed -i.bak '/bob@thinkpad/d' ~/.ssh/authorized_keys

# 3. 確認結果
cat ~/.ssh/authorized_keys

立即生效,但不會中斷已建立的連線。金鑰疑似外洩時要一併踢掉現有 session:

who                    # 找出登入中的 session
sudo pkill -u bob      # 或針對特定 pts 終止

而且要記得:這只清了這一台。金鑰外洩時必須逐台清理,或用 grep 掃過所有主機。

問題 5:換新電腦,金鑰要怎麼搬?

兩種做法:

# 做法 A:產生新金鑰(較安全,推薦)
# 1. 新電腦上產生新金鑰對
ssh-keygen -t ed25519 -C "alice@new-laptop"
# 2. 從舊電腦把新公鑰部署到各伺服器
ssh-copy-id -i ~/.ssh/id_ed25519_new.pub alice@server
# 3. 確認新金鑰可用後,從各伺服器移除舊公鑰

# 做法 B:搬移現有私鑰(方便但風險較高)
# 用加密方式傳輸,絕不透過 email/雲端硬碟/聊天軟體明文傳送
scp ~/.ssh/id_ed25519 new-machine:~/.ssh/
chmod 600 ~/.ssh/id_ed25519    # 在新機器上修正權限

做法 A 的好處是舊電腦遺失時,撤銷舊公鑰即可,不影響新電腦。

問題 6:Too many authentication failures

金鑰太多,還沒試到正確那把就超過 MaxAuthTries

# 臨時解法
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519_prod alice@server

# 根本解法:寫進 ~/.ssh/config
Host prod
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes

最佳實踐

1. 用 Ed25519,人用的金鑰設 passphrase

# ✅
ssh-keygen -t ed25519 -a 100 -C "alice@laptop"

# ❌ 沿用舊習慣產生 RSA 2048、且不設 passphrase
ssh-keygen -t rsa

2. 依用途分開金鑰,檔名要能辨識

# ✅ 看檔名就知道用途,撤銷時不會誤刪
~/.ssh/id_ed25519_work
~/.ssh/id_ed25519_personal
~/.ssh/id_ed25519_aws_prod

# ❌ 一把金鑰打天下:任何一處外洩,全部機器都要換
~/.ssh/id_rsa

分開的實際好處是爆炸半徑可控:GitHub 用的金鑰外洩,不會連帶影響生產伺服器。

3. 每個 Host 都加 IdentitiesOnly yes

避免把所有金鑰丟給對方試——既會撞上 MaxAuthTries,也等於向對方揭露你持有哪些金鑰。

4. 自動化金鑰用 restrict + command= 限制

# ✅ CI 部署金鑰只能觸發部署腳本
command="/usr/local/bin/deploy.sh",restrict,from="203.0.113.0/24" ssh-ed25519 AAAA... ci-deploy

# ❌ 給 CI 一把完整權限的金鑰
ssh-ed25519 AAAA... ci-deploy

5. 關閉密碼登入前,先確認金鑰可用

# ✅ 順序
# 1. 部署金鑰 → 2. 新開視窗測試登入成功 → 3. 才改 PasswordAuthentication no → 4. reload

# ❌ 先關密碼登入再測試 → 金鑰若有問題就把自己鎖在門外

6. 伺服器端開 LogLevel VERBOSE

共用帳號時,這是唯一能從日誌追出「哪把金鑰登入」的方式。

7. 私鑰備份要加密

# ✅ 加密後再存放
tar czf - ~/.ssh/id_ed25519 | gpg -c > ssh-key-backup.tar.gz.gpg

# ❌ 絕不要做的事
# - commit 進 Git(即使是私有 repo)
# - 存到未加密的雲端硬碟
# - 用 email/聊天軟體傳送
# - 放進容器映像檔或 CI 環境變數的明文

8. 機器數量上去就改用憑證或平台代管

authorized_keys 在十台以內好管,上百台之後撤銷成本會變成實際的安全漏洞。


總結

核心要點

  • SSH 金鑰認證的本質是挑戰/簽名驗證:私鑰簽名、公鑰驗簽,私鑰永遠不離開本地
  • 一次連線有兩組金鑰:主機金鑰(伺服器證明自己,對應 known_hosts)與使用者金鑰(你證明自己,對應 authorized_keys
  • 公鑰的落點由「登入身分」決定ssh alice@host~alice/.ssh/authorized_keys;root 是 /root/.ssh/;雲端環境則多由平台代寫
  • 公鑰是一整行(演算法 + Base64 + 註解),私鑰是 BEGIN OPENSSH PRIVATE KEY 區塊;從私鑰第二行可看出有沒有設 passphrase
  • 認證失敗第一順位懷疑:公鑰放錯帳號、權限/擁有者不對(StrictModes),真正原因在伺服器日誌而非客戶端訊息

快速參考

基本操作

ssh-keygen -t ed25519 -C "alice@laptop"          # 產生金鑰
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host   # 部署公鑰
ssh-keygen -lf ~/.ssh/id_ed25519.pub             # 看指紋
ssh-keygen -y -f ~/.ssh/id_ed25519               # 從私鑰導出公鑰
ssh -i ~/.ssh/id_ed25519 user@host               # 指定金鑰連線
ssh -vvv user@host                               # 除錯
ssh -G host                                      # 看實際生效的客戶端設定
sudo sshd -T                                     # 看實際生效的伺服器設定
ssh-keygen -R host                               # 清除舊主機金鑰記錄

檔案角色

檔案 位置 內容
id_ed25519 客戶端 私鑰,絕不外流
id_ed25519.pub 客戶端 公鑰,用來部署
authorized_keys 伺服器(登入帳號的家目錄) 允許登入的公鑰清單
known_hosts 客戶端 認得的伺服器主機公鑰
config 客戶端 連線別名與參數
sshd_config 伺服器 /etc/ssh/ 伺服器認證政策
ssh_host_*_key 伺服器 /etc/ssh/ 主機金鑰對

權限

路徑 權限
家目錄 不可 group/other 可寫
~/.ssh 700
私鑰 600
公鑰 644
authorized_keys 600

一句話原則

私鑰留在本機,公鑰進「要登入的那個帳號」的家目錄,權限太鬆一切免談。


建立日期:2025-10-28 最後更新:2026-07-28

🔗相關文章