Skip to content

obs-agent Message Link Encryption

This document is for users who deploy and use Beak. It explains how Beak and self-hosted obs-agent encrypt communication, and what configuration is required.

Overview

On top of existing HTTPS/WSS, Beak adds another layer of application-layer encryption for chat payloads between Beak and obs-agent. Even if a man-in-the-middle captures WebSocket messages, they can only see base64-encoded ciphertext and a small set of routing fields. They cannot read user messages, agent replies, thinking, plans, task insights, or natural-language content in tool calls.

This is not full end-to-end encryption. The Beak server remains the trusted boundary: Beak decrypts messages, stores history, audits, searches, and displays plaintext to Web UI, obscli, and IM channels.

Protection Boundary

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

The encryption goal is to protect the obs-agent link, not to hide plaintext from Beak itself. Therefore:

  • Web UI, obscli, and IM channels do not need encryption/decryption changes.
  • Beak database message history is still stored with the existing plaintext semantics.
  • A self-hosted agent decrypts incoming messages before passing them to the LLM and local tools.
  • Natural-language payloads returned by the agent are encrypted before being sent to Beak. Beak decrypts them and then broadcasts them to user-side clients.

Encryption Scheme

The current implementation uses HPKE Auth mode. The algorithm identifier is:

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

HPKE is public-key message encryption defined by IETF RFC 9180. In simple terms:

  • The receiver owns a private key and publishes a public key.
  • The sender encrypts the message with the receiver public key.
  • The receiver decrypts the message with its private key.
  • Auth mode also binds the sender identity, so the receiver can confirm that the message came from the expected sender key.

Encrypted Content

After encryption is enabled, the following obs-agent link payloads are encrypted:

  • chat:user sent from Beak to agent
  • chat:agent sent from agent to Beak
  • chat:agent_thinking sent by agent
  • chat:agent_tool_call sent by agent
  • Natural-language or business payloads inside chat:agent_event, such as plan and task insight

Before encryption, Beak does not encrypt only the content field. It encodes both business content and business metadata as JSON, then encrypts that JSON:

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

The WebSocket message on the link still keeps minimal routing fields, such as message ID, session ID, sender ID, message type, and metadata.message_encryption. These fields are used for routing and validation, and do not carry user text.

View Message Structure On The Wire

Encrypted content is base64-encoded ciphertext bytes, not readable JSON. metadata.message_encryption is a public envelope that tells the receiver which key, algorithm, and direction to use for decryption.

Example structure:

{
  "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"
    }
  }
}

Both content and enc are base64 strings. They can be safely transmitted through JSON/WebSocket, but they cannot be interpreted as plaintext.

Key Exchange

Beak and agent each maintain their own receiving key. The rule is: the side that decrypts owns the private key.

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

Key points:

  • The agent private key is stored locally at AGENT_MESSAGE_HPKE_KEY_PATH. The default path is /var/lib/obs-agent/message-hpke-key.json.
  • If the key file does not exist, the agent automatically generates a key pair and writes it with 0600 permissions.
  • Beak does not generate or store the agent private key.
  • Beak public key is sent to the agent through the connection ack. The agent keeps it in memory only and does not write it into the local key file.
  • If the agent refreshes its own key during a long-lived connection, it sends control:message_key_update. Beak uses the new public key from that message onward.

Trace Message Encryption And Decryption

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

A man-in-the-middle on the obs-agent link can only see ciphertext, routing fields, and the envelope. Beak and agent restore plaintext inside their own trusted boundaries.

AAD Protection

AAD means Additional Authenticated Data: data that is not encrypted but is authenticated. It binds ciphertext to a specific context and prevents an attacker from copying a ciphertext into another deployment, workspace, agent, session, or direction.

The current unified AAD fields are:

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

If any of these fields is tampered with, decryption fails. Different message types do not use different AAD schemas. Business details such as tool calls, plans, and thinking are placed inside the encrypted payload so the protocol does not become unstable as business fields change.

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]

