使用 OpenSSH 與 Nginx 建置自架隧道服務
在部落格或開發測試時,常會遇到 本機預覽只能在 localhost:8080 的窘境。市面上有 ngrok、Cloudflare Quick Tunnels 等商業方案,也有 frp、localtunnel 需要額外客戶端的自建工具;甚至有只需要 SSH 客戶端、但依賴特定 SSH 伺服器(如 sish)的解法。本文示範如何僅憑 OpenSSH 與 Nginx,在自己的伺服器上完成安全的隧道服務,完全不依賴第三方平台。
1. 由遠端伺服器轉發埠至本機服務
使用 ssh -R 0:127.0.0.1:41535 user@remote,將遠端伺服器的隨機空閒埠(指定 0 讓核心自動分配)映射到本機的 41535 埠。這一步只需要 OpenSSH,無需額外軟體。
2. Nginx 代理與 DNS 設定
取得一個子域,例如 https://p41535.ssh.luffy.cx,在 Nginx 中設定:
location / {
proxy_pass http://127.0.0.1:41535;
# 其他 proxy 設定...
}
同時在 DNS 服務(本文以 Route 53 為例)為 *.ssh.luffy.cx 新增通配記錄,並透過 Let’s Encrypt 申請通配 SSL/TLS 憑證。本文的 acme.luffy.cx 區域已由 NixOS 自動完成憑證簽發。
3. 以埠號作為唯一「祕密」的風險
因為核心分配的隨機埠號熵值不高(僅 15‑16 位元,且偏好奇數),若僅靠埠號保護,仍有被窮舉的可能。為提升安全性,我們導入 ngx_http_secure_link_module(Nginx 的安全連結模組),透過雜湊驗證防止未授權存取。
3.1 雜湊機制
模組會對以下三個值計算 MD5 雜湊(雖非最強,但足以防止一般攻擊):
-
到期時間戳記(Unix epoch)
-
隨機埠號
-
事先設定的祕密字串
產生的雜湊再以 Base64 編碼,放入 URL 的「使用者名稱」部份,格式為:
<hash>,<expire_timestamp>
4. Nginx 端的驗證流程
-
客戶端以 HTTP Basic Auth 方式送出上述使用者名稱(大多數 HTTP 客戶端,包括
curl,皆支援)。 -
Nginx 透過
$remote_user變數取得該字串,利用map指令切割出雜湊與時間戳記。 -
交給
secure_link模組比對計算結果;若不符回傳 401 Unauthorized,若已過期回傳 410 Gone。 -
成功驗證後,移除
Authorization標頭,並將請求轉交給本機服務,同時加入 WebSocket 代理相關指令。
5. 完整設定範例(摘要)
map $remote_user $secure_link {
default "";
"~^(?<hash>[^,]+),(?<exp>\d+)$" $hash,$exp;
}
secure_link $secure_link;
secure_link_md5 "$arg_expires$arg_port$secret";
if ($secure_link = "") { return 401; }
if ($secure_link = "0") { return 410; }
proxy_set_header Authorization "";
proxy_pass http://127.0.0.1:$arg_port;
註:
$arg_port為先前由 SSH 隧道分配的埠號,$secret為自行定義的祕密字串。
結語與未來展望
透過 OpenSSH 與 Nginx 的組合,我們不僅省下商業隧道服務的費用,更能自行掌控憑證與 DNS 設定,確保資料不外流。雖然 MD5 雜湊的安全性有限,但在「埠號 + 時間戳記 + 祕密」的三層防護下,已足以抵禦一般惡意掃描。未來若需求更高,可改用 SHA‑256 或 HMAC‑SHA256,並將祕密儲存在安全金鑰管理服務(如 AWS KMS)中,進一步提升抗攻擊能力。
對於開發者而言,這套自架隧道不僅適用於部落格預覽,亦可延伸至 CI/CD 測試環境、內部 API 示範等多種場景。只要掌握好 SSH 轉發與 Nginx 安全連結的配置,便能在自己的雲端或 VPS 上,快速、可靠地提供外部存取。
