新手上路之 SSH

Oct 30, 2023

System
SSH to remote? Right the way!

相信大家對「SSH 進機器」這句話肯定不陌生,但在寫這篇文章之前,我只知道 SSH 是由三個英文單字組成,其他的一概不知 🫠 所以今天就讓我們來了解 SSH 的魔法,以及如何更方便的用 SSH 連線吧!

說真的,SSH 是什麼

Secure Shell Protocol - 又稱 SSH,讓使用者可以透過安全的管道,操控及存取遠端的 Server。在 macOS 及 Linux 系統中都有內建 SSH,所以我們才能直接用終端機來連線至遠端的 server。

ssh username@remote-server-ip

SSH 連線的過程中,會經過五個步驟。我們可以在連線時用 -v-vv-vvv option 來查看連線的過程,v 越多越詳細(越囉唆)。

ssh -v username@remote-server-ip

接著就會看到 terminal 噴出一堆訊息

OpenSSH_9.0p1, LibreSSL 3.3.6
debug1: Reading configuration data /Users/cooper/.ssh/config
 
...
 
debug1: channel 1: connected to /var/tmp/fig/cooper/secure.socket port -2

接著我們來拆解每一個步驟,並對照到剛剛的訊息

1. 建立 TCP 連線

TCP handshake diagram
TCP three-way handshake diagram

首先,Client 會和 Server 建立 TCP 連線。Client 向 Server 送出 synchronize(SYN)封包,其中包含了 Client 隨機產生的初始序號(Initial Sequence Number, ISN)。

Server 收到 SYN 後,便會將 Client 的 Sequence Number 加 1 後作為 acknowledge(ACK),再加上自己產生的 sequence number 後一起回覆給 Client,表示 Server 確實收到來自 Client 的 SYN。這個回覆通常會記作 SYN-ACKSYN/ACK

在這兩個步驟中都設有計時器,若在一定的時間中沒有收到預期的回覆,則會自動重新傳送封包。

接著 Client 收到來自 Server 的 SYN-ACK 後,也會將 Server 的 Sequence Number 加 1,作為 acknowledge(ACK)並回覆給 Server。此時 TCP three-way handshake 就完成。

我們可以在 terminal 中看到 Connection established,表示 TCP 連線已建立。

debug1: Connecting to remote-ip-address [remote-ip-address] port 22.
debug1: Connection established.

2. SSH Protocol Version Exchange

接著 Server 及 Client 互相交換 SSH Protocol 版本,確認相容性,並決定用哪一個版本的 Protocol 來溝通。

從 terminal 中,可以看到 Local 的 SSH 版本為 OpenSSH_9.0,而 Remote 的 SSH 版本為 OpenSSH_8.9p1,最後使用 OpenSSH_8.9p1 來溝通。

debug1: Local version string SSH-2.0-OpenSSH_9.0
debug1: Remote protocol version 2.0, remote software version OpenSSH_8.9p1 Ubuntu-3ubuntu0.3
debug1: compat_banner: match: OpenSSH_8.9p1 Ubuntu-3ubuntu0.3 pat OpenSSH* compat 0x04000000

3. Session Encryption Negotiation

當 SSH 版本確定後,就會開始進行 Session Encryption Negotiation。

此時,Client 及 Server 會產生暫時性的 private-public key pairs,並互相交換 public key。接著各自雙方拿著對方的 public key 及自己的 private key,透過 key exchange 演算法獨立計算出 shared secret。這個 shared secret 會被用來加密整個 SSH session 中傳輸的資料。

接著再回到 terminal 裡面瞧瞧:

debug1: kex: algorithm: [email protected]
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: [email protected] MAC: <implicit> compression: none
debug1: kex: client->server cipher: [email protected] MAC: <implicit> compression: none

首先,最前面的「kex」表示 Key Exchange。

第一行表示 Client 及 Server 會使用 [email protected] 做為產生 shared secret 的 key exchange 演算法。第二行表示目前使用者嘗試用來登入的 host key 是由 ssh-ed25519 演算法計算出來的。最後,第三及第四行表示 Client 及 Server 雙方同意使用 [email protected] 演算法,並使用 shared secret 做為 key,加密整個 SSH session 中的訊息。

4. User Authentication

在 Client 及 server 決定好要使用哪一個 key exchange 演算法之後,便會開始進行 User Authentication。

在 terminal 裡面可以看到 server 支援的 authentication methods 為 publickey 及密碼兩種,此時我們以 publickey 來進行驗證。在 Server 接收 key 之後,使用者驗證就完成了!

