The Missing Semester

2023-09-10 18:00:21

第五講(二) SSH入門

介紹完命令列環境後,這半節主要介紹的是ssh的有關入門知識。SSH是Secure Shell的簡稱。

課程視訊地址:https://www.bilibili.com/video/BV1x7411H7wa

課程講義地址:https://missing-semester-cn.github.io/2020/command-line/

本機學習使用平臺:wsl1 && ubuntu20.04 + Windows10

通過如下命令,您可以使用 ssh 連線到其他伺服器:

ssh [user name]@[target ip]

但是...目標伺服器需要先安裝ssh-server,才能進行正常的ssh交流!

設定虛擬機器器SSH

要想把一臺機器作為伺服器使用,就需要設定好相應的服務環境:

  1. 安裝openssh-client:sudo apt-get install openssh-client
  2. 安裝openssh-serversudo apt-get install openssh-server
  3. 啟動前可能需要對ssh伺服器設定進行修改,例如修改埠,在下文有單獨章節說明;
  4. 啟動ssh-server:sudo /etc/init.d/ssh restart (利用file命令可以看到這是一個一百多行的shell指令碼檔案,用來啟動ssh服務)
  5. 連線後,在主機使用netstat 確認ssh-server工作正常。(看到ssh連線的相應條目表示工作正常)

要注意的是,在使用者端第一次對伺服器進行連線時,會彈出一個提示,讓你驗證伺服器的ssh金鑰指紋(當前為ECDSA金鑰):

如圖,該提示意味著主機首次連線到這臺伺服器,SSH無法確認伺服器的身份。該指紋使用的是SHA256加密格式,如果伺服器是你自己的機器,可以使用ssh-keygen工具檢視該機器的指紋,指紋存放在/etc/ssh/ssh_host_ecdsa_key.pub,即驗證的是伺服器的ECDSA公鑰指紋。命令如下:

ssh-keygen -l -f /etc/ssh/ssh_host_ecdsa_key.pub

其中對命令引數的介紹如下:

    -f [filename]
             Specifies the filename of the key file.

	-l       Show fingerprint of specified public key file.  For RSA and
             DSA keys ssh-keygen tries to find the matching public key
             file and prints its fingerprint.  If combined with -v, a
             visual ASCII art representation of the key is supplied with
             the fingerprint.          

輸入命令檢視得到伺服器指紋,對比發現確實是一模一樣的,放心連線啦!選擇yes後,這將把伺服器的金鑰新增到你的本地SSH金鑰列表中,並在以後的連線中自動驗證。(加入-v引數可以看到賽博指紋)

輸入正確的密碼進行連線:

連線成功後主機的情況,已經可以操作伺服器了:

伺服器端使用netstat檢驗連線是否正常,最後可以使用ctrl-D斷開ssh連線。

修改伺服器組態檔

如果要在伺服器上開啟SSH服務,我們應該修改sshd_config組態檔,而不是ssh_config組態檔。

  • ssh_config檔案是SSH使用者端的組態檔,用於設定SSH使用者端的行為和選項。
  • sshd_config檔案是SSH伺服器的組態檔,用於設定SSH伺服器的行為和選項。

但修改前要注意的是,裡面已有的註釋已經是預設選項,即你取不取消註釋都沒問題。

The commented out options in the sshd_config are the defaults.

大多數Linux發行版中,sshd_config檔案位於/etc/ssh/目錄下。對於Windows上的OpenSSH,sshd_config檔案多數位於C:\ProgramData\ssh\目錄下;可以在sshd_config這裡設定免密認證、修改 ssh 埠、開啟 X11 轉發等等,也可以為每個使用者單獨指定設定。

對於Linux系統,可以使用以下命令重新載入SSH伺服器設定:

  • sudo systemctl reload ssh(對於systemd系統)
  • sudo service ssh reload(對於非systemd系統)

基於金鑰的認證機制

基於金鑰的驗證機制使用了密碼學中的公鑰,我們只需要向伺服器證明使用者端持有對應的私鑰,而不需要公開其私鑰。這樣您就可以避免每次登入都輸入密碼的麻煩了。不過,私鑰(通常是 ~/.ssh/id_rsa 或者 ~/.ssh/id_ed25519) 等效於您的密碼,所以一定要好好儲存它。

