Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Feb 25, 2026 - 日記

OpenClawのデバイス認証を読み解く: nonce署名・トークンローテーション・5分タイムアウトの設計意図

OpenClawのデバイス認証を読み解く: nonce署名・トークンローテーション・5分タイムアウトの設計意図 cover image

OpenClaw を初めてセットアップしたとき、ちょっとした違和感を覚えました。通常のWebサービスなら当たり前にある「パスワード登録」や「アカウント作成」がないんです。

「認証どうなってるんだ?」と気になってドキュメントを読み始めたら、Dashboard UIとGatewayのペアリング機構が思いのほか面白い設計になっていました。Challenge-Response認証、デバイストークンのローテーション、5分間のタイムアウトなど、細かい設計判断が積み重なっていて、「ローカル優先だけどセキュアに」という思想が一貫しています。

本記事では、OpenClawのGateway Protocolを読み解きながら、これらの設計がどう組み合わさって「信頼」を構築しているかを解説します。

OpenClawのアーキテクチャ: Gatewayとは

まず前提として、OpenClawのアーキテクチャを簡単に整理します。

OpenClaw は、WhatsApp、Telegram、Slack、Discord、WebChatなど複数のメッセージングプラットフォームと接続できるAIアシスタントです。その中心にあるのがGatewayです。

Gatewayの役割

  • 単一のコントロールプレーン: すべてのクライアント(CLI、WebUI、macOSアプリ、iOS/Androidノード)が接続する中央サーバー
  • メッセージングプラットフォームの管理: WhatsApp(Baileys経由)、Telegram(grammY経由)などのセッションを保持
  • デバイス認証の管理: どのデバイスが接続を許可されるかを決定

デフォルトでは 127.0.0.1:18789 でサーバーが起動します。

Gateway Protocol

OpenClawのGateway Protocolは、JSON-RPCライクな通信を行います。すべての接続は、最初に connect リクエストを送る必要があります。これがハンドシェイク(認証フロー)の起点です。

ペアリングフローの全体像

OpenClawのペアリングは「Gateway-Owned Pairing」と呼ばれる方式で、Gatewayが信頼の源です。

基本フロー

sequenceDiagram
    participant Client as クライアント
    participant Gateway as Gateway

    Gateway->>Client: connect.challenge (nonce)
    Client->>Gateway: connect (署名付き)
    Gateway->>Gateway: 署名検証
    alt 新規デバイス
        Gateway->>Gateway: ペアリングリクエスト保存
        Gateway->>Client: hello-ok (トークンなし)
        Note over Gateway: 承認待ち(5分間有効)
        Gateway->>Client: node.pair.requested イベント
        Note over Gateway: ユーザーが承認
        Gateway->>Client: デバイストークン発行
    else 既知デバイス
        Gateway->>Client: hello-ok (デバイストークン)
    end

3つの重要な要素

  1. Challenge-Response認証: nonceを使った署名検証
  2. デバイストークン: 承認後に発行される認証トークン
  3. 5分タイムアウト: 未承認リクエストの自動失効

それぞれの設計意図を掘り下げます。

Challenge-Response: なぜnonceが必要か

リプレイ攻撃の防止

接続確立時の connect リクエストが盗聴・再利用されるリスクがあります。Challenge-Response認証は、このリプレイ攻撃を防ぐための仕組みです。

フロー詳細

1. Gatewayがnonceを発行

クライアントが接続を開始すると、Gatewayは最初に connect.challenge イベントを送信します。

1
2
3
4
5
6
7
8
{
  "type": "event",
  "event": "connect.challenge",
  "payload": {
    "nonce": "ランダムな文字列",
    "ts": 1737264000000
  }
}

2. クライアントがnonceに署名

クライアントは、自分の秘密鍵でnonceに署名し、connect リクエストに含めます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "type": "req",
  "id": "...",
  "method": "connect",
  "params": {
    "device": {
      "id": "device_fingerprint",
      "publicKey": "...",
      "signature": "...",  // nonceの署名
      "signedAt": 1737264000000,
      "nonce": "..."
    }
  }
}

3. Gatewayが署名を検証

