SSH 系列文章


為什麼寫這篇

你把一台主機開到公網,SSH port 對外開放。你覺得沒人知道你的 IP,應該很安全。

錯。

打開 /var/log/auth.log 看看:

Apr 15 03:12:41 myserver sshd[12345]: Failed password for root from 185.220.101.34 port 44832 ssh2
Apr 15 03:12:43 myserver sshd[12346]: Failed password for root from 185.220.101.34 port 44833 ssh2
Apr 15 03:12:45 myserver sshd[12347]: Failed password for admin from 185.220.101.34 port 44834 ssh2
Apr 15 03:12:47 myserver sshd[12348]: Failed password for ubuntu from 45.133.1.72 port 38291 ssh2
Apr 15 03:12:49 myserver sshd[12349]: Failed password for test from 45.133.1.72 port 38292 ssh2
Apr 15 03:12:51 myserver sshd[12350]: Failed password for postgres from 92.255.85.135 port 52100 ssh2

這不是針對你。全世界有成千上萬的機器人 24 小時掃描每一個 IP 的 port 22,嘗試用常見帳號密碼登入。你的主機上線幾分鐘內就會開始被打。

這篇用六個真實攻擊情境,一步步建立你的 SSH 防禦體系。


情境一:暴力破解密碼

😈 攻擊者做了什麼

攻擊者用自動化工具,對你的主機嘗試成千上萬組帳號密碼:

┌──────────┐     port 22      ┌──────────────┐
│  攻擊者   │ ──────────────→ │   你的主機    │
│  (機器人) │                  │              │
│           │  root / 123456   │  SSH Server  │
│           │  admin / admin   │              │
│           │  ubuntu / ubuntu │  等著被猜...  │
│           │  test / test     │              │
│           │  ...重複幾萬次   │              │
└──────────┘                  └──────────────┘

🔍 你怎麼發現的

# 查看失敗登入記錄
grep "Failed password" /var/log/auth.log | tail -20

# 統計哪些 IP 在攻擊你
grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10
# 輸出範例:
#   4523 185.220.101.34
#   2891 45.133.1.72
#   1205 92.255.85.135

# 統計攻擊者嘗試了哪些帳號
grep "Failed password" /var/log/auth.log | awk '{print $9}' | sort | uniq -c | sort -rn | head -10
# 輸出範例:
#   8901 root
#   2341 admin
#   1523 ubuntu
#    892 test
#    456 postgres

🛡️ 防禦:禁止密碼登入

既然你已經設定好金鑰認證(參考第一篇),直接關掉密碼登入:

sudo nano /etc/ssh/sshd_config
# 禁止密碼登入(只允許金鑰)
PasswordAuthentication no

# 禁止空密碼
PermitEmptyPasswords no

# 禁止 challenge-response 認證(某些系統的密碼登入後門)
ChallengeResponseAuthentication no
# 或在較新的系統上
KbdInteractiveAuthentication no
# 重啟 SSH 服務
sudo systemctl restart sshd

✅ 驗證防禦有效

# 嘗試用密碼登入(應該直接被拒絕)
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no username@your-server
# 預期結果:Permission denied (publickey).

# 用金鑰登入(應該正常)
ssh username@your-server

⚠️ 重要:修改 sshd_config 之前,確保你的金鑰登入是正常的!否則你會把自己鎖在外面。建議先開兩個 SSH session,一個改設定,另一個保持連線用來救援。


情境二:攻擊者都先試 root

😈 攻擊者做了什麼

從上面的日誌可以看到,root 被嘗試的次數最多。原因很簡單:每一台 Linux 都一定有 root 帳號,而且 root 有最高權限,打下來就是全拿。

攻擊者的字典檔:
  root:root
  root:123456
  root:password
  root:admin
  root:toor
  root:root123
  ...(幾十萬組)

🛡️ 防禦:禁止 root 登入 + 建立專用帳號

sudo nano /etc/ssh/sshd_config
# 完全禁止 root 透過 SSH 登入
PermitRootLogin no

# 或者只允許 root 用金鑰登入(但建議直接禁止)
# PermitRootLogin prohibit-password

建立專用帳號 + sudo 權限

# 建立新帳號
sudo adduser deploy

# 加入 sudo 群組
sudo usermod -aG sudo deploy

