Try   HackMD

用Nginx架設local端https應用

tags: Nginx Ubuntu WSL WebApp

背景

某天閒來無事的我寫了一個Web App,想要分享給別人使用,剛好實驗室有固定IP的電腦可以用,為了省錢(X)就決定直接開在上面。一開始一切都很美好,用Docker開好了MySQL server,Node.js的server也順利開起來連上DB,localhost測起來相安無事,於是乎,拿起我的小筆電,連連看實驗室IP,恩,可以看到網頁,但好像有某個功能壞了(?)

原本在WebApp中有個功能是可以直接複製文字到剪貼簿中,用到了Clipboard API,在localhost中測試時一切都好好的,也可以順利地要求剪貼簿權限,同意後就有相關API可以call,但這時卻連權限要求的通知都沒有跳出來,從網址列點進去權限面板看看發現,不對啊!怎麼剪貼簿權限連開都沒辦法開,正常來講應該要像下面這張圖片一樣,Clipboard那邊是可以選擇要不要給權限的

在Edge上面看到的關於網頁各種權限的管理>在Edge上面看到的關於網頁各種權限的管理

但我點進去卻發現那個selector完全是disabled的,不給按QQ仔細看了敘述之後才發現,應該是因為網站是用http所以才會被擋掉權限,但這也太狠了,連我想要把自記置身於危險之中都不讓,不過其實早就有傳言Chrome會加強安全性,之後連http的網站都沒辦法訪問,用Chromium核心的Edge自然也是跟上這股歪風(X)

不給按

雖然只是個不起眼的小功能,但不能用了還是會感到很阿雜,手動選取文字再按 ctrl+c 就是很麻煩(懶),所以決定要幫這個小專案加上https。

實作

當然這個只是個個人用的小project,不太可能為了它去買個網域+SSL憑證,所以想說可以自己sign一個憑證來用用,雖然瀏覽器會跳警告說這是不安全的憑證,但至少還算是有個https可以用,於是我們就開做吧!

Web App

這個部分就沒甚麼好講的了,基本上任何的APP都可以用這個方法賦予它https,這次我是用nest-next開了一個前+後端的APP在windows上的port 8081上面,連線到localhost:8081確定APP可以正常運行,這時候如果從外部連到http://xxx.xxx.xxx.xxx:8081是可以順利看到運行中的APP的,但如果切https://xxx.xxx.xxx.xxx:8081則完全沒有畫面

建立SSL(Secure Sockets Layer)憑證

這部分我其實沒有很熟悉,主要是參考這篇一步一步做的,而SSL的原理可以看這裡,簡言之SSL會加密browser與server間傳輸的通訊內容,使傳遞的資料在傳輸過程中,不會被第三方(如proxy、router、攔截)看見明文,為了達到這個目的,server會生成一對可以相互加解密的公鑰(public key)與私鑰(private key),並將public key工愾讓所有連線的browser都可以取得,在傳輸資料前,瀏覽器會以這支公鑰加密內容,server收到data後,再以自己保有的私鑰解密。

但瀏覽器怎麼知道這隻公鑰是不是真的從server發出來的呢?如果有惡意人士替換掉了傳給browser的公鑰,那麼他就有辦法從中攔截回傳的data並解密出明文。所以現行server發出來的公鑰,必須經過權威第三方,也就是CA(Certificate Authority)去簽發認證你是這個公鑰的擁有者,這個簽發過的公鑰就被稱為憑證(certificate)。

certificate

憑證生成流程圖

為了方便管理等一下生成的檔案,我在/home/{username}新建一個名為CA的資料夾,以下的指令都是在該資料夾中執行。第一步我們先生成一個rsa非對稱加密演算法的2048位元長的密鑰,這隻密鑰等一下會同時作為上圖中的private key(Application)與private key(CA)的用途

openssl genrsa -out privatekey.pem 2048

接者以這支密鑰建立憑證簽章請求檔案,這邊會有一輪身家調查,但我們也不是正經(?)的公司,所以就意思意思回答一下即可

openssl req -new -key privatekey.pem -out private-csr.pem

最後再以同一隻密鑰來簽發憑證,這裡面的參數-days可以調久一點,就不會需要常常回來更新憑證

openssl x509 -req -days 365 -in private-csr.pem -signkey privatekey.pem -out certificate.pem

簡單的三個步驟我們久有了一組免費自製的SSL憑證了,接下來就要把這些憑證掛到Nginx的server上面。

安裝Nginx

Nginx是非同步框架的網頁伺服器,也可以用作反向代理、負載平衡器和HTTP快取。該軟體由俄羅斯程式設計師伊戈爾·賽索耶夫開發並於2004年首次公開發布。2011年成立同名公司以提供支援服務。2019年3月11日,Nginx公司被F5網路公司以6.7億美元收購。

維基百科

