22
sshd 監聽的連接埠。後面掛著 `scp`、`rsync`、走 SSH 的 `git`、各種通道,還有 SFTP——所以這裡被拒絕一次,會同時弄壞五件不同的事。
照你的系統挑一行貼上去執行。它會直接印出占著這個連接埠的行程,以及它綁在哪個位址上——通常看到這兩件事答案就出來了。
sudo lsof -nP -iTCP:22 -sTCP:LISTEN每個監聽中的 socket 一行:COMMAND、PID、USER,然後 NAME 放綁定位址。`*:22` 是每一張介面;`127.0.0.1:22` 只有回送位址——當通道端點時這是正當設定,而遠端用戶端只會逾時這件事,光靠它就解釋完了。`-n` 讓 lsof 不把位址解析成主機名稱,`-P` 讓它不要把 `22` 印成 `ssh`。`-sTCP:LISTEN` 會藏起已建立的工作階段,想列出現在誰連著就拿掉它。完全沒有輸出、lsof 以 1 結束,代表沒有東西占著它;在 macOS 上,那是你到系統設定裡打開遠端登入之前的正常狀態。不加 `sudo` 只看得到自己的行程,root 擁有的 daemon 會像不存在一樣。
sudo ss -tlnp 'sport = :22'`-t` 只看 TCP,`-l` 只看監聽中的 socket,`-n` 讓連接埠維持數字,`-p` 補上所屬行程;`sport = :22` 是 ss 對來源埠的過濾式。先讀 Local Address:Port——`0.0.0.0:22` 是每一張 IPv4 介面,`127.0.0.1:22` 只有回送位址,`[::]:22` 是 IPv6 萬用位址,多數 Linux 主機連 IPv4 也一起收。持有者會顯示成 `users:(("sshd",pid=812,fd=3))`,而在 SSH 被 socket activation 起來的主機上則是 `users:(("systemd",pid=1,fd=53))`,這是預期之內,不是可疑跡象。沒裝 `ss` 的話,net-tools 的寫法是 `sudo netstat -tlnp | grep ':22 '`,`-p` 會印出以斜線分隔的 pid/program;末尾那個空格留著,不然 `:2222` 也會命中。兩種寫法都要超級使用者權限才看得到不屬於你的行程,沒有的話那一欄就只是空著。
netstat -ano | findstr ":22"`-a` 列出連線和監聽中的連接埠,`-n` 讓位址和連接埠維持數字,`-o` 把所屬行程 ID 放在最後一欄。你要的那一行 State 是 LISTENING,其他都是用戶端的工作階段。用 `tasklist /FI "PID eq 812"` 把 PID 換成名字,這個過濾器吃 eq、ne、gt、lt、ge、le;在 Windows 上監聽者通常是選用功能 OpenSSH Server 帶來的 `sshd.exe`,而那個並非預設安裝。比對要小心:`findstr` 比的是子字串,所以 `":22"` 也會命中 `:2222`、`:22000` 和任何含 22 的臨時連接埠。動手之前,把 Local Address 那一欄整個讀完。
Get-NetTCPConnection -LocalPort 22 | Select-Object LocalAddress,LocalPort,State,OwningProcess它把連接埠當數字比對而不是子字串,所以不會像 `findstr` 那樣把 `:2222` 一起撈進來。`State` 是 `Listen` 而 `LocalAddress` 是 `0.0.0.0` 代表每一張介面,是 `127.0.0.1` 就只有本機。把 `OwningProcess` 餵給 `Get-Process -Id` 可以拿到執行檔名稱。結果是空的但綁定一直失敗,指向的是保留而不是占用——`netsh interface ipv4 show excludedportrange protocol=tcp` 會列出 Hyper-V、WSL2 和 Docker Desktop 開機時搶走的區段,落在那些範圍裡的連接埠誰都綁不了,也沒有 PID 可以怪。
`nmap -Pn -p 22 host.example.com` 回報的是這個連接埠從機器外面看起來的樣子,而 `-Pn` 直接當目標是活的,所以會丟掉 ping 的主機照樣掃得到。nmap 的定義很精確:`open` 是那裡真的有應用程式在接受連線;`closed` 是連接埠到得了也回了話,但沒有東西在監聽——sshd 停掉時就長這樣;`filtered` 是封包過濾讓探測封包到不了連接埠,所以 nmap 分不出 open 還是 closed,而那道防火牆正是害你的用戶端卡住、而不是乾脆快速失敗的元凶。`--reason` 會印出實際回來的東西,open 是 `syn-ack`,closed 是 `conn-refused`。`-sV` 讀的是版本橫幅,而 SSH 的橫幅就是協定在任何加密之前用明文送出的識別字串,長得像 `SSH-2.0-OpenSSH_9.6`。那串字也正是全網掃描器在收集的東西,所以請當成公開資訊看待。只掃你自己負責的主機。
| 你看到的訊息 | 代表什麼 |
|---|---|
ssh: connect to host host.example.com port 22: Connection refused | 主機活著也回話了——它的核心送了一個 reset,因為沒有任何行程占著連接埠 22。可能是 sshd 停了或啟動失敗、綁在一個不含你撥過去那個位址的地方,或是改完設定之後改聽別的連接埠了。這個失敗是瞬間回來的,而那個速度就是有用的線索:網路路徑是通的,缺的只有服務。請從主控台或另一條進得去的路,在那台機器上跑你系統對應的探測指令。 |
ssh: connect to host host.example.com port 22: Connection timed out | 這是相反的診斷,也正是這兩個值得分清楚的原因。什麼都沒回來:防火牆、雲端安全群組或網路 ACL 正安靜地丟掉你的 SYN,再不然就是你撥的那個位址已經不屬於那台機器了。macOS 會把同一件事講成 `Operation timed out`。你等的那幾十秒就是破綻——被拒絕會立刻回來,被丟掉則要一路坐到連線逾時。用 `nmap --reason` 確認,`filtered` 就是被丟掉。 |
Permission denied (publickey). | TCP、連接埠和 daemon 全都正常——你已經走到認證才失敗,而且這句話直接點名了伺服器唯一願意嘗試的方式。照順序來:先跑 `ssh -v`,讀 `Offering public key` 那幾行,看用戶端實際送出的是哪把金鑰;agent 裡塞了一堆金鑰時,可能還沒輪到對的那把就先耗光伺服器的嘗試次數上限,所以用 `-i` 搭 `IdentitiesOnly=yes` 把它釘死。接著看伺服器端,金鑰必須放在那個帳號的 `~/.ssh/authorized_keys` 裡。OpenSSH 的 `StrictModes` 預設是 yes,所以家目錄、`.ssh` 或 `authorized_keys` 只要是群組或所有人可寫,sshd 就會直接忽略那個檔案,而且不會告訴用戶端任何事——`~/.ssh` 設 700,`authorized_keys` 設 600,擁有者是那位使用者。真正的原因伺服器紀錄會用一行寫清楚,用戶端這邊則永遠不會。 |
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! ... Host key verification failed. | 伺服器出示的金鑰,跟 `known_hosts` 裡以那個名字記著的不一樣,而這是少數幾個「反射動作剛好是錯的」的錯誤。不要一上來就 `ssh-keygen -R`:那會刪掉你手上唯一一份「這台主機以前長什麼樣」的紀錄。無害的原因有好幾種——機器重建過、雲端位址被釋放後配給別人的執行個體、那個名字現在解析到另一個後端、或你是透過轉發的連接埠連到了另一台主機;還有一種不無害的,就是有人坐在你和伺服器中間。先把是哪一種確定下來:把 ssh 印出來的指紋,拿去跟在主控台上跑 `ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key` 得到的,或跟執行個體開機紀錄裡那份對照。`ssh-keygen -F host` 會告訴你存起來的那筆在哪裡、內容是什麼。確認過新的指紋之後,`ssh-keygen -R host` 才變成正確的指令。 |
error: Bind to port 22 on 0.0.0.0 failed: Address already in use. | 已經有東西占著那個 socket,所以 sshd 起不來。通常是舊的 sshd 還在跑——一次跟自己的關閉搶跑的 restart,或是一份手動編譯的裝置在跟套件管理的服務打架;也有 socket unit 占著 22、你又同時啟動了 service unit 的情況。用上面的指令把持有者找出來,不要用猜的。還有,沒有主控台或第二條進得去的路之前,不要在一個依賴著它的工作階段裡把正在跑的 sshd 停掉。 |
ssh_exchange_identification: Connection closed by remote host | TCP 連線是成功的,是 sshd 在送出協定橫幅之前就把你掛掉,所以連接埠、路由和防火牆全都沒事。常見原因有三個:fail2ban 之類的東西封鎖了你的來源位址;機器負載高或檔案描述子用盡時 `MaxStartups` 開始丟連線;以及 `AllowUsers`、`DenyUsers` 或 TCP wrappers 這類存取規則。這不是網路故障,是伺服器對你做了決定,而是哪一種,伺服器的紀錄會說。 |
IANA 把 22 指派給 `ssh`,也就是 Secure Shell 協定,TCP、UDP、SCTP 都登記了,實務上只有 TCP 在用。這個連接埠上的一個監聽者扛的遠不只互動式 shell:`scp`、跑在 SSH 上的 `rsync`、`git clone git@host:repo`、`ssh -L` 和 `-D` 的通道、`ProxyJump` 的跳板,還有 SFTP 子系統——所謂 SFTP 就只是這個,跟連接埠 21 的 FTP 一點關係都沒有。OpenSSH 文件寫明的預設值是 `sshd_config` 裡的 `Port 22`,綁哪幾張介面則由 `ListenAddress` 決定;這兩個指令都可以寫不只一次,所以一台伺服器同時在 22 和另一個連接埠上正常回應,是完全合理的。動手找行程之前先知道一件事:現在有好幾個發行版會附上 SSH 的 systemd socket unit,在 `ssh.socket` 是啟用單元的主機上,占著連接埠 22 的是 `systemd`,`sshd` 要等連線進來才存在。這是正常的,不代表有別的東西搶了它。把 SSH 從 22 搬走也不是改一行就好:改 `sshd_config` 的 `Port`、在主機防火牆開新的連接埠,SELinux 的系統還要再用 `semanage port -a -t ssh_port_t -p tcp 2222` 登記一次。少了最後這步,sshd 會啟動、把綁定失敗寫進紀錄,而你被鎖在一台從其他任何角度看都很健康的機器外面。新連接埠一定要在第一條連線還開著的時候,從第二個工作階段去測。
連接埠 22 是少數幾個「開出去講得過去」的管理用連接埠,因為傳輸是加密的,而且用戶端會在送出任何機密之前先驗證伺服器的主機金鑰。但那跟安全不是同一回事。22 上的監聽者幾小時內就會被發現,接著開始承受一股無趣但不會停的猜密碼流量,目標是 `root`、`admin`、`ubuntu`、`git` 還有另外幾百個名字。真正會改變結果的措施有三個:只用金鑰,並設 `PasswordAuthentication no`——這個選項在 OpenSSH 文件裡的預設值是 `yes`,而各發行版兩個方向都改過,所以請去讀正在跑的設定,不要用猜的;`PermitRootLogin` 最寬也只到文件預設的 `prohibit-password`,能設 `no`更好;再加一個像 fail2ban 這樣的速率限制,讓來源位址失敗幾次就被封鎖,它主要幫你換來的是看得懂的紀錄。把 daemon 搬到高位連接埠可以大幅減少雜訊,但幾乎沒有減少風險,因為針對性的掃描幾秒鐘就找得到。真正有效的是在防火牆上收緊來源位址、把整批機器藏在一台用 `ProxyJump` 走的跳板後面讓只有它在 22 上回應,或是乾脆要求先連 VPN。不管選哪一個,都要從一個已經登入的第二工作階段去驗證——每一次把自己鎖在門外,都是從「只用重新連線來確認的變更」開始的。
這些全都在你的瀏覽器裡跑,不會上傳任何東西。
查 22 的時候,通常也會順手看一下這幾個。
幾乎都是權限問題。lsof 和 ss 會列出 socket,但別人的行程它不會顯示所有者,所以由系統帳號、容器執行環境或 launchd 啟動的服務,在你補上 sudo 之前就只是一個沒有主人的監聽項。Windows 則是反過來:netstat 什麼都沒有,綁定卻還是失敗,那代表這個號碼落在 Hyper-V、WSL2 或 Docker Desktop 開機時預留的範圍裡。
先看清楚它是什麼。沒收乾淨的開發伺服器砍掉沒事;正在寫入的資料庫不行,其他東西依賴的系統服務也不行。如果持有者是容器,請停容器,不要砍主機看到的那個行程——砍了執行環境只會把它拉起來。遇到真的不能動的,把自己的服務換一個連接埠反而更快。
因為它綁在 127.0.0.1 而不是 0.0.0.0。每一頁的指令之所以會把綁定位址跟行程名一起印出來,就是為了這件事:綁在回送位址上的服務,不管防火牆怎麼設,都只有這台機器自己連得到。現在不少框架是刻意這樣預設的,所以通常是一個參數的問題,不是 bug。如果已經是 0.0.0.0 還是不通,下一個要懷疑的就是主機防火牆或雲端的安全群組。
幾乎沒有幫助。掃全網的機器持續在掃整段範圍,換號碼買到的是幾個小時,不是安全,代價卻是每個同事都得記住它。它確實能讓針對預設埠的無差別掃描少污染一點日誌,這算是實際但有限的好處。真正會改變結果的是認證、限制來源位址的防火牆,以及一開始就不要讓它對外。
連接埠 22 還是卡住嗎?看完整的連接埠對照表,或是回到上面,從你實際看到的那行錯誤往回追。