# 設定金鑰(切換到新帳號)
sudo su - deploy
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 把你的公鑰貼到這裡
nano ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# 重啟 SSH
sudo systemctl restart sshd

✅ 驗證

# root 登入應該被拒絕
ssh root@your-server
# Permission denied.

# 用新帳號登入,需要 root 權限時用 sudo
ssh deploy@your-server
sudo apt update  # 正常運作

情境三:Port 22 被全網掃描

😈 攻擊者做了什麼

網路上有大量的掃描器(如 Shodan、Masscan)24 小時掃描整個 IPv4 地址空間,找出開放 port 22 的主機。掃完就交給暴力破解工具自動攻擊。

掃描器掃全網 port 22
         │
         ▼
┌─ 1.0.0.1    → port 22 關閉 → 跳過
├─ 1.0.0.2    → port 22 開放 → 加入攻擊清單 ✓
├─ 1.0.0.3    → port 22 關閉 → 跳過
├─ ...
└─ 你的 IP    → port 22 開放 → 加入攻擊清單 ✓

🛡️ 防禦一:改 SSH Port

sudo nano /etc/ssh/sshd_config
# 改用非標準 port
Port 2222
# 可以選 1024-65535 之間的任意 port
# 常見選擇:2222, 22222, 或隨機的高位 port
sudo systemctl restart sshd
# 之後連線要指定 port
ssh -p 2222 username@your-server

改 port 不是真正的安全措施(security through obscurity),但它能擋掉 99% 的自動化掃描機器人。搭配其他防禦才有效。

🛡️ 防禦二:防火牆限制來源 IP

# 使用 ufw(Ubuntu 預設防火牆)
sudo ufw default deny incoming
sudo ufw default allow outgoing

# 只允許特定 IP 連 SSH
sudo ufw allow from 203.0.113.10 to any port 2222 proto tcp

# 或允許整個公司網段
sudo ufw allow from 10.0.0.0/8 to any port 2222 proto tcp

# 開放其他必要服務
sudo ufw allow 80/tcp    # HTTP
sudo ufw allow 443/tcp   # HTTPS

# 啟用防火牆
sudo ufw enable
sudo ufw status

🛡️ 防禦三:fail2ban 自動封鎖

fail2ban 會監控登入日誌,發現某個 IP 連續失敗就自動封鎖:

# 安裝
sudo apt install fail2ban

# 建立自訂設定(不要直接改 jail.conf)
sudo nano /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log

# 10 分鐘內失敗 3 次就封鎖
maxretry = 3
findtime = 600

# 封鎖 1 小時
bantime = 3600

# 累犯加重封鎖(fail2ban 0.11+)
bantime.increment = true
bantime.factor = 2
# 第一次 1hr → 第二次 2hr → 第三次 4hr → ...
# 啟動
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# 查看目前被封鎖的 IP
sudo fail2ban-client status sshd

✅ 驗證

# 查看 fail2ban 狀態
sudo fail2ban-client status sshd
# Status for the jail: sshd
# |- Filter
# |  |- Currently failed: 2
# |  |- Total failed:     156
# |  `- File list:        /var/log/auth.log
# `- Actions
#    |- Currently banned: 3
#    |- Total banned:     47
#    `- Banned IP list:   185.220.101.34 45.133.1.72 92.255.85.135

# 手動解鎖某個 IP(不小心把自己鎖住時)
sudo fail2ban-client set sshd unbanip 203.0.113.10

情境四:私鑰不小心外洩

😈 發生了什麼

# 新手常見的悲劇
cd my-project
git add .                    # 不小心把 .ssh 目錄加進去了
git commit -m "init"
git push origin main         # 私鑰推到 GitHub 了!

# 或者
scp ~/.ssh/id_ed25519 colleague@shared-server:~/   # 把私鑰傳給同事

GitHub 上有自動化工具在掃描每一個新 commit,找出 SSH 私鑰和其他 secret。推上去幾分鐘內就會被發現。

🛡️ 防禦:多層保護

第一層:私鑰加 passphrase

# 產生金鑰時設定 passphrase
ssh-keygen -t ed25519 -C "ct@company.com"
# 在提示時輸入 passphrase(不要留空)