debug1: Authentications that can continue: publickey,password
debug1: Next authentication method: publickey
debug1: Offering public key: ...
debug1: Server accepts key: ...
Authenticated to remote-server-address ([remote-server-address]:port) using "publickey".

5. 建立 SSH tunnel

在 Server 驗證 Client 身份且產生 shared secret 之後,Client 和 Server 便可以使用 shared secret 來加密及解密封包。最後 server 開啟 shell environment,讓 client 可以透過 shell 來操作 server。而這個讓雙方互相溝通的「通道」稱為 SSH tunnel。

最後在 terminal 中,可以看到 SSH tunnel 已經建立完成。

debug1: channel 1: new [forwarded-streamlocal]

從這裡開始,所有傳輸的封包都會使用 shared secret 進行對稱式加密。整個封包可以拆解成五個部分:

  • Packet Length (4 bytes) - 表示整個封包的長度,不包含 MAC 及 Packet Length 本身。如果 Payload 有經過壓縮,則會計算壓縮後的長度
  • Padding length (1 byte) - 表示 Padding field 的長度
  • Payload - 真正要傳輸的資料。如果有進行壓縮,則只會壓縮 Payload
  • Padding field - Random bytes,會和真正的資料(payload)一起加密,讓真實資料更難被有心人士讀取。最小必須要是 4 bytes,最大則是 255 bytes
  • Message Authentication Code (MAC) - 包含用來驗證的演算法,檢驗封包是否被竄改

除了 message authentication code 之外,其餘的資料都會被 shared secret 加密。

了解 SSH 是如何運作之後,接著就來看看如何更快速且更安全地與機器連線!

修改預設的 Port,避免機器被瘋狂嘗試登入

Port 22 door breaking

通常預設的 SSH 連線 Port 會是 22,所以機器被掃到就會被瘋狂嘗試登入!

default port
短短幾分鐘內就被踹了四千多次 🤔

因此,我們需要把預設的 Port 換掉!這裡會介紹兩種方法,分別是使用 iptables 及 firewalld 來設定。

iptables

iptables 是 Linux 系統中用來管理防火牆及封包過濾的指令工具。透過不同的 chain 來進行管理進出的 traffic。

  1. 編輯位於 /etc/sshsshd_config 檔案
sudo vim /etc/ssh/sshd_config
  1. 找到一行寫著 Port 22(可以按下 / 並輸入 22 來尋找),並將 Port 改成其他 Port,例如改成 20388
Port 20388
  1. 儲存並離開檔案
  2. 使用 systemctl 指令重新啟動 sshd 服務,並開始監聽新的 Port。
sudo systemctl restart sshd

Systemd 使用 systemctl 這個指令來管理各種系統服務。在這邊,我們用來重啟(restart)sshd 這個服務。

Note

sshd 是 OpenSSH daemon,會持續聽取來自 client 的 SSH 連線,並做出相對應的處理。另外可以對服務進行的操作還有停止(stop)、啟動(start)、檢查狀態(status)等等。sudo systemctl restart sshd 這個指令等同於 sudo systemctl restart sshd.service,省略了後面的 .service

  1. 新增一條規則,讓剛剛調整過的 Port 可以通過防火牆
sudo iptables -A INPUT -p tcp --dport 20388 -j ACCEPT

-A INPUT 是指我們想要 append 一個新的規則到 INPUT Chain 當中。另外還有 FORWARDOUTPUT chain。

這些 chain 用來管理連線的進出,例如 INPUT chain 管理所有進到 server 的封包,OUTPUT chain 管理所有由離開系統的封包,FORWARD chain 則管理所有「路過」系統的封包,指那些最終目的地不是該系統,只是透過系統來 routing 的封包們。這三個 chain 又稱為 filter table。在 iptables 當中的規則會由上至下的逐一比對,當條件符合時,就用該規則來處理封包,並且不會再往下繼續比對。

-p tcp 指的是將這個規則套用在透過 tcp 的連線上。--dport 定義目的地的 port,可以是單一個 port,像我們寫的 20388,或是一個 port 的開始到結束範圍,如 20388:20340

-j 用來定義這些封包最後該「跳(jump)」到哪裡。iptables 預設可允許四種目標:

  • ACCEPT - 接收封包,並停在這裡,不繼續往下比對其他 chain 的規則。
  • REJECT - 拒絕封包傳遞,並跟傳送封包的傢伙說:「我 reject 了啦哈哈」並停在這裡,不繼續往下比對其他 chain 的規則。
  • DROP - 什麼都不做,無聲無息的忽略這個封包,並停在這裡,不繼續往下比對其他 chain 的規則。
  • LOG - Log 這個封包,並往下繼續比對規則。
  1. 最後,用 Drop 來忽略 Port 22 的連線
