noVNC リバース SSH トンネル常駐化 手順書(汎用版)
NAT 内サーバのブラウザ VNC(noVNC)を、パブリック IP を持つ VPS を中継して HTTPS で外部公開するための汎用手順書。環境固有の値はすべてプレースホルダにしてある。
プレースホルダ一覧(最初に自分の環境の値に読み替える)
| プレースホルダ | 意味 | 例 |
|---|---|---|
<LAN_SERVER_IP> |
NAT 内サーバの LAN 内 IP | 192.168.1.10 |
<LAN_USER> |
NAT 内サーバのログインユーザ | user |
<VPS_IP> |
VPS のパブリック IP | 203.0.113.10 |
<VPS_USER> |
VPS のログインユーザ | ubuntu |
<DOMAIN> |
VPS に向いている HTTPS 化済みドメイン | example.com |
<SITE_CONF> |
nginx のサイト設定ファイル名 | mysite |
登場するマシン(まずここを把握する)
本手順には 3 台のマシンが登場する。以降、各手順の見出しに 【A】【B】【C】 のラベルで「どのマシン上で作業するか」を明記する。
| ラベル | マシン | アドレス | ログイン方法(入り方) |
|---|---|---|---|
| 【A】NAT 内サーバ | 自宅/社内 LAN 内のマシン(トンネルの発信元、noVNC 稼働機) | <LAN_SERVER_IP> |
手元 PC から同一 LAN 内で ssh <LAN_USER>@<LAN_SERVER_IP>(または直接キーボード操作) |
| 【B】VPS | パブリック IP を持つクラウド VM(トンネルの中継点。AWS Lightsail/EC2 等) | <VPS_IP> |
手元 PC から ssh <VPS_USER>@<VPS_IP>
|
| 【C】手元 PC | あなたが操作している作業端末(最終的な閲覧側) | — | (ログイン不要、自分の端末) |
作業の動線: すべての作業は【C】手元 PC のターミナルを起点に、SSH で【A】または【B】へ入って行う。各手順の冒頭に「どこに入ってから実行するか」を必ず確認すること。
構成図
【C】手元 PC 【B】VPS <VPS_IP> 【A】NAT 内サーバ <LAN_SERVER_IP>
| | (LAN/NAT内)
| | ←←← ① 【A】から SSH 接続を発信 ←←← | systemd unit: novnc-tunnel.service
| | (NAT 内→外なので接続できる) | ssh -N -R 6080:localhost:6080
| | |
| ── ② https でアクセス ──→ nginx :443 (TLS 終端) |
| <DOMAIN>/vnc/vnc.html |→ 127.0.0.1:6080 (トンネル入口) |
| | →→→ トンネル内を逆流 →→→→→→→→→→ localhost:6080 (noVNC)
- 【B】VPS の 6080 は loopback バインドのまま。インターネットに露出するのは nginx の 443(TLS)のみ。
- 暗号化の担保: 【C】⇄【B】= TLS(Let's Encrypt 等)、【B】⇄【A】= SSH トンネル。平文区間なし。
- 【B】VPS ファイアウォールの 6080 開放は不要(443 は既存開放を流用)。
- 代替アクセス手段として、二段 SSH トンネル(
ssh -L、手順⑤(3))も使用可能。 -
注意: 平文の
http://<VPS_IP>:6080直公開(GatewayPorts yes方式)は、VNC パスワードも画面転送も暗号化されずに流れるため採用しないこと。
前提条件
| 項目 | 内容 |
|---|---|
| 【A】NAT 内サーバ | Linux(systemd)、noVNC/websockify がポート 6080 で稼働中 |
| 【B】VPS | nginx + TLS 証明書(Let's Encrypt 等)で <DOMAIN> を HTTPS 配信済み |
| 【C】手元 PC | 【B】VPS への SSH 鍵を保有 |
手順全体の流れ(どこで何をするか)
| 手順 | 作業場所 | 内容 |
|---|---|---|
| ① | 【A】NAT 内サーバ | トンネル専用 SSH 鍵を作成 |
| ② | 【B】VPS | ①の公開鍵を authorized_keys に登録 |
| ③ | 【A】NAT 内サーバ | 手動で疎通テスト(known_hosts 登録) |
| ④ | 【A】NAT 内サーバ | systemd unit を作成・起動 |
| ⑤ | 【A】→【B】→【C】の順 | 3 台それぞれで動作確認 |
| ⑥ | 【A】NAT 内サーバ | 障害試験(自動復旧の確認) |
| ⑦ | 【B】VPS | nginx で HTTPS 公開(TLS 終端 + WebSocket プロキシ) |
手順
①【A】NAT 内サーバで作業 — トンネル専用の SSH 鍵を作成
入り方: 手元 PC から
ssh <LAN_USER>@<LAN_SERVER_IP>でログインして実行する。
ssh-keygen -t ed25519 -f ~/.ssh/vps-tunnel -N "" -C "tunnel-novnc"
- パスフレーズ無し(
-N "")にするのは、systemd が非対話で起動するため。 - その代わり、次のステップで鍵の用途をポート転送のみに制限する。
- 作成した公開鍵の中身を表示してコピーしておく(②で使う):
cat ~/.ssh/vps-tunnel.pub
②【B】VPS で作業 — 公開鍵を登録
入り方: 手元 PC から
ssh <VPS_USER>@<VPS_IP>でログインして実行する(①とは別のマシンなので注意)。
~/.ssh/authorized_keys に、①でコピーした公開鍵を 先頭オプション付きで 1 行追加する:
restrict,port-forwarding ssh-ed25519 AAAA...(vps-tunnel.pub の中身)... tunnel-novnc
-
restrict,port-forwardingにより、この鍵はポート転送しかできない鍵になる(シェル起動・X11 転送・agent 転送すべて不可)。 - 万一【A】側の秘密鍵が漏れても、VPS 上でコマンド実行はできない。
③【A】NAT 内サーバで作業 — 手動で 1 回疎通テスト
入り方: ①と同じく
ssh <LAN_USER>@<LAN_SERVER_IP>でログインして実行する。
ssh -i ~/.ssh/vps-tunnel -o ExitOnForwardFailure=yes \
-N -R 6080:localhost:6080 <VPS_USER>@<VPS_IP>
- 初回は host key の確認に yes と応答する(known_hosts に登録され、以後 systemd から非対話で接続できる)。
- エラーなく無言で張り付いたままになれば成功。Ctrl+C で抜ける。
④【A】NAT 内サーバで作業 — systemd unit を作成(sudo 必要)
入り方: ③に引き続き【A】上で実行する。
sudo tee /etc/systemd/system/novnc-tunnel.service > /dev/null <<'EOF'
[Unit]
Description=Reverse SSH tunnel to VPS (noVNC 6080)
After=network-online.target
Wants=network-online.target
[Service]
User=<LAN_USER>
ExecStart=/usr/bin/ssh -N \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes \
-o StrictHostKeyChecking=yes \
-i /home/<LAN_USER>/.ssh/vps-tunnel \
-R 6080:localhost:6080 <VPS_USER>@<VPS_IP>
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now novnc-tunnel.service
※ unit 内の <LAN_USER> <VPS_USER>@<VPS_IP> は heredoc 貼り付け前に実値へ置換すること。
オプションの意味(切断対策の核心)
| 設定 | 役割 |
|---|---|
ServerAliveInterval=30 + ServerAliveCountMax=3
|
30 秒ごとに keepalive を送り、3 回連続無応答(≒90 秒)で ssh が自ら異常終了する。これが無いと Wi-Fi 断などで TCP が黙って死んだとき、ssh は「接続しているつもり」のまま固まり Restart=always が発動しない |
ExitOnForwardFailure=yes |
再接続時に VPS 側で 6080 が未解放(前回セッションの残骸)だと、転送失敗のまま接続だけ成功する「ゾンビトンネル」になる。転送失敗時は ssh ごと終了させてリトライに回す |
Restart=always + RestartSec=10
|
どんな終了理由でも 10 秒後に再起動。回線復帰・スリープ復帰・VPS 再起動のいずれも自動追従 |
StrictHostKeyChecking=yes |
③ で登録済みの host key と一致しない場合は接続拒否(中間者攻撃対策)。③ を飛ばした場合は接続に失敗するので注意 |
After=network-online.target |
起動直後のネットワーク未確立時の無駄な失敗を減らす(失敗しても Restart で拾えるので保険) |
⑤ 動作確認 — 3 台を【A】→【B】→【C】の順に確認する
(1)【A】NAT 内サーバ上で(ssh <LAN_USER>@<LAN_SERVER_IP> でログインして):
systemctl status novnc-tunnel # active (running) を確認
journalctl -u novnc-tunnel -f # 接続ログ監視(Ctrl+C で抜ける)
(2)【B】VPS 上で(ssh <VPS_USER>@<VPS_IP> でログインして):
ss -tln | grep 6080 # 127.0.0.1:6080 の LISTEN を確認
(3)【C】手元 PC 上で(SSH ログインせず自分の端末でそのまま):
ssh -N -L 6080:localhost:6080 <VPS_USER>@<VPS_IP> # 二段目のトンネルを張る(張ったまま放置)
別ウィンドウ(またはブラウザ)で http://localhost:6080/vnc.html を開き、VNC パスワードでログインできれば全経路開通。
補足: この二段トンネルはトンネル区間の検証・緊急時の代替手段。通常利用は手順⑦の HTTPS URL(
https://<DOMAIN>/vnc/vnc.html)を使う(二段トンネル不要)。
⑥【A】NAT 内サーバで作業 — 障害試験(推奨)
入り方:
ssh <LAN_USER>@<LAN_SERVER_IP>でログインして実行する。
# トンネルを強制終了 → 10 秒程度で自動復旧することを確認
sudo systemctl kill novnc-tunnel
sleep 15 && systemctl status novnc-tunnel
# 回線一時切断 → 復帰後 10〜100 秒程度で自動復旧することを確認
⑦【B】VPS で作業 — nginx で HTTPS 公開(TLS 終端 + WebSocket プロキシ)
入り方: 手元 PC から
ssh <VPS_USER>@<VPS_IP>でログインして実行する。目的: ブラウザから
https://<DOMAIN>/vnc/vnc.htmlで直アクセスできるようにする。TLS は<DOMAIN>の既存証明書を流用するため、DNS 変更・証明書取得は不要。
(1) 443 を実際に担っている server block を特定する
nginx はサイト設定ファイルが複数あり、ファイルによって listen ポートが分かれていることがある(本番 443 とレガシー 8443 が別ファイル、など)。思い込みで編集せず、必ず以下で確認する:
sudo ss -tlnp | grep -E ":(80|443)\s" # 443 を握っているのが nginx か確認
grep -l "listen 443" /etc/nginx/sites-enabled/* # 443 の server block を持つファイルを特定
(2) 編集前にバックアップを取る(必須)
sudo cp -L /etc/nginx/sites-available/<SITE_CONF> ~/<SITE_CONF>.nginx.bak-$(date +%Y%m%d)
-
sites-enabled/配下はsites-available/へのシンボリックリンク。cp -aだとリンクだけコピーされて中身が保存されない罠がある →-L(実体参照)を付ける。
(3) 443 の server block 内に、以下 2 つの location を追加する
# noVNC WebSocket default path (vnc.html default path=websockify)
location = /websockify {
proxy_pass http://127.0.0.1:6080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# noVNC (reverse SSH tunnel -> 127.0.0.1:6080), TLS termination here
location /vnc/ {
proxy_pass http://127.0.0.1:6080/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
設定のポイント:
| 設定 | 理由 |
|---|---|
proxy_http_version 1.1 + Upgrade/Connection "upgrade" ヘッダ |
WebSocket 必須 3 点セット。無いと vnc.html(静的ページ)は表示できるが VNC 接続(WebSocket)だけが失敗する |
location = /websockify を root にも置く |
noVNC のデフォルト WebSocket パスはルート直下の /websockify。サブパス公開(/vnc/)だけだと、URL に ?path=vnc/websockify を付けない限り接続失敗する。root にも置けばパラメータ無しの素の URL で動く |
proxy_read_timeout 3600s |
VNC は長時間接続。デフォルト 60 秒だと無操作時に切断される |
proxy_pass http://127.0.0.1:6080/;(末尾スラッシュ) |
/vnc/xxx → /xxx にプレフィックスを剥がして転送するため |
(4) 構文検証してから反映する(この順序を厳守)
sudo nginx -t && sudo systemctl reload nginx
-
nginx -tが失敗したら reload せず、バックアップから復旧する:sudo cp ~/<SITE_CONF>.nginx.bak-* /etc/nginx/sites-available/<SITE_CONF>
(5) 動作確認
# 【B】または【C】どちらからでも可
curl -s -o /dev/null -w "%{http_code}\n" https://<DOMAIN>/vnc/vnc.html # → 200
curl -s -m 5 -o /dev/null -w "%{http_code}\n" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://<DOMAIN>/websockify # → 101 (Upgrade 成功)
curl -s -o /dev/null -w "%{http_code}\n" https://<DOMAIN>/ # → 既存サイトが無事なこと
- WebSocket の疎通は HTTP 101 が合格。curl はハンドシェイク成功後に接続を保持し続けるので
-m 5のタイムアウト必須(付けないと固まる)。
(6) ブラウザからのアクセス
https://<DOMAIN>/vnc/vnc.html を開き、「接続」→ VNC パスワードを入力。WebSocket パスを聞かれた場合は websockify または vnc/websockify のどちらでも通る。
補足・トラブルシューティング
-
再バインド失敗が続く場合(作業場所:【B】VPS): sshd 側に死んだ接続が残り、6080 の再バインドに失敗し続けることがある。【B】の
/etc/ssh/sshd_configに以下を追加してsudo systemctl restart sshすると、サーバ側からも死活監視して残骸を早く回収できる:ClientAliveInterval 30 ClientAliveCountMax 3 -
journalctl -u novnc-tunnelで "remote port forwarding failed": 上記の残骸バインド問題。数分待つか、【B】VPS 上で該当 sshd プロセスを kill する。 -
unit が即 failed になる(確認場所:【A】): 鍵パス(
/home/<LAN_USER>/.ssh/vps-tunnel)と known_hosts 登録(手順③)を確認。 -
【B】VPS が他サービスと同居している場合: トンネル中継・nginx プロキシは軽負荷だが、nginx 設定を触るときは必ずバックアップ →
nginx -t→ reload の順を守り、既存サイトの疎通確認までをワンセットにすること。 -
GatewayPorts 方式(平文直公開)は使わない:
GatewayPorts yes+http://<VPS_IP>:6080の直公開は、VNC パスワードも画面転送も平文で流れるため不可。外部公開は必ず手順⑦の nginx TLS 経由にする。 -
セキュリティ上の唯一の砦は VNC パスワード: HTTPS 化後も、URL を知っていれば誰でもパスワード入力画面まで到達できる。VNC パスワードは 12 文字以上にすること。可能なら nginx 側に Basic 認証や IP 制限を追加してもう 1 段守るとなお良い。
-
【A】の noVNC バインド範囲: noVNC(websockify)が
0.0.0.0:6080で listen していると LAN 内からも直アクセスできる。トンネル経由に一本化するなら127.0.0.1:6080バインドに変更する。