1. 初見SSH
SSH是一種協議標準,其目的是實現安全遠程登錄以及其它安全網絡服務。
SSH僅僅是一協議標準,其具體的實現有很多,既有開源實現的OpenSSH,也有商業實現方案。使用范圍最廣泛的當然是開源實現OpenSSH。
2. SSH工作原理
在討論SSH的原理和使用前,我們需要分析一個問題:為什么需要SSH?
從1.1節SSH的定義中可以看出,SSH和telnet、ftp等協議主要的區別在于安全性。這就引出下一個問題:如何實現數據的安全呢?首先想到的實現方案肯定是對數據進行加密。加密的方式主要有兩種:
- 對稱加密(也稱為秘鑰加密)
- 非對稱加密(也稱公鑰加密)
所謂對稱加密,指加密解密使用同一套秘鑰。如下圖所示:
對稱加密的加密強度高,很難破解。但是在實際應用過程中不得不面臨一個棘手的問題:如何安全的保存密鑰呢?尤其是考慮到數量龐大的Client端,很難保證密鑰不被泄露。一旦一個Client端的密鑰被竊據,那么整個系統的安全性也就不復存在。為了解決這個問題,非對稱加密應運而生。非對稱加密有兩個密鑰:“公鑰”和“私鑰”。
兩個密鑰的特性:公鑰加密后的密文,只能通過對應的私鑰進行解密。而通過公鑰推理出私鑰的可能性微乎其微。
下面看下使用非對稱加密方案的登錄流程:
- 遠程Server收到Client端用戶TopGun的登錄請求,Server把自己的公鑰發給用戶。
- Client使用這個公鑰,將密碼進行加密。
- Client將加密的密碼發送給Server端。
- 遠程Server用自己的私鑰,解密登錄密碼,然后驗證其合法性。
- 若驗證結果,給Client相應的響應。
私鑰是Server端獨有,這就保證了Client的登錄信息即使在網絡傳輸過程中被竊據,也沒有私鑰進行解密,保證了數據的安全性,這充分利用了非對稱加密的特性。
這樣就一定安全了嗎?
上述流程會有一個問題:Client端如何保證接受到的公鑰就是目標Server端的?,如果一個攻擊者中途攔截Client的登錄請求,向其發送自己的公鑰,Client端用攻擊者的公鑰進行數據加密。攻擊者接收到加密信息后再用自己的私鑰進行解密,不就竊取了Client的登錄信息了嗎?這就是所謂的中間人攻擊
SSH中是如何解決這個問題的?
1. 基于口令的認證
從上面的描述可以看出,問題就在于如何對Server的公鑰進行認證?在https中可以通過CA來進行公證,可是SSH的publish key和private key都是自己生成的,沒法公證。只能通過Client端自己對公鑰進行確認。通常在第一次登錄的時候,系統會出現下面提示信息:
The authenticity of host 'ssh-server.example.com (12.18.429.21)' can't be established.
RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.
Are you sure you want to continue connecting (yes/no)?
上面的信息說的是:無法確認主機ssh-server.example.com(12.18.429.21)的真實性,不過知道它的公鑰指紋,是否繼續連接?
之所以用fingerprint代替key,主要是key過于長(RSA算法生成的公鑰有1024位),很難直接比較。所以,對公鑰進行hash生成一個128位的指紋,這樣就方便比較了。
如果輸入yes后,會出現下面信息:
Warning: Permanently added 'ssh-server.example.com,12.18.429.21' (RSA) to the list of known hosts.
Password: (enter password)
該host已被確認,并被追加到文件known_hosts中,然后就需要輸入密碼,之后的流程就按照圖1-3進行。
2.基于公鑰認證
在上面介紹的登錄流程中可以發現,每次登錄都需要輸入密碼,很麻煩。SSH提供了另外一種可以免去輸入密碼過程的登錄方式:公鑰登錄。流程如下:
Client端用戶TopGun將自己的公鑰存放在Server上,追加在文件authorized_keys中。Server收到登錄請求后,隨機生成一個字符串str1,并發送給Client。Client用自己的私鑰對字符串str1進行加密。將加密后字符串發送給Server。Server用之前存儲的公鑰進行解密,比較解密后的str2和str1。根據比較結果,返回客戶端登陸結果。
上述流程有誤,更正如下
- Client將自己的公鑰存放在Server上,追加在文件authorized_keys中。
- Server端接收到Client的連接請求后,會在authorized_keys中匹配到Client的公鑰pubKey,并生成隨機數R,用Client的公鑰對該隨機數進行加密得到pubKey(R)
,然后將加密后信息發送給Client。 - Client端通過私鑰進行解密得到隨機數R,然后對隨機數R和本次會話的SessionKey利用MD5生成摘要Digest1,發送給Server端。
- Server端會也會對R和SessionKey利用同樣摘要算法生成Digest2。
- Server端會最后比較Digest1和Digest2是否相同,完成認證過程。
在步驟1中,Client將自己的公鑰存放在Server上。需要用戶手動將公鑰copy到server上。這就是在配置ssh的時候進程進行的操作。下圖是GitHub上SSH keys設置視圖:
GitHub中SSH keys設置
3. SSH實踐
生成密鑰操作
經過上面的原理分析,下面三行命令的含義應該很容易理解了:
$ ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
$ chmod 0600 ~/.ssh/authorized_keys
ssh-keygen是用于生產密鑰的工具。
- -t:指定生成密鑰類型(rsa、dsa、ecdsa等)
- -P:指定passphrase,用于確保私鑰的安全
- -f:指定存放密鑰的文件(公鑰文件默認和私鑰同目錄下,不同的是,存放公鑰的文件名需要加上后綴.pub)
首先看下面~/.ssh中的四個文件:
- id_rsa:保存私鑰
- id_rsa.pub:保存公鑰
- authorized_keys:保存已授權的客戶端公鑰
- known_hosts:保存已認證的遠程主機ID(關于known_hosts詳情,見文末更新內容)
四個角色的關系如下圖所示:
需要注意的是:一臺主機可能既是Client,也是Server。所以會同時擁有authorized_keys和known_hosts。
登錄操作
# 以用戶名user,登錄遠程主機host
$ ssh user@host
# 本地用戶和遠程用戶相同,則用戶名可省去
$ ssh host
# SSH默認端口22,可以用參數p修改端口
$ ssh -p 2017 user@host
4 總結
本文以圖文方式對SSH原理進行解析(主要指遠程登錄,沒有涉及端口轉發等功能)。同時分析了非對稱加密的特性,以及在實踐過程中如何對加密操作進行改進。
5.========== 持續更新 ==========
- 感謝@李白走天涯、Dargonfly429的指正,圖1.5中認證流程有誤,下面是更正后的流程:
Server端在authorized_keys中匹配到Client的公鑰后,會生成隨機數R,并用Client的公鑰對該隨機數進行加密,然后將加密后信息發送給Client,Client端通過私鑰進行解密得到隨機數R,然后對隨機數R和本次會話的SessionKey利用MD5生成摘要Digest1,發送給Server端。Server端會也會對R和SessionKey利用同樣摘要算法生成Digest2,最后比較Digest1和Digest2是否相同,完成認證過程。
- 感謝 Michael2397評論,Client端的public key是Client手動Copy到Server端的,SSH建立連接過程中沒有公鑰的交換操作。另外圖1.5還需要添加一點,Server端根據什么信息在authorized_keys中進行查找的呢?主要是根據Client在認證的開始會發送一個KeyID給Server,這個KeyID會唯一對應該Client的一個PublicKey,Server就是通過該KeyID在authorized_keys進行查找對應的PublicKey。
===========2018-08-02 更新================
感謝@風笑天2013指正,下面關于SSH的known_hosts機制做如下更正:
1. known_hosts中存儲的內容是什么?
known_hosts中存儲是已認證的遠程主機host key,每個SSH Server都有一個secret, unique ID, called a host key。
2. host key何時加入known_hosts的?
當我們第一次通過SSH登錄遠程主機的時候,Client端會有如下提示:
Host key not found from the list of known hosts.
Are you sure you want to continue connecting (yes/no)?
此時,如果我們選擇yes,那么該host key就會被加入到Client的known_hosts中,格式如下:
# domain name+encryption algorithm+host key
example.hostname.com ssh-rsa AAAAB4NzaC1yc2EAAAABIwAAAQEA。。。
3. 為什么需要known_hosts?
最后探討下為什么需要known_hosts,這個文件主要是通過Client和Server的雙向認證,從而避免中間人(man-in-the-middle attack)攻擊,每次Client向Server發起連接的時候,不僅僅Server要驗證Client的合法性,Client同樣也需要驗證Server的身份,SSH client就是通過known_hosts中的host key來驗證Server的身份的。
這中方案足夠安全嗎?當然不,比如第一次連接一個未知Server的時候,known_hosts還沒有該Server的host key,這不也可能遭到中間人攻擊嗎?這可能只是安全性和可操作性之間的折中吧。
灰常感謝大家,希望收到更多的評論指正。