0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

noVNC リバース SSH トンネル

0
Posted at

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 バインドに変更する。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?