SSH採用的公私鑰體系,簡而言之,就是使用者端給伺服器提供公鑰,伺服器利用公鑰認證登入的使用者端(即證明使用者端確實擁有登入私鑰)。(此處有對ssh公私鑰體系的知識講解)

金鑰有很多種,如ed25519 、EDCSA、RSA等等,這篇部落格講解了它們之間的區別,當前最好的還是ed25519

生成金鑰的工具為ssh-keygen,工具檔案在此

生成命令範例:

ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519

-o:指出私鑰檔案格式為 OpenSSH, OpenSSH 6.5版本引入。

-a:加密輪數。越高越難以BF破解。

-t:加密型別。 ed25519為其中一種加密方式,預設RSA加密。簡略介紹見此

-f:輸出檔名稱。

生成的金鑰會問你是否需要設定一個passphrase,這是為了防止別人能夠輕易獲取你的私鑰。但是,這也會在你想要利用公鑰認證登入時,需要再次輸入儲存該金鑰的passphrase。那這還算什麼免密登入呢???幸好,我們有ssh-agent幫我們解決這個麻煩,在這裡先不提,先把流程走一遍先。

生成ed25519金鑰後,可以利用管道輸入ssh伺服器組態檔authorized_keysauthorized_keys檔案的格式在sshd手冊中有介紹):

# windows作主機也可以進行這樣的管道操作
cat .ssh/id_ed25519.pub | ssh user_name@remote 'cat  >>  ~/.ssh/authorized_keys'

或者可以用tee工具完成這個工作:

cat .ssh/id_ed25519.pub | ssh user_name@remote tee  ~/.ssh/authorized_keys

如果支援 ssh-copy-id 的話,可以使用下面這種更簡單的解決方案:

ssh-copy-id -i .ssh/id_ed25519.pub user_name@remote

在這裡比較推薦使用ssh-copy-id。因為單純的管道操作不會給你驗證你的輸入檔案到底是公鑰還是私鑰,而ssh-copy-id則給了這個防禦性措施。(如果你錯誤地使用了私鑰檔案,還可能會暴露)

Windows沒有ssh-copy-id,則可以編寫相應功能的指令碼來完成這一工作:

function ssh-copy-id([string]$userAtMachine, $args){   
    $publicKey = "$ENV:USERPROFILE" + "/.ssh/id_rsa.pub"
    if (!(Test-Path "$publicKey")){
        Write-Error "ERROR: failed to open ID file '$publicKey': No such file"            
    }
    else {
        & cat "$publicKey" | ssh $args $userAtMachine "umask 077; test -d .ssh || mkdir .ssh ; cat >> .ssh/authorized_keys || exit 1"      
    }
}

設定好使用者端公鑰後,即可進行「免密」的公鑰認證登入,無需再次輸入登入使用者的密碼(但仍需要passphrase)。

但是要注意的是,伺服器端必須開啟公鑰認證!在/etc/ssh/sshd_config檔案中進行修改,並且記得重啟伺服器端的sshd。

# 是否允許公鑰身份驗證,預設為yes
PubkeyAuthentication yes
# 認證公鑰檔案,預設情況就是我們之前所做的那樣
AuthorizedKeysFile     .ssh/authorized_keys .ssh/authorized_keys2
#是否允許密碼身份驗證,預設為yes,當我們使用金鑰可以成功登陸後,可以改為no
PasswordAuthentication no

而且要注意的是,公鑰登入似乎對金鑰檔案許可權要求特別嚴格..最好把伺服器端的.ssh目錄設為700,authorized_keys檔案設為644。對於使用者端:私鑰必須為600許可權或者更嚴格許可權 (400), 一旦其他使用者可讀, 私鑰就不起作用 (如640), 表現為系統認為不存在私鑰; 對於伺服器端:要求必須公鑰其他使用者不可寫, 一旦其他使用者可寫 (如660), 就無法用key登入, 表現為:Permission denied (publickey).

參考的博文這樣寫:

  • ~/.ssh/authorized_keys must have 644 permission
  • Private key file i.e. ~/.ssh/id_rsa should be 600, the name of the key may be different as per user environment
  • Public key file i.e. ~/.ssh/id_rsa.pub should be 644, the name of the key may be different as per user environment
  • The ~/.ssh directory must not be world readable/writable so you can keep it with 700 permission