Gatewayは、クライアントの公開鍵を使って署名を検証します。これにより、以下を確認できます。

  • クライアントが秘密鍵を保持している(なりすましではない)
  • nonceが改ざんされていない
  • リクエストが最新のもの(古いnonceの再利用ではない)

設計意図

  • リプレイ攻撃の防止: 過去の connect リクエストを再利用できない
  • なりすまし防止: 秘密鍵を持たないクライアントは署名を作成できない
  • タイムスタンプ検証: signedAtts の差分で、古すぎるリクエストを拒否できる

この仕組みは、SSH公開鍵認証と同じ原理です。

デバイストークン: ローテーションの設計

トークンのライフサイクル

OpenClawのデバイストークンは、以下のライフサイクルを持ちます。

stateDiagram-v2
    [*] --> PendingRequest: connect (新規デバイス)
    PendingRequest --> Approved: 承認
    PendingRequest --> Rejected: 拒否
    PendingRequest --> Expired: 5分経過
    Approved --> TokenIssued: デバイストークン発行
    TokenIssued --> Reconnect: 次回接続
    Reconnect --> TokenRotated: 再ペアリング時
    Rejected --> [*]
    Expired --> [*]

トークンローテーションの仕組み

初回承認時:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  "type": "res",
  "id": "...",
  "ok": true,
  "payload": {
    "type": "hello-ok",
    "protocol": 3,
    "auth": {
      "deviceToken": "新しいトークン",
      "role": "operator",
      "scopes": ["operator.read", "operator.write"]
    }
  }
}

再ペアリング時:

  • 既存のトークンは無効化される
  • 新しいトークンが発行される

設計意図

なぜトークンをローテーションするのか?

  1. 漏洩時の影響を限定: トークンが漏洩しても、再ペアリングで無効化できる
  2. 権限の再確認: 再ペアリング時に、ユーザーが明示的に承認する
  3. 長期トークンのリスク回避: 永続的なトークンは避け、定期的な更新を促す

この設計は、OAuth 2.0のRefresh Tokenローテーションと似た考え方です。

トークンの保存場所

トークンは、Gatewayのステートディレクトリに保存されます。

~/.openclaw/
├── nodes/
│   ├── paired.json    # 承認済みデバイスとトークン
│   └── pending.json   # 承認待ちリクエスト