所以我們的目標就是開啟Nginx的反向代理,把80 port跟443 port的流量都導向8081 port,然後再掛一個SSL憑證來啟用https,說起來簡單做起來難,好在網路上都已經有各路大神給出的詳細指示,照著做準沒錯(吧

但我從來沒有去過Nginx的官網,之前都是直接下指令在Linux上安裝的,那要怎麼在Windows上面裝呢?很簡單,不要裝在Windows上面XDD這時候我們的好朋友WSL(Windows Subsystem for Linux)就派上用場啦!在Terminal裡面打開WSL,輸入以下指令一鍵安裝

sudo apt-install nginx

等它安裝好了之後,再輸入以下指令檢查,應該就會顯示版號了

nginx -v
nginx version: nginx/1.18.0 (Ubuntu)

接者要在Ubuntu上面把Nginx打開

sudo service nginx start
 * Starting nginx nginx

他預設會在80 port上面開啟自帶的網頁,這時候到localhost去看就可以看到Nginx自帶的首頁

Nginx反向代理(Reverse proxy)

正向的代理(proxy)是指我們在瀏覽網頁的時候,不直接用我們的電腦去跟server溝通,而是將請求發到proxy server,再由後者去向server request資料,好處是client端的IP位址可以被隱藏起來,server只會知道proxy server的IP,另外就是可以用proxy server來做一些cache,讓瀏覽時的反應速度更好。

而反向代理則是網路上的用戶在瀏覽我們的時候,不能直接跟server request資料,而是統一向reverse proxy server要求,後者根據要求的資料來將流量導至背後不同的server,好處是我們可以在同一台電腦上開多個server在不同的port,並由同一個proxy server對外接口,根據user request的網址來決定要將流量導至哪一個server,另外也可以做到load balance的功能,所謂load balance就是同樣的server我有好幾台,在request叫到proxy server的時候,去分流到不同的機器上面,降低每一台機器再同意時間的負擔,從而提供比較流暢的體驗。

reverse proxy

Reverse proxy示意圖 source

要在Nginx中做反向代理也不難,只要修改相關config檔就可以了,這邊我直接修改default的config,首先開啟設定檔,我這邊是直接用vim修改,但其實升級WSL2之後可以支援WSLg,就可以安裝一些GUI的編輯器來開啟Linux上的檔案

sudo vim /etc/nginx/sites-available/default

/etc/nginx/sites-available/default預設是readonly的檔案,若沒有修改權限不加sudo就直接編輯,最後會無法寫入檔案

設定檔打開後大概會長得像是這樣

server {
    listen 80;
    listen [::]:80 ;

    # SSL configuration
    listen 443 ssl ;
    listen [::]:443 ssl ;
    
    server_name www.example.com;
    root /var/www/nginx-default/;
    
    index index.html
    
    location / {
        # [...]
    }
    location /foo {
        # [...]
    }
    location /bar {
        root /some/other/place;
        # [...]
    }
}

每一個block會用大括號括起來,block裡面的參數則是每一行以key value;的形式去設定,如果要下comment就用放在一行的最開始。簡單介紹一下這裡面出現的一些參數:

  • server 是一個基礎的block,nginx會依照block裡面的設定去開一台server
  • listen 則是設定這台server要監聽那些port,第二列的[::]:80是設定IPv6的port
  • server_name 是設定這台sever的網域名稱,這裡如果沒有網域可以設定為_代表任何網域
  • root 是設定這台server的根目錄,如果有要serve靜態檔案的話會從這裡面找
  • index 是設定沒有path name的時候要回傳的首頁檔案
  • location 可以個別設定server在收到不同路徑的request時要套用的設定

接著來設定proxy,這邊想做的是將連進來的請求,全部導向我們的Web App,上面已經設定好監聽80與443 port了,剩下要做的就是重新導向,將location /裡面的內容清空,設定為以下這樣

 location / {
    proxy_pass  http://172.27.224.1;
    proxy_redirect off;
    proxy_set_header Host $proxy_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

}

其中

  • proxy_pass 是用來指定proxy的目的地,就是我們App所在的位置,這邊因為App是開在windows上所以IP位置是填WSL所看到的windows網卡接口
  • proxy_redirect 是用來改寫server回傳的301或302(redirect)中header的字串,這邊目前還用不到,所以先設置為off
  • proxy_set_header 則是加一些額外的header到請求的封包裡面

要如何知道windows主機在WSL上面的ip位置?可以在windows下輸入指令ipconfig找到裡面Ethernet adapter vEthernet (WSL)底下的ip就可以了

記得要將其他的location block給刪掉或是註解起來,才不會有些路徑被導向別的地方。config檔存好了之後,要重啟nginx才能套用變更,輸入以下指令

sudo service nginx restart

這時候如果連線到http://localhost,應該會發現沒有辦法順利看到我們的APP,為甚麼呢?雖然我們已經順利把流量導回windows了,但是在windows中,WSL所在的區域網路(172.27.224.1),跟localhost(127.0.0.1)所在的區域網路是不同的,所以沒辦法直接互通,我們需要將這個流量再forword過去。

Port forwording

前面提到,不同的區域網路是不能互通的,好在windows有內建portproxy的指令,讓我們可以把流量導過去,首先用管理員權限開啟terminal,輸入以下指令

netsh interface portproxy add v4tov4 listenport=80 listenaddress=172.27.224.1 connectport=8081 connectaddress=127.0.0.1

整串指令的意思是:「我們要修改網卡介面的設定,增加一個port proxy,從IPv4到IPv4,將所有進來172.27.224.1 port 80的流量都導向127.0.0.1的port 8081」
可以輸入以下指令確認是否有添加成功

 netsh interface portproxy show all

這樣就可以從http://localhost不加port,看到我們的Web App了,但這時候還沒有設定SSL的憑證,所以https是沒辦法連線的

如果要刪除這個portproxy可以用以下指令

netsh interface portproxy delete v4tov4 listenport=80 listenaddress=172.27.224.1

記得將上面指令中的172.27.224.1改為你電腦上的WSL端口IP

設定SSL憑證

現在我們nginx server也架好了,SSL憑證也簽完了,萬事俱備只欠東風,最後就只要把這兩個東西串在一起就大功告成,而串在一起也非常的簡單,就只需要在剛剛的config檔裡面,添加兩行設定,讓nginx server知道我們憑證與私鑰的檔案所在

server {
    # ...
    ssl_certificate /home/{username}/CA/certificate.pem;
    ssl_certificate_key /home/{username}/CA/privatekey.pem;
}

存檔後,一樣再執行一次重啟nginx的指令,就可以連線到https://localhost了,但由於我們的憑證是自己簽署的,所以瀏覽器會把它檔下來,但只要在進階選項中,點繼續前往就可以了

被擋下來了

疑難雜症

當然,事情不會一帆風順,一定會有什麼小小的問題藏在細節裡面。當上面一切都設定好了之後,也測試了http跟https都可以看到我們的APP,但localhost可以連到,為什麼改用固定IP就連不上了呢?!!

localhost可以連線,IP無法連線

這個問題其實跟前面的Port Forwording類似,從IP位址連進來的流量跟localhost是在不同的網路介面上,所以無法互通,解法就是如同前述指令,但將WSL的IP改為固定IP,監聽的port要有80(http)與443(https),都導向127.0.0.1的80與443,就可以用IP連線了

$IP={固定IP}
netsh interface portproxy add v4tov4 listenport=80 listenaddress=$IP connectport=80 connectaddress= 127.0.0.1
netsh interface portproxy add v4tov4 listenport=443 listenaddress=$IP connectport=443 connectaddress= 127.0.0.1

用其他電腦沒辦法連進來固定IP

明明ping也ping的到,但為甚麼從瀏覽器連進來的時候卻是一片虛無呢?原因大概是

防火牆沒開

解法也不難,打開防火牆設定(可以用windows內建的搜尋功能),點左上角的Inbounce Rules(不知道中文要翻成啥XD)

再點右上的新增規則

Type選擇Port

開啟TCP的port 80與443

允許所有連線

套用在所有類型的網路

設定一個名稱

最後按確定就可以從外面連線進來啦

結語

如果一路從上面看下來可能會發先有一個地方怪怪的,我們要大費周章地把WSL裡面的流量forword回windows,可是開在WSL上的nginx server卻一啟用就可以從localhost連上,這其實是WSL在設計的時候,windows是可以直接訪問到WSL內的localhost的,詳細的情況我沒有很清楚,但windows上的localhost與WSL內的是同一個,但如果要從WSL連回windows,就必須要用interface的IP去連,所以我們將IP進來的流量導向windows上的localhost,回直接被WSL中的nginx server抓住,但nginx在做proxy的時候,沒辦法指回windows中的localhost,才會需要先導向172.27.224.1在從windows中forword到127.0.0.1。

這麼看來一開始好像應該直接在windows上裝nginx就不會需要這麼麻煩的轉來轉去了,但也是因為這一番操作,對WSL跟一些linux上的指令更熟悉了一點,好像也不是壞事(?)。

其實為了寫這一篇出來,雖然整個網站都可以用了,但還是去查了很多SSL或nginx的細節,比起只是讓它可以work,仔細了解背後的原理,我覺得更能夠加深記憶,才不會每次都是複製貼上了事XDD

儘管如此,Nginx還有許多這次沒用到所以不熟悉的部分,像是load balance,在AWS上只要點幾個鍵就可以設定好的東東,到底背後的細節是甚麼?這要等之後有機會再來研究了(大概率不會