Expiration And Replay Protection

Each encryption envelope has expires_at. The current default validity period is 5 minutes. The receiver rejects decryption after expiration.

The receiver also records ciphertext fingerprints seen within a short time window to avoid processing the same ciphertext twice in one process. This is in-process replay protection. It does not promise global replay protection across Beak pods, agent restarts, or shared storage.

Deploy Beak

Beak enables message encryption by default. When enabled, Beak must have a receiving private key configured. Otherwise Beak fails to start and reports that BEAK_MESSAGE_HPKE_KEYS_JSON or BEAK_MESSAGE_HPKE_PRIVATE_KEY is missing.

Generate a Beak HPKE key:

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

Example output:

{
  "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"
}

For Kubernetes deployments, put the Beak key ring in a 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"}]'

Reference it in the Deployment:

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

Only explicitly disable encryption for temporary troubleshooting or compatibility with old deployments:

env:
  - name: BEAK_MESSAGE_ENCRYPTION_ENABLED
    value: "false"

Deploy Agent

Agent message encryption is enabled by default:

AGENT_MESSAGE_ENCRYPTION_ENABLED=true

Usually you do not need to configure the agent private key. The agent uses:

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

If the file does not exist, the agent automatically generates:

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

Directory and file permissions:

  • Key directory: 0700
  • Key file: 0600
  • The agent runtime user must be able to read and write this path.

To disable link encryption for a specific agent:

AGENT_MESSAGE_ENCRYPTION_ENABLED=false

This is an agent-level switch. Beak can still serve that agent as a plaintext-compatible connection and records observability logs and metrics. A connection that has declared encryption enabled does not silently downgrade to plaintext if encryption or decryption fails.

Key Rotation

The Beak key ring supports three statuses:

  • active: used for new messages and can decrypt.
  • decrypt_only: only decrypts old messages and is no longer used for new messages.
  • retired: no longer used to decrypt ordinary messages.

Regular Beak key rotation flow:

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 keys can be rotated manually by deleting or replacing the key file and restarting the agent, or automatically with:

AGENT_MESSAGE_HPKE_ROTATION_INTERVAL=2160h

The default 0 means automatic rotation is disabled. If the agent rotates its key during a long-lived connection, it sends control:message_key_update so Beak can use the new agent public key.

Logs And Troubleshooting

Common issues:

  • Beak fails to start and reports missing BEAK_MESSAGE_HPKE_KEYS_JSON or BEAK_MESSAGE_HPKE_PRIVATE_KEY: configure the Beak key ring, or temporarily set BEAK_MESSAGE_ENCRYPTION_ENABLED=false.
  • Agent cannot receive encrypted user messages: check whether the agent reported control:connect.metadata.message_encryption, which must include key_id and public_key.
  • Agent cannot encrypt responses: check whether the Beak ack includes metadata.message_encryption and workspace_uuid.
  • Beak decryption fails: check whether the envelope key_id exists in the Beak key ring and whether its status is active or decrypt_only.
  • Replay or expired errors appear: check whether the message was sent repeatedly, whether system time drift exists, or whether the message exceeded the default 5-minute validity period.

Debug logs can record full ciphertext and full metadata.message_encryption for local testing and controlled troubleshooting. Info/warn/error logs should not record chat plaintext or private keys. Do not keep debug logs enabled for long periods in production.

Difference From Full E2EE

Capability obs-agent link encryption Full end-to-end encryption
Prevent MITM from reading obs-agent messages Yes Yes
Beak server can see plaintext Yes No
Web UI / obscli / IM need changes No Yes
Beak can keep history, audit, search, and routing Yes Requires redesign
First implementation scope Core link implemented Not a goal

The current scheme prioritizes protecting self-hosted agent link exposure while keeping existing clients and Beak server capabilities unchanged.