SSH 是每一臺電腦的標準配置,Linux 就不必說了,連 windows 也從 2018 年開始自帶 OpenSSH 了。
它主要的用途是登陸到遠程電腦中執行命令,在云開發的時代,它是每個程序每天都要用到的工具。本文將簡單介紹一下它的原理,
基本用法以及端口轉發和動態轉發等高階用法。
介紹
SSH 叫安全外殼協議(Secure Shell),是一種加密的網絡傳輸協議,可在不安全的網絡中網絡服務提供安全的傳輸環境。它通過在網絡中創建安全隧道來實現 SSH 客戶端和服務器之間的連接。最早的時候,互聯網通信都是明文通信,一旦被截獲,內容就會被暴露。1995年,芬蘭學者 Tatu Ylonen 設計了 SSH 協議,將登錄信息全部加密,成為互聯網安全的一個基本解決方案,迅速在全世界獲得推廣,目前已經成為所有操作系統的標準配置。
SSH 是一種協議, 存在多種實現,既有商業實現,也有開源實現(OSSH,OpenSSH)。本文使用的自有軟件 OpenSSH, 畢竟它是目前最流行的 SSH 實現,而且是所有操作系統的默認組件。
TIPS: OpenSSH發展史
1995 年 7 月, Tatu Ylonen 以免費軟件的形式將一套保護信息傳輸的程序(也就是 SSH )發布出去。程序很快流行,到年底已經有兩萬用戶,遍布五十國家。所以在年底時,他創立了 SSH 通信安全公司來繼續開發和銷售 SSH, 所以它變成了專有軟件。在 1999 年,瑞典程序員基于 SSH 最后一個開源的版本 1.2.12 開發了 OSSH,之后 OpenBSD 開發者在 OSSH 的基礎上進行大量修改,形成了 OpenSSH。它是目前唯一一種最流行的 SSH 實現,成為了所有操作系統的默認組件。
原理介紹
SSH 之所以一經提出,就得到了快速發展,是因為數據的安全性對任何人都非常重要。這里我們對其保護數據安全的原理進行探究。
在聊加密前先介紹一下幾個密碼學的基本概念:
- 明文
plaintext
指傳送方(一般指客戶端)想要接受方(一般指服務端)獲得的可讀信息 - 密文
ciphertext
指明文經過加密后所產生的信息 - 秘鑰
key
指用來完成加密、解密、完整性驗證等密碼學應用的密碼信息,是明文轉換為密文或密文轉換為明文的算法需要的參數 - 私鑰 指私有的秘鑰
- 公鑰 指公開的秘鑰
對稱加密
對稱加密就是加密或解密使用的是同一個秘鑰。比較常用的對稱加密算法有 AES,DES等。其具體的時序圖如下:
對稱加密的優點是加解密效率高,速度快。對于服務端而言,它和每個客戶端都要有一個秘鑰,龐大的客戶端數目導致秘鑰數目多,而且一旦機器被登錄,所有的秘鑰都泄露,所以缺點是秘鑰的管理和分發比較困難,不安全。
非對稱加密
非對稱加密需要一對秘鑰來進行加密和解密,公開的秘鑰叫公鑰,私有的秘鑰叫私鑰。注意公鑰加密的信息只有私鑰才能解開(加密過程),私鑰加密的信息只有公鑰才能解開(驗簽過程)。比較常用的非對稱加密算法有 RSA。其具體的時序圖如下:
非對稱加密的優點是安全性更高,秘鑰管理比較方便,每個服務器只要維護一對公私鑰即可。缺點是加解密耗時長,速度慢。不過對于現在的計算機而言,這點成本可以忽略不計。
中間人攻擊
中間人攻擊的英文全稱是 Man-in-the-middle attack,縮寫為 MITM。在密碼學和計算機安全領域中是指攻擊者與通訊的兩端分別創建獨立的聯系,并交換其所收到的數據,使通訊的兩端認為他們正在通過一個私密的連接與對方直接對話,但事實上整個會話都被攻擊者完全控制。在中間人攻擊中,攻擊者可以攔截通訊雙方的通話并插入新的內容。在許多情況下這是很簡單的(例如,在一個未加密的 Wi-Fi 無線接入點的接受范圍內的中間人攻擊者,可以將自己作為一個中間人插入這個網絡)。其具體的時序圖如下:
受到中間人攻擊的關鍵原因是客戶端不知道服務端的公鑰真假,服務端也不知道客戶端的公鑰真假。所以破解這個問題的關鍵是如何相互認證,也就是要像黃宏《開鎖》小品里一樣證明我就是我,你就是你。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ 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)!
It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:sYNNR1L6T5cSAG4BndqtdDhJEI0eB9LamBTkuIue3+0.
Please contact your system administrator.
Add correct host key in /Users/xx/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /Users/xx/.ssh/known_hosts:40
ECDSA host key for [xx.com] has changed and you have requested strict checking.
Host key verification failed.
基本用法
生成公鑰
ssh-keygen 是安全外殼( SSH )協議套件的標準組件,用于生成,管理和轉換身份驗證密鑰。
參數說明
- -b bits 指定要創建的秘鑰中的位數,默認 2048 位。值越大,密碼越復雜
- -C comment 注釋,在 id_rsa.pub 中末尾
- -t rsa/dsa等 指定要創建的秘鑰類型,默認為 RSA
- -f filename 指定公私鑰的名稱,會在
$HOME/.ssh
目錄下生產私鑰 filename 和公鑰 filename.pub - -N password 指定使用秘鑰的密碼,使得多人使用同一臺機器時更安全
常用命令
# 生成公私鑰,默認文件為 ~/.ssh/id_rsa
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
管理秘鑰
ssh-agent 和 ssh-add 是安全外殼(SSH)協議套件的標準組件,用于管理私鑰。一般情況下我們使用不帶密碼的 id_rsa 作為我們的默認私鑰,此時是沒必要啟動 ssh-agent 的。當我們碰到以下兩種情況則需要它:
- 使用不同的秘鑰連接到不同的主機時,需要手動指定對應的秘鑰。(ssh-agent 幫我們選擇對應的秘鑰進行認證)
- 當私鑰設置了密碼,而我們又需要頻繁的使用私鑰進行認證。(ssh-agent 幫我們免去重復輸入密碼)
代理常用命令
# 啟動代理
eval `ssh-agent`
# 關閉代理
ssh-agent -k
# 在 ~/.bashrc 中加入以下來實現登陸自動啟動 ssh-agent,退出自動 kill 掉程序
eval $(ssh-agent -s) > /dev/null
trap 'test -n "$SSH_AGENT_PID" && eval `/usr/bin/ssh-agent -k` > /dev/null' 0
# 查看代理中的私鑰
ssh-add -l
# 查看代理中私鑰對應的公鑰
ssh-add -L
# 移除指定的私鑰
ssh-add -d /path/of/key/key_name
# 移除所有的私鑰
ssh-add -D
發送公鑰
ssh-copy-id 是一個用來將公鑰放到服務器上的腳本。它通過 SSH 密碼登陸遠程服務器,并將指定的公鑰放到遠程服務器 $HOME/.ssh/authorized_keys
中。這個操作也可以先登陸到服務器中,然后通過 vi 等文本編輯命令向 $HOME/.ssh/authorized_keys
中加入允許登陸的公鑰。不過對于云服務器可以在啟動服務器時在頁面上操作綁定公鑰,這樣更安全些(阿里云和騰訊云默認關閉秘鑰登陸 PasswordAuthentication no
)。特別注意的是,千萬別在公共的網絡中通過密碼登陸遠程服務器,而秘鑰登陸沒有問題。
# 發送公鑰的兩種方式(等價)
ssh-copy-id -i ~/.ssh/id_rsa.pub user@host
ssh user@host 'mkdir -p .ssh && cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub
登錄
登錄配置
SSH 登陸服務器需要知道服務器的主機地址(主機名或主機 IP 地址),用戶名和密碼,有時還要指定端口號(默認 22 )。主機名還好,但是主機IP 地址就比較難記的,特別是當你可能要登錄十幾臺服務器時。一般我們使用的登陸命令如下:
# 登陸目標服務器( 172.17.132.120 )
ssh -p 58422 user@172.17.132.120
# 通過跳板機登陸目標服務器( 172.17.132.120 )
ssh -p 58422 user@jumper.example.com ssh user@172.17.132.120
# 端口映射
ssh -p 58422 user@jumper.example.com -fNL 5433:172.17.132.120:5432 -N
通過配置 $HOME/.ssh/config
可以使用以下命令來登錄。
# 登陸目標服務器( 172.17.132.120 )
ssh target
# 通過跳板機登陸目標服務器( 172.17.132.120 )
ssh jump_target
# 端口映射
## 登陸時通過 LocalForward 配置
ssh jump_target
## 使用-L來實現本地端口映射
ssh -C -N -g -L 5433:127.0.0.1:5432 jump_target
# 通用配置,所有配置都使用
Host *
AddKeysToAgent yes # 將私鑰添加到ssh-agent中
UseKeychain yes # 保存密碼到agent中
ServerAliveInterval 10 # 連接心跳間隔10s
ServerAliveCountMax 3 # 重連次數為3
# target配置
Host target
HostName 172.17.132.120
User user
Port 58422
IdentityFile ~/.ssh/id_rsa
# 跳板機配置
Host jumper
HostName jumper.example.com
User user
Port 58422
IdentityFile ~/.ssh/id_rsa
Host jump_target
HostName 172.17.132.120
User user
Port 22
IdentityFile ~/.ssh/id_rsa
ProxyCommand ssh user@jumper -W %h:%p 2>/dev/null
LocalForward 5433 localhost:5432 # 本地5433映射到jump_target的5432
TIPS:
VS Code 的 Remote 插件會讀取本地的配置文件$HOME/.ssh/config
,以便像本地一樣進行遠程開發。
首次登陸
一般在 $HOME/.ssh
目錄下除了公私鑰文件,config 配置文件,authorized_keys 認證文件外,還有一個 known_hosts 文件。
這個文件記錄了遠程主機 ip 和遠程主機對應的公鑰指紋。我們在第一次登陸(密碼或秘鑰登陸)服務器時,會有如下的提示界面:
### SSH 首次登陸的提示
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:HosOqhcUmbB7QG81yCuDPkvxTgot+vpple+czXPrEug.
ECDSA key fingerprint is MD5:fd:d7:e1:2c:42:4e:b4:2d:a3:21:4d:d1:c4:74:64:2d.
Are you sure you want to continue connecting (yes/no)?
此時 known_hosts 并沒有 127.0.0.1 這臺機器的指紋信息,所以顯示這個提示來讓我們確認這個指紋是否是目標機器的 ECDSA 算法的指紋。
當我們輸入 yes 確認后,在下次登錄的時候,遠程主機發送過來的公鑰指紋,直接和 known_hosts 文件中對應 ip 的公鑰指紋比較即可。
# 本機查看服務器 172.17.132.120 的所有公鑰(要與服務器上 /etc/ssh 下面的公鑰 *.pub 一致)
ssh-keyscan -p 22 172.17.132.120
# 查看服務器公鑰 ecdsa 的指紋 -E md5/sha256 指紋 hash 算法
ssh-keygen -E md5 -lf /etc/ssh/ssh_host_ecdsa_key.pub
## 256 MD5:84:3d:9c:6e:75:f2:6b:b2:0b:40:aa:d6:29:2f:b4:40 no comment (ECDSA)
## 256 SHA256:ZoGnph63gnKLC9wQYrHYVU8ROTf6+K9LKAjn+jrXB2o no comment (ECDSA)
# 從客戶端查看服務器公鑰 ecdsa 的指紋(初次登陸時要驗證的指紋)
ssh-keyscan -t ecdsa -p 22 172.17.132.120 |ssh-keygen -lf -
# 公鑰轉換成特定指紋 hash 算法的指紋
awk '{print $2}' /etc/ssh/ssh_host_ecdsa_key.pub | base64 -d|openssl sha256 -binary |base64
TIPS: known_hosts的重要性
known_hosts 這個文件是客戶端驗證服務端身份的重要依據。每次客戶端向服務端發起連接請求時,不僅服務端要驗證客戶端的合法性,客戶端也需要驗證服務端的身份。客戶端就是通過 known_hosts 中的公鑰指紋來驗證服務器是否發生了變化。它在一定程度上能避免中間人攻擊,除了第一次登陸,因為那時 known_hosts 中還沒有服務器的身份信息,所以對于首次提示的登陸指紋信息還是需要和服務器比對的。最安全保險的做法是第一次登陸就使用秘鑰登陸。
登陸流程
- 版本號協商階段
- 密鑰和算法協商階段
服務端和客戶端分別發送算法協商報文給對方,報文中包含自己支持的公鑰算法列表、加密算法列表、消息驗證碼算法列表、壓縮算法列表等。服務端和客戶端根據對方和自己支持的算法得出最終使用的算法。服務端和客戶端利用 DH 交換算法、主機密鑰對等參數,生成會話密鑰和會話 ID。 - 認證階段( publickey > gssapi-keyex > gssapi-with-mic > password )
- 會話請求階段
- 會話交互階段
密碼登陸
密碼登陸的認證流程如下:
- 客戶端使用密鑰和算法協商階段生成的會話密鑰加密賬號、認證方法、口令,將結果發送給服務器。
- 服務端使用獲得的會話密鑰解密報文,得到賬號和口令。
- 服務端對這個賬號和口令進行判斷,如果失敗,向客戶端發送認證失敗報文,其中包含了可以再次認證的方法列表。
- 客戶端從認證方法列表中選擇一種方法進行再次認證。
- 這個過程反復進行,直到認證成功或者認證次數達到上限,服務端關閉本次TCP連接。
秘鑰登陸
秘鑰登陸的認證流程如下:
- 客戶端使用密鑰和算法協商階段生成的會話密鑰加密賬號、認證方法、id_rsa.pub,將結果發送給服務端。
- 服務端使用會話密鑰解密報文,得到賬號、id_rsa.pub。服務端在
$HOME/.ssh/authorized_keys
中找對應的公鑰,如果沒有找到,發送失敗消息給客戶端,如果找到,比較客戶發送過來的這個公鑰和找到的公鑰,如果內容相同,服務端生成一個隨機的字符串,簡稱“質詢”,然后使用找到的公鑰加密這個質詢,然后使用會話密鑰再次加密。 - 服務端把這個雙重加密的數據發送給客戶端
- 客戶端使用會話密鑰解密報文,然后使用 id_rsa 再次解密數據,得到質詢。
- 客戶端使用會話密鑰加密質詢,發送給服務端。
- 服務端使用會話密鑰解密報文,得到質詢,判斷是不是自己生成的那個質詢,如果不相同,發送失敗消息給客戶端,如果相同,認證通過。
二者區別
我們常說使用秘鑰登陸比密碼登陸更方便更安全,為什么這么說呢?方便是因為不用記密碼,安全是一方面敏感關鍵的密碼沒有在傳輸,另一方面是因為質詢的存在使得在一次對話中同時驗證了客戶端和服務端。
高階用法
免密安全傳輸
scp/rsync/sftp 都可以基于 SSH 來進行免密安全傳輸,常見命令如下:
# 從本地同步 src.tar.gz 文件到遠程服務器 jump_target 的目錄 /path/to/des/
scp src.tar.gz jump_target:/path/to/des/
rsync -avz src.tar.gz jump_target:/path/to/des/
# 從遠程服務器 jump_target 的文件 /path/to/src.tar.gz 到本地
scp jump_target:/path/to/src.tar.gz .
rsync -avz jump_target:/path/to/src.tar.gz .
端口轉發
SSH 不僅僅能夠自動加密和解密 SSH 客戶端與服務端之間的網絡數據,同時,SSH 還能夠提供了一個非常有用的功能,那就是端口轉發,即將 TCP 端口的網絡數據,轉發到指定的主機某個端口上,在轉發的同時會對數據進行相應的加密及解密。如果工作環境中的防火墻限制了一些網絡端口的使用,但是允許 SSH 的連接,那么也是能夠通過使用 SSH 轉發后的端口進行通信。轉發主要分為本地轉發與遠程轉發兩種類型。
轉發常用參數
- -C: 壓縮傳輸,提高傳輸速度。
- -f: 將 SSH 傳輸轉入后臺執行,不占用當前 SHELL, 常與 -N 一起使用
- -N: 建立靜默連接(建立了連接但看不到具體會話)
- -g: 在 -L/-R/-D 參數中,允許遠程主機連接到建立的轉發的端口,如果不加這個參數,只允許本地主機建立連接。
- -L: 本地端口轉發
- -R: 遠程端口轉發
- -D:動態轉發( SOCKS 代理)
- -P: 指定 SSH 端口
本地端口轉發
由本地網絡服務器的端口 A,轉發到遠程服務器端口 B。說白了就是,將發送到本地端口 A 的請求,轉發到目標端口 B。格式如下
ssh -L 本地網卡地址:本地端口:目標地址:目標端口 用戶@目標地址
常見的應用場景見下圖:
對應的命令如下:
# jump_target 服務器上的 3306 端口服務映射到本地 33306 `mysql -u root -p root -H localhost -P 33306`
## 1 是 2,3,5 路線中的加密通道,將本地 33306 的網絡數據轉發到 jump_target 的 3306 端口
ssh -C -N -g -L 33306:localhost:3306 jump_target
## 在 2,3 中搞了個加密通道,然后在跳板機上將本地 33306 的網絡數據轉發到 172.17.132.120 的 3306 端口
ssh -C -N -g -L 33306:172.17.132.120:3306 jumper
遠程端口轉發
由遠程服務器的某個端口,轉發到本地網絡的服務器某個端口。說白了,就是將發送到遠程端口的請求,轉發到目標端口。格式如下:
ssh -R 遠程網卡地址:遠程端口:目標地址:目標端口 用戶@目標地址
常見的應用場景有個專用術語叫內網穿透,結構如下圖:
# 將公網上的服務器 jump_target 的端口 33333 映射到本地的 22,這樣就可以通過在 jump_target 上通過 SSH 來訪問本地機器
ssh -f -N -g -R 33333:127.0.0.1:22 jump_target
TIPS:
公網上的服務器 jump_target 要設置GatewayPorts yes
,默認為 no。此外要映射的端口 33333 要可以訪問。
動態轉發
動態轉發就是建立一個SSH加密的SOCKS 4/5代理通道。任何支持 SOCKS 4/5 協議的程序都可以使用這個加密的通道進行訪問。格式如下:
ssh -D [本地地址:]本地端口號 遠程用戶@遠程地址
# 將訪問本地 55558 端口的請求都轉發給 jump_target ,并讓它去執行
ssh -C -N -g -T -D 127.0.0.1:55558 jump_target
日常使用問題
跳板機的配置
# 跳板機的配置
Host jump
HostName jumper.example.com
Port 58422
User haojunyu
IdentityFile ~/.ssh/dg_rsa
AddKeysToAgent yes # 將私鑰添加到 agent 中
UseKeychain yes # 保存密碼到 agent 中
# 目標機的配置
Host ws
HostName 172.17.132.120
Port 22
User haojunyu
IdentityFile ~/.ssh/dg_rsa
ProxyCommand ssh haojunyu@jump -W %h:%p 2>/dev/null
ServerAliveInterval 10
ServerAliveCountMax 3
內網任意服務訪問
日常工作中經常會啟很多服務在內網機器上,然后通過打洞(本地端口轉發)來將本地的端口映射到內網機器上服務端口。
這樣有個問題就是一個服務就得維持一個打洞命令 ssh -C -N -g -L 33306:172.17.132.120:3306 jumper
。
對應這樣的問題,最好的解決方案是使用動態轉發 ssh -C -N -g -T -D 127.0.0.1:55557 hb_jumper
,
本地通過 SwitchyOmega 或 proxifier 工具來將內網 IP 段 172.17.* 的請求轉發到本地的 55557 端口。
git push 報權限不允許(公鑰)
通常在服務器上執行 git push
時會報如下錯誤
具體報錯信息:
Permission denied (publickey).
fatal: Could not read from remote repository.Please make sure you have the correct access rights and the repository exists.
報錯的原因是當前機器上沒有服務告訴 git 要使用哪個私鑰來進行 git 的操作。
對應的解決方法也比較多,推薦解法一和二:
-
解法一:通過
~/.ssh/config
指定(適用個人機器)Host github.com HostName github.com User haojunyu IdentityFile ~/.ssh/id_rsa
-
解法二:配置倉庫或全局的 core.sshCommand(指定倉庫適用共享機器,全局適用個人機器.git版本高于2.3.0)
git config core.sshCommand "ssh -i ~/.ssh/id_rsa -F /dev/null"
-
解法三:ssh-agent 臨時授權(適用共享機器)
eval `ssh-agent` ssh-add ~/.ssh/id_rsa
端口轉發命令服務化
這個情況是希望開機時就把端口轉發開通,并且一直保持著。這就得介紹 linux 中常用的兩種服務化的工具:Supervisor 和 Systemd。
前者是需要安裝 Supervisor, 但工具比較輕量,使用也比較簡單,后者雖然比較重,但是基本所有系統都自帶。下面提供兩者的配置方法:
- Supervisor 的配置
[program:ssh-wifi_ol]
command=ssh -C -N -g -L 9789:127.0.0.1:9789 jump
stdout_logfile=/Users/haojunyu/.supervisord_log/ssh-wifi_ol.log
autostart=true
autorestart=true
startsecs=5
priority=1
stopasgroup=true
killasgroup=true
- Systemd 的配置
# gfw service
[Unit]
Description=gfw
After=network.target
[Service]
Type=simple
User=hjy
ExecStart=ssh -C -N -g -T -D 127.0.0.1:55558 gfw
Restart=on-failure
[Install]
WantedBy=multi-user.target
TIPS:
把一些經常用的服務通過端口轉發服務化,而一些臨時性的服務通過命令來進行端口轉發,也可以使用同事編寫的端口轉發的 Python 程序來進行。
參考文獻
- 什么是SSH?你應該用過吧
- 維基百科-SSH
- windows支持openssh
- 圖解SSH原理
- SSH官方文檔
- 所有配圖
- 中間人攻擊
- 了解ssh代理
- ssh遠程登陸中的鑰匙指紋是什么以及如何比對
- ssh登陸認證過程詳解
如果該文章對您產生了幫助,或者您對技術文章感興趣,可以關注微信公眾號: 技術茶話會, 能夠第一時間收到相關的技術文章,謝謝!
本篇文章由一文多發平臺ArtiPub自動發布