sudo iptables -A INPUT -p tcp --dport 22 -j DROP

如此一來設定就完成了!如果用原先預設的 Port 22 來連線,最後應該會等到 Connection time out 的錯誤訊息。

firewalld

firewalld 透過「Zone」來定義不同信任層級,並透過策略(Policy)來管理及過濾 zone 與 zone 之間的 traffic。

Firewalld 將設定區分為 Runtime ConfigurationPermanent Configuration,前者只在當下的 session 生效,所以在重啟或重新載入後就會消失;後者則是當下不會生效,但重啟或重新載入後會永久生效。

Firewalld Zones
Firewalld ゾーン!

在安裝完 firewalld 後,預設會有九個 zones,分別是 droppublictrustedblockdmzexternalhomeinternalwork

每個 zone 都有不同的設定及信任層級。舉例來說,drop 會 drop 所有進來的連線,只允許出去的連線;public 則是預設的 zone,會允許一些常見的服務,例如 http、https、ssh 等等,trusted 則會允許所有的連線。

而如果有安裝 Docker,則會自動多出一個 docker zone,將 Docker 產生的所有 network interfaces 放入 docker zone 當中。

接著來看怎麼使用 firewalld 來設定 Port 吧!

firewalld 設定 Port

  1. 允許 20388 透過 tcp 的連線,沒有帶入 zone option 則預設為 public zone
sudo firewall-cmd --add-port=20388/tcp --permanent

--permanent flag 將該設定標記為永久存在的設定,因此要重新載入後才會生效。如果沒有 --permanent flag,則預設會是 runtime config。

  1. 重新載入 firewalld daemon
sudo firewall-cmd --reload
  1. 檢查 port 是否設定正確
sudo firewall-cmd --list-all

此時沒意外的話,就可以看到 port 設定好了!

public
  target: default
  icmp-block-inversion: no
  interfaces:
  sources:
  services: ...
  ports: 20388/tcp
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
  1. 編輯位於 /etc/sshsshd_config 檔案
sudo vim /etc/ssh/sshd_config
  1. 找到一行寫著 Port 22(可以按下 / 並輸入 22 來尋找),並將 Port 改成 20388
Port 20388

如果想要支援多個 Port,可以在下面繼續加上去,例如:

Port 20388
Port 20400

記得要重複一次 firewalld 的設定。

如何不用輸入密碼及 IP 進入機器

完成 Port 的設定之後,接著就可以拿著熱騰騰的 Port 來快速進入機器!

  1. 建立 public-private key pairs,並指定檔案名稱及路徑。-t 參數用來指定使用何種演算法來產生 keys。這裡我們把它叫做 id_rsa_remote_vps
ssh-keygen -t rsa

或是用 key size 更小、更安全的 ed25519 演算法

ssh-keygen -t ed25519
ssh-keygen
  1. 接著到 /User/username/.ssh 這個路徑底下,就會看到剛才產生的兩個 key 檔案,分別是 id_rsa_remote_vpsid_rsa_remote_vps.pub
  2. 將新鮮出爐的 public key 丟到 remote server 上。可以手動將 id_rsa_remote_vps.pub 裡面的內容貼到 remote server 裡面的 ~/.ssh/authorized_keys 這個檔案當中,或者是使用指令。-i 後面指向剛才產生的 public key 位置
ssh-copy-id -i ~/.ssh/id_rsa_remote_vps.pub root@your-remote-server-ip

此時進到 remote server 當中,就可以在 ~/.ssh/ 底下看到 authorized_keys 這個檔案

authorized-keys
  1. 接著要來設定本機。在本機的 ~/.ssh/ 資料夾底下新增一個檔案,名稱叫做 config。如果沒有的話,可以用 touch 來新增
touch ~/.ssh/config
  1. 編輯剛才新增的 config,填入 remote server IP address、User、剛才設定的 Port,並將剛才新增的 SSH private key 路徑放在 IdentityFile,最後在 Host 填上想要用來連接的名稱
# ~/.ssh/config
Host remote-vps
	HostName your-remote-ip
	User someuser
	IdentityFile ~/.ssh/id_rsa_remote_vps
	Port 20388
  1. 最後,就可以直接用名稱,透過 SSH 連線至 remote server!🥳🥳
ssh remote-vps

總結

我們一路從 SSH 如何連線,說到如何修改 Port,最後了解如何不用輸入密碼及 IP 來連線至 remote server。以後終於可以理直氣壯地說出 SSH 三個字了!

💡

\ SSH! / \ SSH! / \ SSH! /

Reference

Last updated onFeb 11, 2024

Comments