セキュリティ上の注意:

  • paired.json は秘密情報として扱う
  • ファイルパーミッションを適切に設定する(chmod 600
  • Gitリポジトリには含めない

5分タイムアウト: なぜこの値か

ペアリングリクエストの有効期限

OpenClawでは、未承認のペアリングリクエストは5分後に自動的に失効します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 疑似コード
const PAIRING_REQUEST_TTL = 5 * 60 * 1000; // 5分

function createPairingRequest(deviceId) {
  const request = {
    deviceId,
    createdAt: Date.now(),
    expiresAt: Date.now() + PAIRING_REQUEST_TTL
  };
  pendingRequests.set(deviceId, request);
  
  setTimeout(() => {
    if (pendingRequests.has(deviceId)) {
      pendingRequests.delete(deviceId);
      emit('node.pair.resolved', { status: 'expired' });
    }
  }, PAIRING_REQUEST_TTL);
}

設計意図

なぜ5分なのか?

  1. DoS攻撃の防止: 大量のペアリングリクエストでメモリを消費させる攻撃を防ぐ
  2. ユーザー体験: 5分あれば、ユーザーがCLIやUIで承認操作を完了できる
  3. セキュリティ: 長すぎる有効期限は、攻撃者に時間的猶予を与える

他のシステムとの比較:

システムタイムアウト用途
OpenClaw5分デバイスペアリング
Bluetooth2分デバイスペアリング
OAuth 2.010分認可コード
SSHなし公開鍵登録(手動)

5分という値は、Bluetoothのペアリングタイムアウト(約2分)よりは長く、OAuth 2.0の認可コード(10分)よりは短い、バランスの取れた設定です。

CLIでの承認フロー

ペアリングリクエストは、CLIで確認・承認できます。

1
2
3
4
5
6
7
8
# 承認待ちリクエストを確認
openclaw nodes pending

# 承認
openclaw nodes approve <requestId>

# 拒否
openclaw nodes reject <requestId>

ヘッドレスサーバーでも、SSH経由でこのコマンドを実行すれば承認できます。

ローカル自動承認: UXとセキュリティのトレードオフ

ローカル接続の特別扱い

OpenClawには、ローカル接続の自動承認という機能があります。

ローカル接続の定義:

  • ループバック接続(127.0.0.1
  • Gatewayホストの自身のTailnetアドレス

これらの接続は、ペアリング承認をスキップして自動的に許可されます。

設計意図

なぜローカルだけ自動承認するのか?

  1. UXの改善: 同一ホストのCLI/UIは、毎回承認を求めると煩雑
  2. セキュリティの妥当性: ローカルホストにアクセスできる時点で、すでに信頼されている
  3. Tailnetの信頼: Tailscaleのネットワーク内は、すでに認証済み

トレードオフ:

  • ✅ 利便性: 開発時の体験が向上
  • ❌ リスク: ローカルホストが侵害された場合、自動承認される

macOSアプリのサイレント承認

macOSアプリには、さらに高度な自動承認機能があります。

条件:

  1. ペアリングリクエストが silent: true フラグを持つ
  2. アプリがSSH接続でGatewayホストを検証できる

この場合、ユーザーに確認なしで承認されます。失敗した場合は、通常の「承認/拒否」プロンプトにフォールバックします。

リモートアクセス時の考慮事項

SSH/Tailscaleの推奨

OpenClawは、リモートアクセス時に以下を推奨しています。

推奨方法:

  1. Tailscale: VPN不要で、Tailnetアドレスで接続
  2. SSH Tunnel: ローカルポートフォワーディング
1
2
# SSH Tunnel経由で接続
ssh -N -L 18789:127.0.0.1:18789 user@host

非推奨:

  • 公開インターネットへの直接露出
  • 認証なしのHTTP接続

TLS + ピンニング

リモートアクセス時は、TLS + 証明書ピンニングを有効にできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# openclaw.json
{
  "gateway": {
    "tls": {
      "enabled": true,
      "cert": "/path/to/cert.pem",
      "key": "/path/to/key.pem"
    }
  }
}

クライアント側で証明書のフィンガープリントを検証:

1
openclaw connect --tls-fingerprint "sha256:..."

これにより、中間者攻撃(MITM)を防げます。

トラブルシューティング: よくあるエラー

“pairing required (1008)” エラー

原因:

  1. デバイスIDが未承認
  2. トークンが無効化された
  3. HTTPSなしでリモート接続している

対処法:

1
2
3
4
5
6
7
8
# 1. トークンを再生成
openclaw auth regenerate

# 2. ペアリングリクエストを確認
openclaw nodes pending

# 3. 承認
openclaw nodes approve <requestId>

Docker環境での接続エラー

Docker内でGatewayを実行している場合、ホストからの接続が「リモート」と判定されることがあります。

対処法:

1
2
3
4
5
6
7
# docker-compose.yml
services:
  openclaw:
    environment:
      - OPENCLAW_GATEWAY_TOKEN=your-token
    ports:
      - "127.0.0.1:18789:18789"  # ループバックにバインド

クライアント側でトークンを設定:

1
2
export OPENCLAW_GATEWAY_TOKEN=your-token
openclaw connect

まとめ: 設計から学べること

OpenClawのペアリングプロトコルは、以下の設計判断が組み合わさっています。

要素設計意図トレードオフ
Challenge-Responseリプレイ攻撃防止実装の複雑さ
トークンローテーション漏洩時の影響を限定再ペアリングの手間
5分タイムアウトDoS攻撃防止ユーザー体験
ローカル自動承認UXの改善ローカル侵害時のリスク

これらは、「ローカル優先だけどセキュアに」という一貫した思想を反映しています。

個人的には、この設計が面白いのは、完璧なセキュリティを目指していない点だと思います。ローカル自動承認のように、「実用上の妥当性」を優先している箇所があります。

OpenClawは「パーソナルアシスタント」という前提で設計されているため、エンタープライズ向けのZero Trust原則とは異なる判断をしています。この「用途に応じた設計」が、実際に使いやすいツールを生む鍵なのかもしれません。

興味のある方は、公式ドキュメント でプロトコル仕様を確認してみてください。

参考リンク