Conduit — Web SSH Terminal

⚠️ 開発中 (Work in Progress)
このプロジェクトは現在開発中であり、実環境での動作確認は行っていません。
本番環境での使用は推奨しません。
ブラウザから SSH に接続できる Web ターミナルアプリケーションです。
HashiCorp Vault が発行する短命 SSH 証明書(TTL=5分)で認証し、WebSocket 経由でリアルタイムにターミナルを操作できます。
アーキテクチャ
Browser (xterm.js)
│ WebSocket (binary frames)
▼
Go HTTP Server
├─ POST /api/connect → Vault で証明書発行 → SSH 接続確立 → セッション生成
└─ GET /ws → WebSocket ↔ SSH ストリームブリッジ
│
▼
Target SSH Server (証明書認証)
主要な設計ポイント
| 機能 |
詳細 |
| 短命 SSH 証明書 |
Vault SSH Secrets Engine で TTL=5分の証明書を発行。秘密鍵はメモリ上のみに保持しディスクに書かない |
| グレース期間再接続 |
WebSocket 切断後 15 分間は SSH セッションを保持。同じトークンで再接続すると続きから操作できる |
| バックプレッシャー |
SSH → クライアント方向のチャンネルが詰まった場合、50ms 待って送れなければドロップ。ゴルーチンのフリーズを防ぐ |
技術スタック
バックエンド
- Go 1.22
golang.org/x/crypto/ssh — SSH クライアント・証明書認証
github.com/gorilla/websocket — WebSocket サーバー
- HashiCorp Vault HTTP API — SSH 証明書署名
フロントエンド
- React 18 + TypeScript
@xterm/xterm — ターミナルエミュレータ(WebGL レンダラー)
@xterm/addon-fit — ウィンドウサイズ自動追従
@xterm/addon-webgl — GPU アクセラレーション描画
- Vite 5 — ビルドツール・開発サーバー
ディレクトリ構成
.
├── cmd/server/ # エントリポイント (main.go)
├── internal/
│ ├── api/ # HTTP ハンドラー (connect, terminal, middleware)
│ ├── config/ # 環境変数設定
│ ├── session/ # セッション状態管理・GC
│ ├── sshconn/ # 鍵生成・SSH ダイアル・証明書サイナー
│ ├── tunnel/ # WebSocket↔SSH ポンプ・PTY リサイズ
│ └── vault/ # Vault クライアント
├── pkg/token/ # セッショントークン生成
├── tests/ # E2E 統合テスト
└── frontend/ # React フロントエンド
└── src/
├── api/ # REST クライアント
├── components/ # ConnectForm, Terminal
├── hooks/ # useTerminal, useWebSocket
└── types/ # 型定義
本番デプロイ
詳細は DEPLOY.md を参照してください。
接続先 SSH サーバーのセットアップ
Conduit から接続したい SSH サーバーで以下のスクリプトを実行します:
curl -fsSL https://raw.githubusercontent.com/nagayon-935/Conduit/main/scripts/setup-ssh-server.sh \
| bash -s http://<VaultのIP>:8200
またはリポジトリをクローンしている場合:
bash scripts/setup-ssh-server.sh http://<VaultのIP>:8200
スクリプトが行うこと:
- Vault から CA 公開鍵を取得し
/etc/ssh/trusted-ca.pub に保存
/etc/ssh/sshd_config に TrustedUserCAKeys を追記
sshd を再読み込み
ユーザーガイド
SSH 接続
ブラウザで Conduit を開くと接続フォームが表示されます。
Host / Port / User を入力し、認証方式を選択して Connect を押します。
認証方式
| 方式 |
対象 |
必要な入力 |
| Vault(デフォルト) |
Vault CA を信頼するよう設定済みのサーバー |
なし(証明書は自動発行) |
| Password |
パスワード認証を許可する任意の SSH サーバー・NW機器 |
パスワード |
| Public Key |
公開鍵認証を許可する任意の SSH サーバー |
秘密鍵(PEM 貼り付けまたはファイル選択) |
パスワード・秘密鍵はブラウザの localStorage に保存されません。
複数ホストへの同時接続
+ Add host ボタンで接続先を追加すると、Connect All で全ホストへ並列接続してスプリット表示できます。
プロファイル
よく使う接続先をプロファイルとして保存できます。
- 保存: フォーム入力後、+ Save as Profile からプロファイル名を入力して保存
- 読み込み: Profiles リストのプロファイルをクリックすると Host・Port・User・認証方式が自動入力
- Import: Import ~/.ssh/config ボタンで
~/.ssh/config ファイルを選択すると一括インポート
- 記憶: 一度接続した認証方式はプロファイル・履歴に記録され、次回選択時に自動で切り替わる
タブ・レイアウト
接続中は画面上部のタブバーで複数セッションを管理できます。
| 操作 |
方法 |
| 新規接続 |
+ ボタン |
| タブ切り替え |
タブをクリック |
| タブ並び替え |
タブをドラッグ&ドロップ |
| タブを閉じる |
タブ内の ✕ ボタン |
| 左右分割 |
レイアウトボタン(⊞)から選択 |
| 上下分割 |
レイアウトボタン(⊞)から選択 |
| 2×2 グリッド |
レイアウトボタン(⊞)から選択 |
| 分割サイズ変更 |
ペイン間の仕切りをドラッグ |
プロファイルと一致するタブはプロファイル名で表示されます。
セッションの再接続
WebSocket が切断されても 15 分間はサーバー側で SSH セッションが保持されます。
ブラウザをリロードするか再度アクセスすると自動で再接続されます。
ターミナル操作
キーボードショートカット
| ショートカット |
機能 |
Ctrl + = |
フォントサイズを拡大 |
Ctrl + - |
フォントサイズを縮小 |
Ctrl + F |
ターミナル内検索を開く / 閉じる |
Enter |
次の検索結果へ |
Shift + Enter |
前の検索結果へ |
Escape |
検索を閉じる |
フォントサイズは変更後も localStorage に保持され、次回起動時に引き継がれます。
ローカル開発セットアップ
前提条件
- Go 1.22+
- Node.js 18+
- HashiCorp Vault(SSH Secrets Engine 有効化済み)
環境変数
| 変数名 |
必須 |
デフォルト |
説明 |
VAULT_ADDR |
✅ |
— |
Vault サーバーのアドレス (例: http://127.0.0.1:8200) |
VAULT_TOKEN |
✅ |
— |
Vault アクセストークン |
VAULT_SSH_ROLE |
✅ |
— |
SSH 署名に使用するロール名 |
VAULT_SSH_MOUNT |
|
ssh |
Vault SSH Secrets Engine のマウントパス |
SERVER_PORT |
|
8080 |
HTTP サーバーのリッスンポート |
GRACE_PERIOD |
|
15m |
WebSocket 切断後にセッションを保持する期間 |
SESSION_GC_INTERVAL |
|
1m |
期限切れセッションの GC 実行間隔 |
バックエンド起動
# 依存パッケージ取得
go mod download
# ビルド & 起動
make build
make run
# または開発モード(go run)
make dev
フロントエンド起動
cd frontend
npm install
npm run dev # http://localhost:5173 で起動
バックエンドは localhost:8080 で起動している必要があります。
Vite の開発サーバーが /api と /ws を自動プロキシします。
テスト
# 全テスト(レースディテクター付き)
make test
# カバレッジレポート
go test -covermode=atomic -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
カバレッジ(現状)
| パッケージ |
カバレッジ |
internal/config |
100% |
internal/vault |
89.5% |
internal/session |
86.2% |
internal/api |
76.5% |
pkg/token |
75.0% |
internal/sshconn |
71.8% |
internal/tunnel |
33.7% ※ |
※ readPump / writePump / handleControlMessage はライブ WebSocket 接続が必要なため静的テストでは未カバー
API
POST /api/connect
SSH 接続を確立してセッションを作成します。
リクエスト
// Vault 証明書認証(デフォルト)
{ "host": "192.168.1.10", "port": 22, "user": "ubuntu", "auth_type": "vault" }
// パスワード認証
{ "host": "192.168.1.10", "port": 22, "user": "admin", "auth_type": "password", "password": "..." }
// 公開鍵認証
{ "host": "192.168.1.10", "port": 22, "user": "ubuntu", "auth_type": "pubkey", "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n..." }
レスポンス (201)
{
"session_token": "a3f9...",
"expires_at": "2024-01-01T00:15:00Z",
"message": "session created"
}
GET /ws?token=<session_token>
WebSocket にアップグレードして双方向ターミナルストリームを開きます。
-
Binary frame — ターミナルの入出力データ
-
Text frame — 制御メッセージ (JSON)
{ "type": "ping" }
{ "type": "resize", "cols": 120, "rows": 40 }
ライセンス
MIT