# 已有的金鑰補上 passphrase
ssh-keygen -p -f ~/.ssh/id_ed25519

# 搭配 ssh-agent 就不用每次都輸入
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519   # 輸入一次 passphrase
# 之後這個 session 都不用再輸入

即使私鑰被偷,沒有 passphrase 也解不開。

第二層:.gitignore 預防

# 全域 .gitignore
echo ".ssh/" >> ~/.gitignore_global
git config --global core.excludesfile ~/.gitignore_global

# 專案的 .gitignore
echo "*.pem" >> .gitignore
echo "*.key" >> .gitignore
echo "id_*" >> .gitignore

第三層:外洩後的緊急處理 SOP

# 1. 立刻產生新金鑰
ssh-keygen -t ed25519 -C "ct@company.com-new" -f ~/.ssh/id_ed25519_new

# 2. 到所有主機上替換 authorized_keys
#    移除舊公鑰,加入新公鑰

# 3. 到 GitHub/GitLab 替換 SSH Key
#    Settings → SSH Keys → 刪除舊的,加入新的

# 4. 從 Git 歷史中清除私鑰
#    (即使刪除檔案,Git 歷史中還是看得到)
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch .ssh/id_ed25519" \
  --prune-empty -- --all
git push --force

# 5. 刪除舊金鑰
rm ~/.ssh/id_ed25519_old ~/.ssh/id_ed25519_old.pub

情境五:中間人攻擊 (MITM)

😈 攻擊者做了什麼

你第一次連到一台新主機,SSH 會問你:

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

大多數人直接打 yes。但如果攻擊者在你和真正的伺服器之間攔截了連線呢?

┌──────────┐          ┌──────────┐          ┌──────────┐
│  你的筆電 │ ───────→│  攻擊者   │ ───────→│  真正的   │
│          │          │ (假冒主機) │          │  伺服器   │
│  "yes"   │          │          │          │          │
│  打了密碼 │          │  記錄你的  │          │          │
│          │          │  所有操作  │          │          │
└──────────┘          └──────────┘          └──────────┘

🔍 known_hosts 是什麼

你打 yes 之後,SSH 會把那台主機的指紋記錄到 ~/.ssh/known_hosts。下次連線時會自動比對。

如果指紋突然變了,你會看到這個大警告:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!

這個警告是在保護你。不要無腦刪掉 known_hosts 來「解決」這個問題。

🛡️ 防禦:正確驗證主機指紋

# 情況一:你確定主機重灌了或 SSH key 換了
# 先跟管理員確認新的指紋,再移除舊紀錄
ssh-keygen -R server.example.com

# 情況二:你第一次連線,想確認指紋
# 請管理員在伺服器上執行:
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub
# 把輸出的指紋傳給你,你比對後再輸入 yes

# 情況三:你不確定為什麼指紋變了
# 不要連線!先聯絡管理員確認

在 sshd_config 中強化

# 只使用 ed25519 host key(更安全)
HostKey /etc/ssh/ssh_host_ed25519_key

# 移除較弱的 host key
# HostKey /etc/ssh/ssh_host_rsa_key      ← 註解掉
# HostKey /etc/ssh/ssh_host_ecdsa_key    ← 註解掉

情境六:進階防護

Port Knocking — 先敲門暗號才開 SSH

預設狀態:SSH port 完全關閉,掃描器看不到

你(知道暗號):
  1. 連 port 7000  ← 第一敲
  2. 連 port 8000  ← 第二敲
  3. 連 port 9000  ← 第三敲
  → 防火牆自動對你的 IP 開放 SSH port 30 秒
  → 你趕快 SSH 連進去
  → 30 秒後 SSH port 再次關閉
# 安裝 knockd
sudo apt install knockd

# 設定 /etc/knockd.conf
[options]
    UseSyslog

[openSSH]
    sequence    = 7000,8000,9000
    seq_timeout = 5
    command     = /sbin/iptables -A INPUT -s %IP% -p tcp --dport 2222 -j ACCEPT
    tcpflags    = syn

[closeSSH]
    sequence    = 9000,8000,7000
    seq_timeout = 5
    command     = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 2222 -j ACCEPT
    tcpflags    = syn
# 客戶端敲門
knock your-server 7000 8000 9000
# 然後立刻連線
ssh -p 2222 username@your-server

