コンテンツにスキップ

obs-agent メッセージリンク暗号化

本ドキュメントは Beak をデプロイ・利用するユーザーを対象としています。Beak とセルフホスト型 obs-agent 間の通信暗号化の仕組みと、必要な設定について説明します。

概要

既存の HTTPS/WSS に加え、Beak は Beak と obs-agent 間のチャットペイロードに対してアプリケーション層の暗号化をもう一層追加します。中間者が WebSocket メッセージを傍受しても、base64 エンコードされた暗号文と少数のルーティングフィールドしか確認できません。ユーザーメッセージ、エージェント応答、思考、プラン、タスクインサイト、ツールコール内の自然言語コンテンツは読み取れません。

これは完全なエンドツーエンド暗号化ではありません。Beak サーバーが信頼境界として機能します。Beak はメッセージを復号し、履歴の保存、監査、検索を行い、Web UI、obscli、IM チャネルに平文を表示します。

保護境界

flowchart LR
    U[User<br/>Web UI / obscli / IM] -->|HTTPS<br/>Plain business API| B[Beak Server<br/>Trusted boundary]
    B -->|WSS + HPKE application ciphertext| A[Self-hosted obs-agent]
    A -->|WSS + HPKE application ciphertext| B
    B -->|HTTPS<br/>Plain display/history/audit| U

    M((Man-in-the-middle))
    M -.Can only see ciphertext.-> B
    M -.Can only see ciphertext.-> A

暗号化の目的は obs-agent リンクを保護することであり、Beak 自体から平文を隠すことではありません。そのため:

  • Web UI、obscli、IM チャネルは暗号化/復号の変更を必要としません。
  • Beak データベースのメッセージ履歴は既存の平文セマンティクスで保存されます。
  • セルフホスト型エージェントは、受信メッセージを LLM やローカルツールに渡す前に復号します。
  • エージェントから返される自然言語ペイロードは、Beak に送信される前に暗号化されます。Beak はそれらを復号し、ユーザー側クライアントにブロードキャストします。

暗号化方式

現在の実装は HPKE Auth モードを使用しています。アルゴリズム識別子は以下のとおりです:

hpke-auth-x25519-hkdf-sha256-aes-256-gcm

HPKE は IETF RFC 9180 で定義された公開鍵メッセージ暗号化です。簡単に言えば:

  • 受信者は秘密鍵を所有し、公開鍵を公開します。
  • 送信者は受信者の公開鍵でメッセージを暗号化します。
  • 受信者は自身の秘密鍵でメッセージを復号します。
  • Auth モードでは送信者の ID もバインドされるため、受信者はメッセージが期待される送信者鍵から送られたことを確認できます。

暗号化対象コンテンツ

暗号化が有効化されると、以下の obs-agent リンクペイロードが暗号化されます:

  • Beak からエージェントに送信される chat:user
  • エージェントから Beak に送信される chat:agent
  • エージェントが送信する chat:agent_thinking
  • エージェントが送信する chat:agent_tool_call
  • chat:agent_event 内の自然言語またはビジネスペイロード(プランやタスクインサイトなど)

暗号化前、Beak は content フィールドのみを暗号化するのではありません。ビジネス content とビジネス metadata の両方を JSON としてエンコードし、その JSON を暗号化します:

{
  "content": "Plaintext body",
  "metadata": {
    "event_type": "task_insight",
    "tool_call_content": "..."
  }
}

リンク上の WebSocket メッセージには、メッセージ ID、セッション ID、送信者 ID、メッセージタイプ、metadata.message_encryption など最小限のルーティングフィールドが残ります。これらのフィールドはルーティングと検証に使用され、ユーザーテキストは含みません。

ワイヤ上のメッセージ構造

暗号化された content は base64 エンコードされた暗号文バイトであり、読み取り可能な JSON ではありません。metadata.message_encryption は公開エンベロープで、受信者に復号に使用する鍵、アルゴリズム、方向を伝えます。

構造の例:

{
  "type": "chat:user",
  "id": "msg_123",
  "session_id": "session_abc",
  "sender_id": "user:u1",
  "receiver_id": "agent_xxx",
  "content": "BASE64_CIPHERTEXT",
  "metadata": {
    "message_encryption": {
      "version": "v1",
      "alg": "hpke-auth-x25519-hkdf-sha256-aes-256-gcm",
      "key_id": "hpke-agent-key",
      "sender_key_id": "beak-2026-06",
      "direction": "beak_to_agent",
      "enc": "BASE64_HPKE_ENCAPSULATED_KEY",
      "expires_at": "2026-06-30T10:05:00Z"
    }
  }
}

contentenc はどちらも base64 文字列です。JSON/WebSocket を通じて安全に送信できますが、平文として解釈することはできません。

鍵交換

Beak とエージェントはそれぞれ自身の受信用鍵を管理します。ルールは:復号する側が秘密鍵を所有する、というものです。

