Aleph
Security

Device Pairing

Trust establishment via bootstrap tickets / device tokens (for new devices) and 6-character codes (for channel senders)

Overview

Device pairing is Aleph's trust-establishment protocol. New devices (macOS app, iOS client, web browser) or new chat senders (Telegram users, Discord members, iMessage unknowns) must complete a flow that verifies "the connection is authorized by the server owner" before interacting with Aleph.

Two trust-establishment paths:

TypeForCredential
Device pairingRemote Panel / phone / CLI clientaleph-bt-* bootstrap ticket exchanged for aleph-dt-* device token
Channel sender pairingUnknown Telegram / Discord / iMessage senders6-character alphanumeric code (single-source inbox store)

Both are managed through RPC (pairing.list / pairing.approve / pairing.reject / gateway.ticket.create) — not CLI commands.

Source locations:

  • Device ticket + token manager: src/gateway/security/device_token_manager.rs
  • Channel pairing store: src/gateway/pairing_store.rs
  • Device registry (shared with cluster nodes): src/gateway/security/store/devices.rs
  • Crypto helpers: src/gateway/security/crypto.rs
  • Security storage: src/gateway/security/store/
  • CLI: src/bin/aleph-server/commands/pair.rs, bootstrap_token.rs
  • Ticket / device RPC: src/gateway/handlers/gateway_ticket.rs, src/gateway/handlers/gateway_devices.rs

Why pairing?

Aleph is a self-hosted AI assistant with access to your files, shell, and online accounts. Allowing unrestricted connections would be a major security risk. The pairing protocol addresses three problems:

  1. Authentication — ensure the connecting device or user is authorized by the server owner.
  2. Key exchange — devices submit or receive key material during pairing that authenticates future connections.
  3. Scope limiting — chat senders gain bounded permissions at pairing.

Device pairing (remote Panel / CLI)

Flow

Device                          Gateway                         Operator
 │                                │                              │
 │── gateway.ticket.create ──────►│                              │
 │                                │                              │
 │◄── { ticket: "aleph-bt-x",    ─│                              │
 │     urls: [...] } ────────────│                              │
 │                                │                              │
 │   [device / QR scan / typed]   │                              │
 │   [URLs are server-resolved,   │                              │
 │    carry ?bt=aleph-bt-x]       │                              │
 │                                │                              │
 │── WS /ws connect {             │                              │
 │     bootstrap_ticket:          │                              │
 │     "aleph-bt-x" } ──────────►│                              │
 │                                │                              │
 │   [exchange_bootstrap_ticket]  │                              │
 │   [mint device_token]          │                              │
 │                                │                              │
 │◄── { device_token:            ─│                              │
 │     "aleph-dt-…",              │                              │
 │     role: "operator" } ────────│                              │
 │                                │                              │
 │   [device persists token       │                              │
 │    for subsequent reconnects]  │                              │

Bootstrap ticket — aleph-bt-*

The aleph-bt-* ticket is a 5-minute TTL, single-use credential. Its entire purpose is to keep long-lived credentials (device_token, shared Gateway token) out of URLs and QR codes.

gateway.ticket.create returns:

{
  "ticket": "aleph-bt-...",
  "urls": [
    "http://192.168.1.20:18790/?bt=aleph-bt-...",
    "http://[fd00::1]:18790/?bt=aleph-bt-..."
  ],
  "expires_at": "..."
}

The urls field is assembled server-side (tls::discover_interface_ips) — a Panel building it from its own window.location would emit http://127.0.0.1:<port>/… whenever the operator generates the ticket from the local desktop App, which a remote operator would never see.

Device token — aleph-dt-*

After successfully exchanging a bootstrap ticket, the handshake returns an aleph-dt-* device token. The Panel persists it and submits it as connect.device_token on every subsequent reconnect.

Device tokens:

  • Long-lived, bound to one paired device.
  • SHA-256-hashed at rest in security.db.
  • Verified through connect::resolve_connect_auth in the connect handshake.

Headless cores: the CLI path

You can mint a ticket or print the recovery token without the daemon running:

# Mint a 5-minute bootstrap ticket
aleph-server pair                    # default TTL = 300s
aleph-server pair --ttl 3600         # 1 hour (clamped to 60..=86400)