# 離開後關門
knock your-server 9000 8000 7000

雙因素認證 (2FA) — Google Authenticator

# 安裝
sudo apt install libpam-google-authenticator

# 以你的帳號執行設定
google-authenticator
# 會產生 QR code,用手機的 Google Authenticator app 掃描
# 回答幾個問題(建議都選 yes)
# 設定 PAM
sudo nano /etc/pam.d/sshd
# 加入這行:
auth required pam_google_authenticator.so

# 設定 sshd_config
sudo nano /etc/ssh/sshd_config
ChallengeResponseAuthentication yes
# 或較新的系統:
KbdInteractiveAuthentication yes

# 同時要求金鑰 + 2FA
AuthenticationMethods publickey,keyboard-interactive

sudo systemctl restart sshd

登入流程變成

SSH 連線
  │
  ├─ 第一關:金鑰認證(自動通過)
  │
  └─ 第二關:Verification code: ______
              輸入手機 App 上的 6 位數驗證碼
              │
              └─ 驗證通過 → 登入成功

完整的 sshd_config 防護範本

把以上所有防禦整合在一起:

# /etc/ssh/sshd_config

# === 基本設定 ===
Port 2222                              # 改掉預設 port
ListenAddress 0.0.0.0                  # 監聽所有介面(或指定內網介面)

# === 認證設定 ===
PermitRootLogin no                     # 禁止 root 登入
PasswordAuthentication no              # 禁止密碼登入
PermitEmptyPasswords no                # 禁止空密碼
PubkeyAuthentication yes               # 允許金鑰認證
AuthorizedKeysFile .ssh/authorized_keys

# === 安全強化 ===
MaxAuthTries 3                         # 最多嘗試 3 次
MaxSessions 5                          # 最多 5 個 session
LoginGraceTime 30                      # 30 秒內要完成認證
ClientAliveInterval 300                # 5 分鐘沒動作就斷線
ClientAliveCountMax 2                  # 2 次沒回應就踢掉

# === 限制使用者 ===
AllowUsers deploy admin                # 只允許這些帳號 SSH 登入
# 或用群組限制
# AllowGroups ssh-users

# === 停用不需要的功能 ===
X11Forwarding no                       # 不需要 GUI 就關掉
AllowTcpForwarding no                  # 不需要 tunnel 就關掉
# (如果你需要 tunnel,改成 yes)

# === Host Key ===
HostKey /etc/ssh/ssh_host_ed25519_key  # 只用最安全的演算法

# === 日誌 ===
LogLevel VERBOSE                       # 記錄詳細日誌
# 檢查設定語法
sudo sshd -t
# 沒有輸出 = 語法正確

# 重啟生效
sudo systemctl restart sshd

防禦層級總整理

由外到內,每一層都在擋不同的攻擊:

第一層:改 Port
  → 擋掉 99% 自動掃描機器人
    │
第二層:防火牆(ufw)
  → 限制誰能碰到你的 SSH port
    │
第三層:fail2ban
  → 自動封鎖暴力破解的 IP
    │
第四層:禁止密碼登入
  → 沒有金鑰 = 完全進不來
    │
第五層:禁止 root 登入
  → 猜到帳號名稱的難度大增
    │
第六層:2FA
  → 即使私鑰被偷也還有一道防線
    │
第七層:Port Knocking
  → SSH port 根本看不到

不是每一層都要做。最低限度建議前五層,2FA 和 Port Knocking 視安全需求而定。


總結

SSH 資安的核心原則只有一個:減少攻擊面

  • 不需要密碼登入 → 關掉
  • 不需要 root 登入 → 關掉
  • 不需要所有人都能連 → 防火牆限制
  • 有人在暴力破解 → fail2ban 封鎖
  • 私鑰可能被偷 → passphrase + 2FA

先從最基本的做起,再根據你的環境需求逐步加強。

上一篇SSH 連線參數完整指南 — X11、Tunnel、跳板機等進階用法 下一篇SSH 實戰應用場景 — 咖啡廳連內網、翻牆、遠端 Demo 番外一Tailscale 入門 — SSH Tunnel 的現代替代方案 番外二Headscale 自架指南 — 無限裝置、DERP、多網段 ACL


參考資源