sequenceDiagram
    autonumber
    participant Agent as obs-agent
    participant Beak as Beak

    Agent->>Agent: Read or generate agent private key on startup
    Agent->>Beak: control:connect<br/>metadata.message_encryption={agent key_id, agent public_key}
    Beak->>Beak: Record agent public key on current connection
    Beak-->>Agent: ack<br/>metadata.message_encryption={beak key_id, beak public_key}
    Agent->>Agent: Store Beak public key in memory only

要点:

  • エージェント秘密鍵は AGENT_MESSAGE_HPKE_KEY_PATH にローカル保存されます。デフォルトパスは /var/lib/obs-agent/message-hpke-key.json です。
  • 鍵ファイルが存在しない場合、エージェントは自動的に鍵ペアを生成し、0600 パーミッションで書き込みます。
  • Beak はエージェント秘密鍵を生成・保存しません。
  • Beak 公開鍵は接続 ack を通じてエージェントに送信されます。エージェントはメモリ内のみに保持し、ローカル鍵ファイルには書き込みません。
  • エージェントが長時間接続中に自身の鍵を更新する場合、control:message_key_update を送信します。Beak はそのメッセージ以降、新しい公開鍵を使用します。

メッセージ暗号化・復号のトレース

sequenceDiagram
    autonumber
    participant User as User-side client
    participant Beak as Beak
    participant Agent as obs-agent
    participant LLM as LLM/Tools

    User->>Beak: Send user message
    Beak->>Beak: Store plaintext history and choose target agent
    Beak->>Beak: Encrypt JSON payload with agent public key
    Beak->>Agent: chat:user<br/>content=BASE64_CIPHERTEXT
    Agent->>Agent: Decrypt with agent private key
    Agent->>LLM: Plaintext enters agent runtime
    LLM-->>Agent: Generate reply/event/tool call
    Agent->>Agent: Encrypt JSON payload with Beak public key
    Agent->>Beak: chat:agent / thinking / tool_call / event<br/>content=BASE64_CIPHERTEXT
    Beak->>Beak: Decrypt with Beak private key and validate context
    Beak-->>User: Plain display, history, audit, stream

obs-agent リンク上の中間者は暗号文、ルーティングフィールド、エンベロープしか確認できません。Beak とエージェントはそれぞれ自身の信頼境界内で平文を復元します。

AAD 保護

AAD は Additional Authenticated Data(追加認証データ)を意味します。暗号化はされませんが認証されるデータです。暗号文を特定のコンテキストにバインドし、攻撃者が暗号文を別のデプロイメント、ワークスペース、エージェント、セッション、方向にコピーすることを防ぎます。

現在の統一 AAD フィールドは以下のとおりです:

  • workspace_uuid
  • deployment_id
  • agent_uuid
  • session_uuid
  • message_uuid
  • message_type
  • direction
  • expires_at
  • version
  • key_id
  • algorithm

これらのフィールドのいずれかが改ざんされると、復号は失敗します。異なるメッセージタイプでも AAD スキーマは異なりません。ツールコール、プラン、思考などのビジネス詳細は暗号化ペイロード内に配置されるため、ビジネスフィールドの変更に伴いプロトコルが不安定になることはありません。

flowchart TD
    C[Ciphertext content] --> D{Validate before decryption}
    E[message_encryption envelope] --> D
    A[AAD context<br/>deployment / workspace / agent / session / message / direction / expires_at] --> D
    D -->|All match| P[Decode JSON payload]
    D -->|Any mismatch| R[Reject message<br/>structured error]

有効期限とリプレイ保護

各暗号化エンベロープには expires_at があります。現在のデフォルト有効期間は 5 分です。受信者は有効期限を過ぎた復号を拒否します。

受信者は短い時間ウィンドウ内に確認された暗号文フィンガープリントも記録し、1 プロセス内で同じ暗号文を 2 回処理することを回避します。これはインプロセスのリプレイ保護です。Beak Pod 間、エージェント再起動間、共有ストレージ間でのグローバルなリプレイ保護は保証しません。

Beak のデプロイ

Beak はデフォルトでメッセージ暗号化を有効にしています。有効化されている場合、Beak には受信用秘密鍵の設定が必要です。設定がない場合、Beak は起動に失敗し BEAK_MESSAGE_HPKE_KEYS_JSON or BEAK_MESSAGE_HPKE_PRIVATE_KEY が不足している旨を報告します。

Beak HPKE 鍵の生成:

beak -generate-message-hpke-key -message-hpke-key-id beak-2026-06

出力例:

{
  "key_id": "beak-2026-06",
  "private_key": "BASE64_PRIVATE_KEY",
  "public_key": "BASE64_PUBLIC_KEY",
  "algorithm": "hpke-auth-x25519-hkdf-sha256-aes-256-gcm",
  "status": "active"
}

Kubernetes デプロイメントの場合、Beak キーリングを Secret に格納します:

kubectl create secret generic beak-message-hpke \
  --from-literal=BEAK_MESSAGE_HPKE_ACTIVE_KEY_ID=beak-2026-06 \
  --from-literal=BEAK_MESSAGE_HPKE_KEYS_JSON='[{"key_id":"beak-2026-06","private_key":"BASE64_PRIVATE_KEY","status":"active"}]'

