掌握SSH這篇就夠了

SSH 是每一臺電腦的標準配置,Linux 就沒必要說了,連 windows 也從 2018 年開始自帶 OpenSSH 了。
它主要的用途是登錄到遠程電腦中執行命令,在雲開發的時代,它是每一個程序天天都要用到的工具。本文將簡單介紹一下它的原理,
基本用法以及端口轉發和動態轉發等高階用法。mysql

介紹

SSH 叫安全外殼協議(Secure Shell),是一種加密的網絡傳輸協議,可在不安全的網絡中網絡服務提供安全的傳輸環境。它經過在網絡中建立安全隧道來實現 SSH 客戶端和服務器之間的鏈接。最先的時候,互聯網通訊都是明文通訊,一旦被截獲,內容就會被暴露。1995年,芬蘭學者 Tatu Ylonen 設計了 SSH 協議,將登陸信息所有加密,成爲互聯網安全的一個基本解決方案,迅速在全世界得到推廣,目前已經成爲全部操做系統的標準配置。linux

SSH 是一種協議, 存在多種實現,既有商業實現,也有開源實現(OSSH,OpenSSH)。本文使用的自有軟件 OpenSSH, 畢竟它是目前最流行的 SSH 實現,並且是全部操做系統的默認組件。git

TIPS: OpenSSH發展史
1995 年 7 月, Tatu Ylonen 以避免費軟件的形式將一套保護信息傳輸的程序(也就是 SSH )發佈出去。程序很快流行,到年末已經有兩萬用戶,遍及五十國家。因此在年末時,他創立了 SSH 通訊安全公司來繼續開發和銷售 SSH, 因此它變成了專有軟件。在 1999 年,瑞典程序員基於 SSH 最後一個開源的版本 1.2.12 開發了 OSSH,以後 OpenBSD 開發者在 OSSH 的基礎上進行大量修改,造成了 OpenSSH。它是目前惟一一種最流行的 SSH 實現,成爲了全部操做系統的默認組件。

原理介紹

SSH 之因此一經提出,就獲得了快速發展,是由於數據的安全性對任何人都很是重要。這裏咱們對其保護數據安全的原理進行探究。程序員

在聊加密前先介紹一下幾個密碼學的基本概念:github

  • 明文plaintext 指傳送方(通常指客戶端)想要接受方(通常指服務端)得到的可讀信息
  • 密文ciphertext 指明文通過加密後所產生的信息
  • 祕鑰key 指用來完成加密、解密、完整性驗證等密碼學應用的密碼信息,是明文轉換爲密文或密文轉換爲明文的算法須要的參數
  • 私鑰 指私有的祕鑰
  • 公鑰 指公開的祕鑰

對稱加密

對稱加密就是加密或解密使用的是同一個祕鑰。比較經常使用的對稱加密算法有 AES,DES等。其具體的時序圖以下:
ssh_symmetric.png算法

對稱加密的優勢是加解密效率高,速度快。對於服務端而言,它和每一個客戶端都要有一個祕鑰,龐大的客戶端數目致使祕鑰數目多,並且一旦機器被登陸,全部的祕鑰都泄露,因此缺點是祕鑰的管理和分發比較困難,不安全。sql

非對稱加密

非對稱加密須要一對祕鑰來進行加密和解密,公開的祕鑰叫公鑰,私有的祕鑰叫私鑰。注意公鑰加密的信息只有私鑰才能解開(加密過程),私鑰加密的信息只有公鑰才能解開(驗簽過程)。比較經常使用的非對稱加密算法有 RSA。其具體的時序圖以下:
ssh_asymmetric.pngwindows

非對稱加密的優勢是安全性更高,祕鑰管理比較方便,每一個服務器只要維護一對公私鑰便可。缺點是加解密耗時長,速度慢。不過對於如今的計算機而言,這點成本能夠忽略不計。api

中間人攻擊

中間人攻擊的英文全稱是 Man-in-the-middle attack,縮寫爲 MITM。在密碼學和計算機安全領域中是指攻擊者與通信的兩端分別建立獨立的聯繫,並交換其所收到的數據,使通信的兩端認爲他們正在經過一個私密的鏈接與對方直接對話,但事實上整個會話都被攻擊者徹底控制。在中間人攻擊中,攻擊者能夠攔截通信雙方的通話並插入新的內容。在許多狀況下這是很簡單的(例如,在一個未加密的 Wi-Fi 無線接入點的接受範圍內的中間人攻擊者,能夠將本身做爲一箇中間人插入這個網絡)。其具體的時序圖以下:
ssh_mitm.png安全