注意:如果你使用的是RSA加密的金鑰,並且發現sshd_config檔案沒有RSAAuthentication這個設定選項,這是一個很有趣的坑,可能會讓你無法進行金鑰登入..詳細原因見此博文。(所以還是用回其它加密方式的金鑰吧)

如果你發現公鑰認證登入失敗,可以在登入時嘗試利用登入引數-v列印debug資訊。注意,v的個數上限為3個。

或者利用sshd的debug模式:先停掉ssh服務,然後在確保擁有755許可權的/run/sshd資料夾的情況下,使用命令:sudo /usr/sbin/sshd -d開啟debug模式的ssh伺服器端。

我自己使用sshd啟動debug模式,在公鑰驗證時出現過"postponed publickey" 的log,這裡有一篇博文講了這方面debug的方法:SSH fails with postponed publickey error,非常非常棒的博文!

關於新增公鑰後,登入仍需密碼?有關這類問題社群有提問:SSH Still Asks for Password with Public Key Setup——對這個問題做了一些以前出現過的問題的歸納。

我有一段時間一直搞不定windows10免密登入wsl1的ubuntu20.04,連wsl自己免密登入localhost也不行,一直是" Permission denied (publickey)"。但是在virtualbox上的Ubuntu18.04就能在ssh-copy-id後直接進行公鑰登入……使用ssh-add也新增不上私鑰認證,也找不出原因。