Deployment で参照します:

envFrom:
  - secretRef:
      name: beak-message-hpke

一時的なトラブルシューティングや旧デプロイメントとの互換性のためにのみ、明示的に暗号化を無効化します:

env:
  - name: BEAK_MESSAGE_ENCRYPTION_ENABLED
    value: "false"

エージェントのデプロイ

エージェントのメッセージ暗号化はデフォルトで有効です:

AGENT_MESSAGE_ENCRYPTION_ENABLED=true

通常、エージェント秘密鍵の設定は不要です。エージェントは以下を使用します:

AGENT_MESSAGE_HPKE_KEY_PATH=/var/lib/obs-agent/message-hpke-key.json

ファイルが存在しない場合、エージェントは自動的に生成します:

{
  "version": "v1",
  "algorithm": "hpke-auth-x25519-hkdf-sha256-aes-256-gcm",
  "key_id": "hpke-...",
  "private_key": "BASE64_PRIVATE_KEY",
  "public_key": "BASE64_PUBLIC_KEY"
}

ディレクトリおよびファイルのパーミッション:

  • 鍵ディレクトリ:0700
  • 鍵ファイル:0600
  • エージェントランタイムユーザーがこのパスの読み書きができる必要があります。

特定のエージェントでリンク暗号化を無効にするには:

AGENT_MESSAGE_ENCRYPTION_ENABLED=false

これはエージェントレベルのスイッチです。Beak は引き続きそのエージェントを平文互換接続として処理し、オブザーバビリティログとメトリクスを記録します。暗号化有効を宣言した接続は、暗号化または復号が失敗しても平文へサイレントにダウングレードされません。

鍵ローテーション

Beak キーリングは 3 つのステータスをサポートします:

  • active:新しいメッセージに使用され、復号も可能。
  • decrypt_only:古いメッセージの復号のみ行い、新しいメッセージには使用されない。
  • retired:通常メッセージの復号に使用されなくなった。

通常の Beak 鍵ローテーションフロー:

sequenceDiagram
    autonumber
    participant Ops as Ops
    participant Secret as K8s Secret / Vault
    participant Beak as Beak pods
    participant Agent as agents

    Ops->>Ops: Generate new Beak key
    Ops->>Secret: New key=active<br/>old key=decrypt_only
    Ops->>Beak: Rolling restart
    Beak-->>Agent: New connection ack sends new Beak public key
    Agent->>Beak: Later messages use new key_id
    Ops->>Secret: Remove or retire old key after grace period
    Ops->>Beak: Rolling restart again

エージェント鍵は、鍵ファイルを削除または置換してエージェントを再起動することで手動ローテーションできます。または以下の設定で自動ローテーションが可能です:

AGENT_MESSAGE_HPKE_ROTATION_INTERVAL=2160h

デフォルトの 0 は自動ローテーション無効を意味します。エージェントが長時間接続中に鍵をローテーションした場合、control:message_key_update を送信して Beak が新しいエージェント公開鍵を使用できるようにします。

ログとトラブルシューティング

よくある問題:

  • Beak が起動に失敗し BEAK_MESSAGE_HPKE_KEYS_JSON or BEAK_MESSAGE_HPKE_PRIVATE_KEY が不足していると報告される:Beak キーリングを設定するか、一時的に BEAK_MESSAGE_ENCRYPTION_ENABLED=false を設定します。
  • エージェントが暗号化されたユーザーメッセージを受信できない:エージェントが control:connect.metadata.message_encryption を報告したか確認します。key_idpublic_key が含まれている必要があります。
  • エージェントが応答を暗号化できない:Beak ack に metadata.message_encryptionworkspace_uuid が含まれているか確認します。
  • Beak の復号が失敗する:エンベロープの key_id が Beak キーリングに存在し、ステータスが active または decrypt_only であるか確認します。
  • リプレイまたは期限切れエラーが発生する:メッセージが繰り返し送信されていないか、システム時刻のずれが存在しないか、メッセージがデフォルトの 5 分間の有効期間を超過していないか確認します。

デバッグログはローカルテストや管理されたトラブルシューティングのために、完全な暗号文と完全な metadata.message_encryption を記録できます。Info/warn/error ログはチャット平文や秘密鍵を記録すべきではありません。本番環境でデバッグログを長期間有効にしないでください。

完全な E2EE との違い

機能 obs-agent リンク暗号化 完全なエンドツーエンド暗号化
中間者による obs-agent メッセージの読み取りを防止 はい はい
Beak サーバーが平文を確認可能 はい いいえ
Web UI / obscli / IM の変更が必要 いいえ はい
Beak が履歴、監査、検索、ルーティングを維持可能 はい 再設計が必要
初期実装スコープ コアリンクを実装済み 目標外

現在の方式は、既存のクライアントと Beak サーバーの機能を変更せずに、セルフホスト型エージェントリンクの露出を保護することを優先しています。

フィードバック

このページは役に立ちましたか?