受到中間人攻擊的關鍵緣由是客戶端不知道服務端的公鑰真假,服務端也不知道客戶端的公鑰真假。因此破解這個問題的關鍵是如何相互認證,也就是要像黃宏《開鎖》小品裏同樣證實我就是我,你就是你。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@  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 的。當咱們碰到如下兩種狀況則須要它:

  1. 使用不一樣的祕鑰鏈接到不一樣的主機時,須要手動指定對應的祕鑰。(ssh-agent 幫咱們選擇對應的祕鑰進行認證)
  2. 當私鑰設置了密碼,而咱們又須要頻繁的使用私鑰進行認證。(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 中尚未服務器的身份信息,因此對於首次提示的登錄指紋信息仍是須要和服務器比對的。最安全保險的作法是第一次登錄就使用祕鑰登錄。

登錄流程

  1. 版本號協商階段
  2. 密鑰和算法協商階段
    服務端和客戶端分別發送算法協商報文給對方,報文中包含本身支持的公鑰算法列表、加密算法列表、消息驗證碼算法列表、壓縮算法列表等。服務端和客戶端根據對方和本身支持的算法得出最終使用的算法。服務端和客戶端利用 DH 交換算法、主機密鑰對等參數,生成會話密鑰和會話 ID。
  3. 認證階段( publickey > gssapi-keyex > gssapi-with-mic > password )
  4. 會話請求階段
  5. 會話交互階段

密碼登錄

密碼登錄的認證流程以下:

  1. 客戶端使用密鑰和算法協商階段生成的會話密鑰加密帳號、認證方法、口令,將結果發送給服務器。
  2. 服務端使用得到的會話密鑰解密報文,獲得帳號和口令。
  3. 服務端對這個帳號和口令進行判斷,若是失敗,向客戶端發送認證失敗報文,其中包含了能夠再次認證的方法列表。
  4. 客戶端從認證方法列表中選擇一種方法進行再次認證。
  5. 這個過程反覆進行,直到認證成功或者認證次數達到上限,服務端關閉本次TCP鏈接。

ssh_password

祕鑰登錄

祕鑰登錄的認證流程以下:

  1. 客戶端使用密鑰和算法協商階段生成的會話密鑰加密帳號、認證方法、id_rsa.pub,將結果發送給服務端。
  2. 服務端使用會話密鑰解密報文,獲得帳號、id_rsa.pub。服務端在 $HOME/.ssh/authorized_keys 中找對應的公鑰,若是沒有找到,發送失敗消息給客戶端,若是找到,比較客戶發送過來的這個公鑰和找到的公鑰,若是內容相同,服務端生成一個隨機的字符串,簡稱「質詢」,而後使用找到的公鑰加密這個質詢,而後使用會話密鑰再次加密。
  3. 服務端把這個雙重加密的數據發送給客戶端
  4. 客戶端使用會話密鑰解密報文,而後使用 id_rsa 再次解密數據,獲得質詢。
  5. 客戶端使用會話密鑰加密質詢,發送給服務端。
  6. 服務端使用會話密鑰解密報文,獲得質詢,判斷是否是本身生成的那個質詢,若是不相同,發送失敗消息給客戶端,若是相同,認證經過。

ssh_key

兩者區別

咱們常說使用祕鑰登錄比密碼登錄更方便更安全,爲何這麼說呢?方即是由於不用記密碼,安全是一方面敏感關鍵的密碼沒有在傳輸,另外一方面是由於質詢的存在使得在一次對話中同時驗證了客戶端和服務端。

高階用法

免密安全傳輸

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 本地網卡地址:本地端口:目標地址:目標端口 用戶@目標地址
常見的應用場景見下圖:
ssh_local

對應的命令以下:

# 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 遠程網卡地址:遠程端口:目標地址:目標端口 用戶@目標地址
常見的應用場景有個專用術語叫內網穿透,結構以下圖:
ssh_remote

# 將公網上的服務器 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 程序來進行。

參考文獻

  1. 什麼是SSH?你應該用過吧
  2. 維基百科-SSH
  3. windows支持openssh
  4. 圖解SSH原理
  5. SSH官方文檔
  6. 全部配圖
  7. 中間人攻擊
  8. 瞭解ssh代理
  9. ssh遠程登錄中的鑰匙指紋是什麼以及如何比對
  10. ssh登錄認證過程詳解

若是該文章對您產生了幫助,或者您對技術文章感興趣,能夠關注微信公衆號: 技術茶話會, 可以第一時間收到相關的技術文章,謝謝!
技術茶話會
本篇文章由一文多發平臺ArtiPub自動發佈

相關文章
相關標籤/搜索