# Print the shared Gateway token (recovery / manual entry)
aleph-server bootstrap-token

aleph-server pair opens the 0600 security.db (WAL) directly — the daemon does not need to be running. aleph-server bootstrap-token prints the shared Gateway token for manual recovery (pasting into the Panel / iOS app's "Authorize" dialog).

Revocation

Two granularities, both immediately effective:

  • gateway.token.rotate — regenerates the shared token, revokes every paired device, and closes every remote socket.
  • gateway.devices.revoke {device_id} — revokes one device only: drops its live sessions back to the login wall synchronously, then closes its socket (WS 4001 device_revoked). gateway.devices.list is the inventory, with a live connected flag.

Both are scoped to device_type = 'panel' and never touch cluster nodes. device_id is client-asserted and the devices table shares one namespace with cluster nodes — the exchange refuses a device_id that already names a non-Panel device (and cluster::admit_node refuses the mirror case). Without that guard, one ticket could buy an operator token the Panel roster cannot see and no revoke path can reach.

Channel sender pairing

A new chat sender (Telegram user, Discord member, iMessage unknown) takes a lighter flow: a 6-character alphanumeric code in the single inbox pairing store (src/gateway/pairing_store.rs) — channels no longer carry their own pairing store.

Sender                        Gateway                         Operator
  │                                │                              │
  │── "Hello, Aleph" ─────────────►│                              │
  │   (from unknown sender)        │                              │
  │                                │── generate 6-char code ─►    │
  │◄── "Please verify with code:   │                              │
  │     MRKV52" ──────────────────│                              │
  │                                │── notify ──────────────────►│
  │                                │   "New sender on Telegram"   │
  │                                │   "Code: MRKV52"             │
  │                                │                              │
  │                                │◄── pairing.approve ─────────│
  │                                │    { channel, code }         │
  │                                │                              │
  │◄── "You're verified!" ─────────│                              │

6-character codes

Channel pairing codes are 6 alphanumeric characters (not 8-character Base32 — the latter was removed):

fn generate_code() -> String {
    // 6 alphanumeric characters, ambiguous chars (0/O, 1/I/l) excluded
    ...
}

Expiry and capacity

[routing] pairing_code_expiry_secs controls expiry (default 24 h, 0 = never expire). Codes are automatically rejected once expired; periodic cleanup removes expired requests from the database.

The pending-request count is capped at MAX_PENDING_REQUESTS = 10 — beyond that, new requests are refused with TooManyPending.

RPC management

# via aleph-server gateway call
aleph-server gateway call -p '{"channel":"telegram"}' pairing.list
aleph-server gateway call -p '{"channel":"telegram","code":"MRKV52"}' pairing.approve
aleph-server gateway call -p '{"channel":"telegram","code":"MRKV52"}' pairing.reject

Or through the Panel Settings → Pairing UI. There is no aleph pairing approve CLI command — it all goes through RPC.

Device vs channel pairing

DeviceChannel sender
Credentialaleph-dt-* device token (long-lived)Sender ID (inbound router store)
Bootstrapaleph-bt-* ticket (5 min TTL, single-use)6-character alphanumeric code
Storagedevices table (Panel type)pairing_requests table
Revocation granularitySingle device (gateway.devices.revoke)Sender (pairing.reject or pairing.revoke)
CarrierWS /ws connect handshake (device_token param)Inbound message
Shared secretShared Gateway token for recovery only (never in URL/QR)

Crypto primitives

Device tickets / tokens

device_token_manager generates:

  • Bootstrap ticket: uuid + TTL stamp.
  • Device token: uuid, SHA-256-hashed at rest.

Shared Gateway token

Generated at boot by SharedTokenManager, HMAC-SHA256-hashed, constant-time verified. aleph-server bootstrap-token is the only path that prints it; it must never travel in a URL, QR, or Referer header.

Security guarantees

  1. 5-minute bootstrap tickets — single-use; limits credential-leak window.
  2. Per-device revocable tokens — no need to rotate the global shared token.
  3. Fail-closed — failed exchange_bootstrap_ticket does not promote the connection.
  4. device_id namespace isolation — Panel devices and cluster-node IDs do not share.
  5. Immediate revocation — not at the next handshake.

See also

On this page