SSH 協議是事實上的互聯網基石之一。在 SSH 協議出現之前(1995 年由 Tatu Ylonen 設計),通過互聯網遠程登錄其他設備(telnet
)的過程是明文的。這意味着,整個通信過程,很容易被旁路嗅探,泄露敏感信息。
OpenSSH 是 SSH 協議的經典實現。目前,它作為各 Linux 發行版默認自帶的 SSH 工具分發;因而廣為使用。
若你在學習工作生活中,需要通過 SSH 協議,登錄、管理多台服務器。那么,一方面你可能會厭煩記憶各個遠程服務器的主機名/IP 地址;另一方面你可能會被繁瑣的口令輸入過程弄得煩躁不安。特別地,若是你需要在同一台遠程主機上,打開多個終端窗口進行操作時;你可能需要反復輸入多次用戶名和口令(在不用 screen
/tmux
之類的工具的前提下)。
這篇文章首先會嘗試介紹 Linux 自帶的 SSH 工具的基本用法,並附帶介紹 SSH 配置文件的編寫規則。而后,嘗試解決上述繁瑣的記憶和口令輸入命令。
SSH
我們說,在創造一件事情之前,你需要想清楚這件事情的意義是什么。通常而言,創造新事物的原動力,是我們發現了舊事物和我們目標之間存在的差距(gap);而新事物就是為了填上這一差距的。因此,講道理,我們首先應該去分析在 SSH 協議誕生之前人們使用的遠程登錄協議有哪些問題;而后去分析 SSH 分別是如何解決這些問題,填上所謂的 gaps 的。
然而,「空談誤國,實干興邦」。一方面在沒有任何實踐之前,大談理論是空洞的;另一方面,我們最終也是要將理論落實到時間上去。因此這一節,我們首先介紹/回顧 SSH 的最基本用法;而后討論 SSH 是如何解決過去遠程登錄協議遺留下來的問題的;借此,我們將討論 SSH 連接建立時會發生什么;介紹完這些內容之后,我們將介紹如何免密登錄。
SSH 的基本用法
Linux 下,ssh
命令的基本用法是
1 |
ssh [params] [flags] [user@]remotehost [command] |
先除去參數(params
)和標識(flags
)不論,以及不討論后續的執行命令(command
);ssh
命令的基本用法是 ssh [user@]remotehost
。若一切順利,如此就能成功以用戶 user
登錄遠程服務器 remotehost
。其中,若你本地登錄賬戶的用戶名與遠程服務器上的用戶名一致,則可以省略 [user@]
。
另外值得一提的是,這里說的 remotehost
指的是「邏輯上」的遠程主機。實際上,若是你嘗試執行下列命令,就能(利用本地回環)登錄你本機的 foo
賬戶了。此時,本機的主機名/IP 地址,就充當了 ssh
命令中邏輯上的遠程主機。
1 |
ssh foo@localhost # 127.0.0.0/24, ::1 |
127.0.0.0/24
都是本地回環的 IPv4 地址。只不過,因為127.0.0.1
是其中第一個合法的主機地址;所以被用來指代本地回環。但千萬不要以為只有127.0.0.1
這一個地址能夠本地回環。: )
SSH 怎樣防止信息泄露?
前文提到,SSH 提出的背景即是在它出現之前,遠程登錄協議無法保障通信安全。那么,在 SSH 出現之前,遠程登錄可能面臨哪些信息泄露風險呢?
首先是前文提到的旁路嗅探。事實上,這是網絡通信不可避免的問題;因為從我們本地主機到遠程主機的通信鏈路,是廣域網上通過 IP 協議路由實現的。在這一通信鏈路上,有太多我們無法控制的通信節點。因此,若是這其中但凡有一個節點被黑客控制,我們經由這條鏈路的通信信息,就可能被嗅探甚至篡改。
解決這一問題的根本辦法是使用加密的信道。這一辦法的思路在於,既然我們無法控制流量被嗅探/篡改,那么我們至少可以讓敵手得到的信息是難以破譯的密文(至少是破譯成本遠高於明文本身蘊含的價值)。SSH 協議正是這樣做的:它通過非對稱加密方法(公鑰加密方法),在預先交換公鑰的前提下,通信雙方通過對方的公鑰加密信息,而使用自身私鑰解開密文。如此一來,若是能保證密鑰交換的可信,則基於非對稱加密方案的加密信道就是安全的。
除了旁路嗅探,信息泄露的另一大風險來自所謂的中間人攻擊。中間人攻擊的源頭依然來自廣域網路由的不可控性。設想,在我們的主機和目標遠程主機的通信鏈路中間,有一個節點充當雙面間諜:一方面,它在鏈路中間截獲我們發出的信號,並偽裝成目標主機予以返回;另一方面,它在鏈路中間偽裝成我們的主機,轉發我們的流量,給真實的遠程主機。在這個過程中,如果沒有恰當的身份驗證手段,那么無論是我們的主機還是遠程主機,都無法驗證對方的身份。因此,事實上,一方面發起中間人攻擊的敵手可以獲取所有通信流量,另一方面它可以隨意篡改通信流量而難以發現。考慮到上述加密信道的可信性,一方面基於非對稱加密的安全性(在這里我們假設為 ground truth,不作懷疑),另一方面基於密鑰交換的可信性;那么,由於中間人攻擊可能在密鑰交換階段從中作梗,則若 SSH 協議不能妥善解決這一問題,則其安全性就仍然存疑。
這樣一來,問題實際上轉換成了密鑰交換過程的身份驗證問題。考慮到我們反復提及的廣域網上的通信鏈路是不可信的;僅憑借當前通信進行身份驗證,就變成了「雞生蛋、蛋生雞」的循環問題。因此,這類驗證不得不采用所謂的「盤外招」。
SSH 的思路的關鍵點在於:既然正常信道建立后,遠程主機需要將自己的公鑰發送給本地主機,那么這一公鑰本身就能看作是遠程主機的一個身份:若是無法驗證遠程主機的身份,那么本地主機使用這一公鑰進行信息加密是不安全的(因為公鑰可能來自敵手,而加密信息可能被敵手使用正確私鑰解密而竊取);若是驗證了遠程主機的身份,則這一公鑰就能放心地用來加密信息。因此,在 SSH 建立鏈接的過程中,它會要求本地主機的操作者確認遠程主機返回的公鑰的 hash 值。若這一 hash 值和操作者通過其他方式(盤外招)得到的值一致,則認可遠程主機的身份。當然,這一驗證不需要每次 SSH 連接時都進行——只需要驗證一次,而后交由本地計算機驗證 hash 與前次連接獲得的 hash 的一致性即可。而這又要求本地計算機將遠程主機的公鑰 hash 值保存下來,以便下次核對。
SSH 遠程登錄的流程
當本機發起登錄請求時,SSH 會依次執行以下幾個主要步驟:
- 通過遠程主機公鑰 hash,確認遠程主機身份;
- 若通過,遠程主機驗證登錄身份,例如:提示輸入遠程主機目標用戶的口令;
- 本地主機將用戶鍵入的口令,使用遠程主機的公鑰加密,並發送給遠程主機;
- 遠程主機使用上述公鑰對應的私鑰,對得到的密文進行解密;
- 遠程主機驗證解密后的口令;
- 若通過,則建立 SSH 連接,成功登錄。
前面已經說過,localhost
也可以充當邏輯上的遠程主機。這里我們就以 localhost
為例,驗證一下這一過程。
1 |
$ ssh sunsky@localhost |
執行 ssh sunsky@localhost
嘗試以 sunsky
的用戶身份登錄(邏輯上的)遠程主機 localhost
時,SSH 如我們預期一樣,提示我們驗證遠程主機的身份。這段文字翻譯如下。
無法驗證主機 'localhost (127.0.0.1)' 的真實性。ECDSA 密鑰指紋為
4d:28:ed:f1:3d:40:fe:68:c8:b3:b0:9b:a7:dc:5d:7e
。你是否要繼續連接?(yes/no)
標准的操作,我們必須通過額外的方式,與遠程主機取得聯系,驗證這一指紋是否真實。不過,此處我們略去這一步驟,鍵入 yes
。
1 |
$ ssh sunsky@localhost |
如前所述,為了下次自動地驗證遠程主機的身份,本地主機會將遠程主機的公鑰指紋保存下來。新出現的提示,翻譯如下。
警告:已將 'localhost' (ECDSA) 永久地加入已知主機列表之中。
在輸入遠程主機目標用戶的口令之后(無終端回顯),本地主機會將輸入的口令以遠程主機提供的密鑰加密並發送給遠程主機。待遠程主機解密並驗證通過后,即提示成功登錄。
上一次成功登錄:2017 年 9 月 12 日(周四)17:49:33,自
127.0.0.1
。
那么,具體來說,本地主機將這一信息保存在哪里了呢?答案是當前用戶的 ${HOME}/.ssh/known_hosts
文件當中。我們可以執行 exit
命令,退出遠程主機;而后使用 tail
命令可以查看剛剛插入在該文件末尾的遠程主機信息。
1 |
$ tail -1 ${HOME}/.ssh/known_hosts |
使用公鑰驗證身份
現在我們考慮下一個問題:除去輸入遠程主機用戶口令的方式,是否還有其他方式能夠驗證登錄者的身份?
對於身份認證來說,通常有三種手段:
- 你知道的(例如賬戶口令);
- 你獨有的(例如網銀的 U 盾);
- 你身上的(例如指紋)。
通常來說,對於沒有極端的安全性要求的場景,通過其一驗證即可。在上述登錄過程中,我們采取了「你知道的」這一手段來驗證登陸者的身份。考慮到,對於遠程登錄來說,很難通過生物信息識別來驗證身份;剩下可行的方案就是驗證「你獨有的」特殊物件來驗證身份了。
對於「你獨有的」這一手段來說,使用類似網銀的 U 盾顯然不現實。一則制作成本太高,二則相關的認證過於復雜。因此,我們須得考慮其他更易行的手段。
在 SSH 協議中,信道的安全是通過非對稱加密保證的。事實上,非對稱加密需要持有私鑰。因此,私鑰這件事情本身,也可以認為是一種「你獨有的」東西。考慮到,在 SSH 登錄成功之前,在不完整的信道中,從本地主機向遠程主機通信是安全的(因為有遠程主機的公鑰可用於加密),而遠程主機可以用持有的私鑰解密本地主機發來的信息。(例如口令登錄驗證的過程)。類似的過程也可以反過來用:
- 本地主機生成一對非對稱密鑰;
- 本地主機將公鑰交付遠程主機;
- 遠程主機在收到登錄請求時,使用上述公鑰加密一串無害的隨機信息;
- 本地主機將接收到的密文,以本地持有的私鑰解密,而后通過遠程主機的公鑰再進行加密;
- 遠程主機使用相應私鑰解密,並與上述隨機信息進行比對;
- 若一致,則認可登錄者的身份,許可登錄。
在這個過程中,遠程主機對比一來一回前后隨機信息的一致性,驗證了本地主機確實持有一個安全介質——本地主機生成的私鑰。因此,這就不需要輸入遠程主機的用戶口令了。
為此,我們首先需要生成一對密鑰。
1 |
$ ssh-keygen |
ssh-keygen
是 OpenSSH 的一部分,它用於生成供 SSH 使用的密鑰。默認情況下,ssh-keygen
生成的是 RSA 密鑰(本機上是 2048 位 RSA),並將私鑰保存在 ${HOME}/.ssh/id_rsa
當中。為了避免與已有的密鑰沖突,這里我們另存為 id_rsa.test
。隨后,ssh-keygen
要求我們為生成的私鑰設置口令(passphrase)。這一口令是對私鑰進行保護的口令,可以留空。這樣一來,我們就生成了一對 RSA 密鑰。其中,私鑰保存在 /home/test/.ssh/id_rsa.test
而公鑰保存在 /home/test/.ssh/id_rsa.test.pub
。
接下來,我們需要將生成的密鑰交付給遠程主機。為此,我們需要使用 ssh-copy-id
這一命令。
1 |
$ ssh-copy-id -i ~/.ssh/id_rsa.test sunsky@localhost |
ssh-copy-id
會將 ~/.ssh/id_rsa.test
對應的公鑰,交付給 sunsky@localhost
。在這個過程中,我們需要輸入用戶 sunsky
在遠程主機 localhost
上的口令。注意,此處我們使用了 -i
參數,指定了需要交付的密鑰。若是省略 -i
參數,則 ssh-copy-id
會將默認的密鑰 ~/.ssh/id_rsa
對應的公鑰交付給遠程主機。
之后,我們就可以「免密登錄」了。同樣,我們需要使用 -i
參數指定所需使用的私鑰。
1 |
$ ssh -i ~/.ssh/id_rsa.test sunsky@localhost |
類似上面提到過的 ~/.ssh/known_hosts
,保存這類公鑰也有一個特定的文件:遠程主機目標用戶的 ${HOME}/.ssh/authorized_keys
。登錄遠程主機后,我們可以使用 tail
命令來查看剛剛添加的公鑰。
1 |
$ tail -1 ~/.ssh/authorized_keys |
返回到本地主機,可見它正是我們剛剛生成的公鑰。
1 |
$ cat ~/.ssh/id_rsa.test.pub |
須得注意的是,出於安全性考慮,若要保證這一特性打開,遠程主機上的相關文件必須限制除當前用戶之外的權限。個人建議
.ssh
目錄權限必須不高於700
;且authorized_keys
文件權限必須不高於600
。
若是你的機器不支持 ssh-copy-id
,也可以直接將公鑰信息寫入遠程主機目標用戶的 ${HOME}/.ssh/authorized_keys
當中。
1 |
$ ssh sunsky@localhost 'mkdir -p .ssh && chmod 700 .ssh && cat >> .ssh/authorized_keys && chmod 600 .ssh/authorized_keys' < ~/.ssh/id_rsa.test.pub |
SSH 的配置文件
我們在文章開頭處提出了利用 SSH 登錄管理大量機器的兩個不便:
- 需要記憶大量機器的主機名;
- 需要記憶、輸入大量機器上的用戶口令。
在上一節中,我們通過在本地主機生成公鑰並交付遠程主機,利用「你獨有的」這條渠道完成了身份認證;從而避免了在登錄時輸入遠程主機用戶的口令。雖然,在示例中,我們不得不使用 -i
參數來指定希望使用的私鑰文件路徑。但這一方面是為了避免與本地主機當前用戶默認密鑰沖突,另一方面是為了演示這一參數的作用,再者也表明了本地主機用戶可以使用功能多個密鑰分別用於連接不同主機。
然而,盡管避免了輸入口令,但是「需要記憶大量主機名」的問題沒有解決;同時還引出了新的問題:需要使用 -i
參數指定私鑰路徑。怎樣解決這些問題呢?
SSH 的配置文件與用戶實際執行 ssh
命令時傳入的參數協同作用。按照優先級,低優先級的配置項可視作默認值;而高優先級的配置項則會覆蓋默認值。按優先級,有如下排序:
- 用戶實際執行
ssh
時傳入的參數; - 用戶的 SSH 配置文件
${HOME}/.ssh/config
; - 系統的 SSH 配置文件
/etc/ssh/ssh_config
。
這樣一來,通過 SSH 配置文件,我們可以按訪問的主機來配置 SSH 的默認行為。
SSH 配置文件的說明
SSH 的配置文件有很多配置項可供配置。限於篇幅,此處顯然是不可能窮盡的。因此,有興趣的讀者可以通過 man ssh_config
查看可用的配置項。
SSH 的配置文件采用空格分割的鍵值形式。例如 Host localhost
表示鍵 Host
對應的值為 localhost
。此篇涉及到的鍵如下:
Host
:值為通配符的模式(Pattern);該鍵之后的鍵值對,將用於匹配於該模式的主機。HostName
:值為真實的目標遠程主機名;在值中,%h
可用於命令行接收到的主機名字的轉義。User
:值為希望登錄的遠程主機的用戶名;IdentityFile
:值為希望登錄時使用的密鑰文件。
一個簡單的例子
這樣一來,我們可以在配置文件中寫入如下內容。
1 |
Host sun.test |
這樣一來,執行 ssh sun.test
就相當於執行了 ssh -i /home/test/.ssh/id_rsa.test sunsky@localhost
了。
1 |
$ ssh sun.test |
批量管理
現在我們構建這樣一個場景。我們有 1000 台服務器;它們的主機名編號從 w-i0.test.sh.localnet
一直到 w-i999.test.sh.localnet
。那么,為了免密以 cloud
用戶的身份登錄這 1000 台機器,我們可以首先將准備好的公鑰上傳到這 1000 台機器上;而后在 SSH 配置文件里配置如下內容。
1 |
Host i? i?? i??? |
這樣一來,我們就只需要使用 ssh i73
就能以 cloud
用戶的身份,登錄 sun.test.sh.localnet
了。
更多場景
在實際使用中,任何基於 SSH 之上的程序,都可以借助 SSH 配置文件達到簡化的目的。例如,代碼托管網站 bitbucket 支持通過 Git 來管理代碼。而 Git 又是支持 SSH 方式與遠程倉庫進行通信。這樣一來,我們就可以通過 SSH 配置文件,簡化對 bitbucket 的訪問;另一方面,通過 SSH 配置文件,我們可以將訪問 bitbucket 時使用的密鑰與其它密鑰區分開。
首先,我們使用 ssh-keygen
生成一對專用於 bitbucket 的密鑰;保存在 ~/.ssh/id_rsa.bitbucket
當中。而后,我們需要將 ~/.ssh/id_rsa.bitbucket.pub
中的內容,粘貼到 bitbucket 的賬戶設置中去。而后,我們可以在 SSH 配置文件中記錄:
1 |
Host bitb |
這樣一來,git clone git@bitb:foo/bar.git
就能克隆 bitbucket 上 foo
用戶的 bar
倉庫了(前提是你的賬戶對這個倉庫有訪問權限)。
特別說明,其中有一個問題是,要不要對私鑰設置口令(passphrase),如果擔心私鑰的安全,可以設置一個。運行結束以后,會在 ~/.ssh/ 目錄下新生成兩個文件:id_rsa.pub和id_rsa。前者公鑰,后者是私鑰。
常見問題:
1、生成密鑰並上傳至遠程主機后,仍然無法實現無密碼登錄?
打開遠程主機的 /etc/ssh/sshd_config 這個文件,以下幾行取消注釋。
#RSAAuthentication yes
#PubkeyAuthentication yes
#AuthorizedKeysFile .ssh/authorized_keys
然后,重啟遠程主機的ssh服務。
#RHEL/CentOS系統
$ service sshd restart
#ubuntu系統
$ service ssh restart
#debian系統
$ /etc/init.d/ssh restart
2、執行ssh-copy-id 命令時,遠程服務器的SSH服務端口不是22,如下:
$ ssh-copy-id nameB@machineB
ssh: connect to host machineB port 22: Connection refused
則使用如下命令:
$ ssh-copy-id "-p 22000 nameB@machineB"
3、ssh連接遠程主機時,出現 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED 警告。
分析原因:系統重裝、帳號信息修改等都會造成密鑰失效。
解決方法:刪除無效的密鑰,重新生成即可。
vi ~/.ssh/known_hosts
找到和遠程主機ip一致的密鑰,將其刪除即可。
補充內容:
$ ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.0.2
$ ssh-copy-id -u eucalyptus -i /home/eucalyptus/.ssh/id_rsa.pub eucalyptus@remote_host
#-u:給eucalyptus用戶設置無密碼登陸
#-i:當沒有值傳遞時或 ~/.ssh/identity.pub 文件不可訪問(不存在),ssh-copy-id將顯示如下錯誤
/usr/bin/ssh-copy-id: ERROR: No identities found
SSH提供兩種方式的登錄驗證:
1、密碼驗證:以服務器中本地系統用戶的登錄名稱,密碼進行驗證。
2、秘鑰對驗證:要求提供相匹配的秘鑰信息才能通過驗證。通常先在客戶機中創建一對秘鑰文件(公鑰和私鑰),然后將公鑰文件放到服務器中的指定位置。
注意:當密碼驗證和私鑰驗證都啟用時,服務器將優先使用秘鑰驗證。
SSH服務的配置文件:
sshd服務的配置文件默認在/etc/ssh/sshd_config,正確調整相關配置項,可以進一步提高sshd遠程登錄的安全性。
配置文件的內容可以分為以下三個部分:
#SSH服務器監聽的選項
#監聽的端口
Port 22
#使用SSH V2協議
Protocol 2
#監聽的地址為所有地址
ListenAdderss 0.0.0.0
#//禁止DNS反向解析
UseDNS no
#用戶登錄控制選項
#是否允許root用戶登錄
PermitRootLogin no
#是否允許空密碼用戶登錄
PermitEmptyPasswords no
#登錄驗證時間(2分鍾)
LoginGraceTime 2m
#最大重試次數
MaxAuthTries 6
#只允許user用戶登錄,與DenyUsers選項相反
AllowUsers user
#登錄驗證方式
#啟用密碼驗證
PasswordAuthentication yes
#啟用秘鑰驗證
PubkeyAuthentication yes
#指定公鑰數據庫文件
AuthorsizedKeysFile .ssh/authorized_keys
查看SSH服務狀態命令:/etc/init.d/sshd status
重新啟動SSH服務命令:/etc/init.d/sshd restart
查看ssh軟件的版本號命令:$ ssh -V
OpenSSH_3.9p1, OpenSSL 0.9.7a Feb 19 2003 #表明該系統正在使用OpenSSH
ssh: SSH Secure Shell 3.2.9.1 (non-commercial version) on i686-pc-linux-gnu #表明該系統正在使用SSH2
當遠程主機的公鑰被接受以后,它就會被保存在文件$HOME/.ssh/known_hosts之中。下次再連接這台主機,系統就會認出它的公鑰已經保存在本地了,從而跳過警告部分,直接提示輸入密碼。
每個SSH用戶都有自己的known_hosts文件,此外系統也有一個這樣的文件,通常是/etc/ssh/ssh_known_hosts,保存一些對所有用戶都可信賴的遠程主機的公鑰。
本文參考如下文章:
http://roclinux.cn/?p=2551
http://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html
http://blog.chinaunix.net/uid-26284395-id-2949145.html