最後溫度上來了把自己.ssh目錄清空,將自己舊的ed25519金鑰對刪掉生成一個新的,然後從頭來一遍,wsl登入localhost卻可以了……而且win到wsl不行的原因更離譜。。竟然是採用管道輸入的公鑰在authorized_keys的格式不對(用cat命令輸出,或者用vim開啟都可以看到),前面多了一個/feff/符,可能是編碼格式的問題。(Linux下檔案開頭的feff的問題

類似wsl的ssh登入失敗問題:Passwordless SSH login into WSL2 failing

關於ssh金鑰格式不對可能產生的問題:SSH Key Format Issues and Fixing the 「invalid format」 Error

ssh-agent和ssh-add

您可以為金鑰設定密碼(passphrase),防止有人持有您的私鑰並使用它存取您的伺服器。

「您可以使用 ssh-agentgpg-agent ,這樣就不需要每次都輸入該passphrase了。」

如教案中所言,ssh-agent存在的意義就是幫助你解決passphrase驗證的過程,這需要ssh-add的幫助。

ssh-agent 是一個金鑰管理器,用來管理一個多個金鑰,併為其他需要使用 ssh 金鑰的程式提供代理(即幫忙驗證passphrase,不用每次都要自己輸入)。在使用ssh-add前,先要保證ssh-agent正在執行。

利用echo $SSH_AGENT_PID來檢查是否有ssh-agent程序在執行。如果沒有,則需要啟動。

不同作業系統中的ssh-agent的啟動方式是不同的。在 Linux中: ssh-agentX對談 或 **登入對談 **之初就已經啟動;在Windows中: 計算機管理 -> 服務 -> OpenSSH Authentication Agent 設定為自動啟動。

也可以手動啟動ssh-agent

ssh-agent $SHELL

另一種命令:eval shell-agent , 在windows中為 eval $(ssh-agent) : 它並不會啟動一個子shell,而是直接啟動一個 ssh-agent 程序;此時當我們退出當前 bash 後,ssh-agent 程序並不會自動關閉。我們可以在當前bash退出之前,使用 ssh-agent -k ,或者在當前 bash 退出之後,使用 kill 命令,關閉對應的 ssh-agent 程序。

如果在Windows中手動啟動失敗,遇到error:1058問題,這裡有一個關於windows10 ssh-agent啟動服務失敗的問題:Starting ssh-agent on Windows 10 fails: "unable to start ssh-agent service, error :1058"

ssh-agent啟動成功後,就可以開始著手passphrase的代理驗證新增了:

在預設情況下,ssh-agent使用的是使用者目錄下.ssh目錄裡面儲存的金鑰。ssh-add在預設情況下,會將~/.ssh/id_rsa, .ssh/id_dsa, ~/.ssh/id_ecdsa, ~/.ssh/id_ed25519, 和 ~/.ssh/identity都新增到ssh-agent裡面。通過ssh-add新增私鑰認證後,ssh-add -l可以看到當前ssh-agent可以存取的私鑰列表。

ssh-add -l顯示"The agent has no identities."的情況:ssh-add command does not add my identity to ssh-agent——大概率也是金鑰檔案許可權問題導致的。

這一節參考博文:ssh agent詳解

通過 SSH 複製檔案

使用 ssh 複製檔案,即在主機與遠端機之間傳輸檔案有很多方法:

  • ssh+tee, 最簡單的方法是執行 ssh 命令,然後通過這樣的方法利用標準輸入實現 cat localfile | ssh remote_server tee serverfile。回憶一下,tee 命令會將標準輸出寫入到一個檔案;
  • scp :當需要拷貝大量的檔案或目錄時,使用scp 命令則更加方便,因為它可以方便的遍歷相關路徑。語法如下:scp path/to/local_file remote_host:path/to/remote_file
  • rsyncscp 進行了改進,它可以檢測本地和遠端的檔案以防止重複拷貝。它還可以提供一些諸如符號連線、許可權管理等精心打磨的功能。甚至還可以基於 --partial標記實現斷點續傳(課上展示了-avP引數)。rsync 的語法和scp類似。

ssh的dotfile——config檔案

我們每次進行ssh登入,都需要輸入一長串的string……太麻煩了!

教案裡面給出了一個很簡單粗暴的方法:為它們建立一個alias

alias my_server="ssh -i ~/.id_ed25519 --port 2222 -L 9999:localhost:8888 user_name@remote_server"

不過,更好的方法是使用dotfile。我們可以使用config檔案,它的預設位置為~/.ssh/config,和.vimrc.bashrc一樣,它是屬於ssh的dotfile

# 這樣登入只需要輸入 ssh vm 就可以了
Host vm
    User foobar
    HostName 172.16.174.141
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 9999 localhost:8888

# 在組態檔中也可以使用萬用字元
Host *.mit.edu
    User foobaz

這麼做的好處是,使用 ~/.ssh/config 檔案來建立別名,類似 scprsyncmosh的這些命令都可以讀取這個設定並將設定轉換為對應的命令列選項。

教案的tips:

注意,~/.ssh/config 檔案也可以被當作組態檔,而且一般情況下也是可以被匯入其他組態檔的。不過,如果您將其公開到網際網路上,那麼其他人都將會看到您的伺服器地址、使用者名稱、開放埠等等。這些資訊可能會幫助到那些企圖攻擊您系統的駭客,所以請務必三思。

埠轉發

該節為教案內容,課程中並沒有提及。

很多情況下我們都會遇到軟體需要監聽特定裝置的埠。如果是在您的本機,可以使用 localhost:PORT127.0.0.1:PORT。但是如果需要監聽遠端伺服器的埠該如何操作呢?這種情況下遠端的埠並不會直接通過網路暴露給您。

此時就需要進行 埠轉發。埠轉發有兩種,一種是本地埠轉發和遠端埠轉發(參見下圖,該圖片參照自這篇StackOverflow 文章)中的圖片。

本地埠轉發

遠端埠轉發

常見的情景是使用本地埠轉發,即遠端裝置上的服務監聽一個埠,而您希望在本地裝置上的一個埠建立連線並轉發到遠端埠上。例如,我們在遠端伺服器上執行 Jupyter notebook 並監聽 8888 埠。 然後,建立從本地埠 9999 的轉發,使用 ssh -L 9999:localhost:8888 user_name@remote_server 。這樣只需要存取原生的 localhost:9999 即可。

雜項

連線遠端伺服器的一個常見痛點是遇到由關機、休眠或網路環境變化導致的掉線。如果連線的延遲很高也很讓人討厭。Mosh(即 mobile shell )對 ssh 進行了改進,它允許連線漫遊、間歇連線及智慧本地回顯。

有時將一個遠端資料夾掛載到本地會比較方便, sshfs 可以將遠端伺服器上的一個資料夾掛載到本地,然後您就可以使用原生的編輯器了。(不過現在sshfs已經是一個orphaned的專案了……)