Aleph
Security

Security Overview

Defense in depth — network boundary, TLS, execution-approval gate, sandboxing, signed ledger

Security Architecture

Aleph's security model is built on several layers that operate independently. A failure in one layer does not collapse the others.

Layer 1 — Network boundary + Gateway credential
├─ Default loopback bind, zero-config operator
├─ Remote connection must present one of:
│   • device token (aleph-dt-*, long-lived)
│   • bootstrap ticket (aleph-bt-*, 5-min single-use)
│   • shared Gateway token (aleph-<uuid>, HMAC-hashed)
├─ Fail closed (per-connect gate)
└─ Native TLS + TOFU client trust

Layer 2 — Tool permissions
├─ ScopedToolService merge (global → agent → channel)
├─ Exec tier (Ask / Auto / Full) — keyed on ToolFacts metadata
├─ Explicit [policies.tool_permissions] entries win
└─ [sandbox.command_policy] hardline floor

Layer 3 — Execution approval
├─ action-aware approval gate (ApprovalAction)
├─ grant fingerprint keys on canonical args (not the redacted summary)
├─ 120s timeout ⇒ refusal (not ledgered)
└─ /deny <reason> routes the reason back to the model

Layer 4 — Sandbox
├─ WorkspaceSandbox (six-step pipeline)
├─ Platform-native enforcement:
│   • macOS — sandbox-exec + Seatbelt
│   • Linux — bwrap + Landlock + seccomp-bpf + cgroup v2
│   • Windows — AppContainer + RestrictedToken + JobObject
├─ [sandbox.command_policy] hardline command filter
└─ SSRF engine + parse normalisation

Layer 5 — Audit
├─ session event log (ToolCallApproved / ToolCallDenied)
└─ per-agent signed ledger (Ed25519 hash chain)

Network boundary + Gateway token

The trust boundary is the network boundary, gated by a single shared Gateway token.

ConfigurationMeaning
host = "127.0.0.1" (default)Only same-machine processes; zero-config operator
host = "0.0.0.0"Remote devices can reach the socket but must present a valid credential

Loopback is always credential-free (is_loopback ⇒ operator). A remote connection must present one of, in priority order:

  1. Device token (aleph-dt-*) — long-lived, bound to one paired device, SHA-256-hashed at rest.
  2. Bootstrap ticket (aleph-bt-*) — 5-minute single-use, exchanged during onboarding for a fresh device token.
  3. Shared Gateway token (aleph-<uuid>) — generated at boot by SharedTokenManager, HMAC-hashed, constant-time verified.

A valid credential = full operator authority (identical to local); a missing/invalid one is walled (the WS dispatch refuses every method but connect).

Remote transport encryption (TLS)

Loopback stays plaintext ws:// — the zero-config desktop / CLI / same-host-proxy hop is unchanged. For a non-loopback bind Aleph is now fail-closed:

  • Boot gate (check_network_exposure): a config that binds a non-loopback host with no native TLS, no trusted proxy, and allow_insecure_remote = false refuses to start with an actionable error.
  • Per-connect gate (refuse_insecure_remote): a remote client whose leg is unencrypted is rejected at the WS upgrade with 426 Upgrade Required.

Three ways to satisfy it:

TierHow
Same-host Caddy / nginx reverse proxy (recommended; needs a domain)
Native self-signed TLS (no domain; clients go through TOFU)
Native TLS with operator-provided PEM (domain, no proxy)

allow_insecure_remote = true is the escape hatch — boot gate and per-connect gate both stand down, restoring pre-hardening behavior. Only for a trusted, isolated LAN.

Client trust for self-signed TLS (TOFU)

Tier ② client trust for self-signed TLS uses Trust-on-First-Use (TOFU) (src/gateway/cert_trust.rs):

  • Shared decision core + pinned TOFU trust store (~/.aleph/data/security.db).
  • SHA-256 fingerprint + SAN / Subject parsing.
  • Approval splash page (fingerprint + SAN + TOFU / change warning).
  • Pending-cert state + approve / reject Tauri commands.
  • macOS WKWebView respondsToSelector cache invalidation; absolute-URL navigation to the trust-approval page.
  • iOS Keychain trust store + decision mirror (shares decision state with macOS).

A newly-appearing interface IP triggers cert regen (new fingerprint ⇒ re-trust once); a sans.txt sidecar tracks SAN coverage so churn stays minimal.

Pairing

Aleph now distinguishes two pairing flows:

Device pairing (remote Panel / phone / CLI)

  1. The remote Panel, in Settings → Security → "Pair new device", calls gateway.ticket.create to mint a bootstrap ticket (aleph-bt-*).
  2. The URL is resolved server-side (gateway.ticket.create returns urls, from 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.
  3. The Panel submits the ticket as connect.bootstrap_ticket; the handshake exchanges it for a fresh device_token (aleph-dt-*) that the Panel persists for subsequent reconnects.
  4. Revocation: gateway.token.rotate (regenerates the shared token, revokes every paired device, closes every remote socket) or gateway.devices.revoke {device_id} (drops just that device's live sessions back to the login wall, then closes their sockets with WS 4001 device_revoked).

Headless cores use aleph-server pair to mint a ticket directly (opens the 0600 security.db, WAL — the daemon need not be running). aleph-server bootstrap-token prints the shared Gateway token for recovery.

Channel sender pairing (Telegram / Discord / iMessage unknown sender)

A lighter flow: a 6-character alphanumeric code in the single inbox pairing store (src/gateway/pairing_store.rs), approved via the RPC pairing.list / pairing.approve (not the aleph pairing approve CLI).

[routing] pairing_code_expiry_secs controls expiry (default 24 h, 0 = never).

How the two flows differ

  • Device: long-lived credential (device_token), individually revocable, never placed in a URL / QR (only the single-use bootstrap ticket travels through those channels).
  • Channel sender: short-lived authorization (inbound router pairing store), managed through RPC.

Tool permissions

Limiting what an agent may do (as opposed to who may connect) is the job of ScopedToolService (src/tools/scoped/). It does not read IdentityContext; it merges a ToolPermissionsConfig across three tiers (global → agent → channel, most-restrictive wins). This is orthogonal to connection trust.

Three compositions happen at the same chokepoint, in this order:

  1. [policies.tool_permissions] — per-tool allow / ask / deny, exact name or glob. An explicit entry beats the tier.
  2. Exec tier (Ask / Auto / Full) — reads ToolFacts {idempotent, requires_approval}, not tool names. Default is fail-closed: a tool Aleph has never heard of is non-idempotent, hence Ask tier holds.
  3. [sandbox.command_policy] — the hardline command floor, which no tier and no permission entry can lower.

Deny hides the tool from the model and refuses the call; Ask routes to the approval gate.

Execution approval gate

The action-aware approval gate (src/sandbox/exec_approval/):

  • Sees the actual call (ApprovalAction), not the tool name.
  • Grant fingerprint = grant_fingerprint(tool, canonical args) — uses the raw canonical args, not the redacted summary (redaction would collapse distinct secrets to one placeholder, letting one grant cover another).
  • Decisions: AllowOnce / AllowSession / AllowAlways (AllowAlways is clamped to AllowSession by ExecApprovalManager::clamp_decision — no persistent allowlist exists).
  • 120 s timeout ⇒ refusal (ApprovalOutcome::is_approved excludes Timeout), not written to the denial ledger.
  • /deny <reason> (channels) or exec.approval.resolve {reason} (RPC) carries the human's reason via ExecApprovalRecord.deny_reasonResolvedDecisionApprovalResponse; the dispatch gate renders it verbatim as The user said: "…" (display layer; the ledger still keys on the fingerprint).

Sandbox

WorkspaceSandbox (src/sandbox/workspace.rs) is the one seam between exec-class tools and the OS, with a six-step pipeline:

  1. Session resolve — lazy create ~/.aleph/workspaces/<session_hash>/.
  2. cwd validatecanonicalize + starts_with(&ws.cwd); a symlink cannot escape (BUG-3 hardening).
  3. Capability checkSandboxCapabilities::is_within; out-of-baseline requests go through ApprovalGate, with per-session grant caching.
  4. Profile generate — platform-native (SBPL / bwrap args / AppContainer profile).
  5. Run — through OsSandboxDriver; default 60 s wall-clock + 1 MiB output budget.
  6. Audittracing::info! to the capability_ledger target.

The command-policy layer (src/sandbox/command_policy/) runs before the OS sandbox: a deterministic hard-filter covering fork bombs, rm -rf / / rm -rf // / /., dd of=/dev/..., mkfs, shadow-copy deletion, powershell -EncodedCommand (decoded before matching), Windows raw-disk writes, and more.

Platform support:

OSEnforcement
macOSsandbox-exec + Seatbelt profile + AllowHosts via managed proxy
Linuxbwrap + Landlock (≥ 5.13) + seccomp-bpf + cgroup v2 resource limits
WindowsAppContainer (preferred) → RestrictedToken + Low IL → baseline JobObject (three-tier soft-degrade)

Linux/Windows NetworkPolicy::AllowHosts flows through a netns→UDS→ loopback bridge (Linux, Phase B) or AppContainer capability SIDs (Windows) — a raw AllowHosts on those OSes still hard-fails with a readable rejection message.

Audit

The session event log is the live trail for approval decisions (tools/scoped/dispatch.rs::record_approval_decision writes ToolCallApproved / ToolCallDenied). Query it through the session service alongside every other event of the run that produced it.

The signed agent ledger (src/identity/, landed 2026-07-25, hardened 2026-07-26) answers who, provably:

  • Each agent holds an Ed25519 keypair (public half + fingerprint in security.db, private half in the encrypted vault).
  • Every mutating tool call, every refusal, every approval decision is appended to that agent's own hash-chained, signed chain.
  • agent_identity (tool, operator-gated) and aleph-server identity (CLI, read-only, daemon-independent) read it.

A delegated sub-agent is its own principal in the signed-ledger model — not a line on its parent's chain. It holds its own Ed25519 key and signs its own work. The acting role is injected by AllowlistToolService, which the spawner builds from the child's AgentDef.

Cross-process safety guarantees (Spec C)

The original .shared_token corruption incident had a structural cause: nothing prevented two aleph-server processes from writing the same ~/.aleph/data/ files. Spec C closes that gap with four layered protections:

  • Singleton lock: aleph-server start acquires ~/.aleph/data/aleph.lock via flock() in main() before any other state. A second start exits cleanly with code 64 + a stderr diagnostic naming the holder PID.
  • Vault writes: every write to secrets.vault goes through vault_io::VaultIo, combining an fcntl exclusive lock with tempfile + persist for atomicity.
  • JSON state writes: acp_sessions.json and equivalent files use atomic_io::write_atomic.
  • SQLite connections: every connection under ~/.aleph/data/ is opened via open_sqlite_safe, setting journal_mode=WAL, busy_timeout=5000, synchronous=NORMAL.
  • CLI dispatch: every CLI subcommand declares a CommandPolicy (NoLock / LockOnly / LockOrIpc) and routes through with_policy or run_no_lock. When the server holds the singleton lock, write subcommands forward to /v1/admin/* IPC endpoints (token rotation self-heals via a single 401 retry).

See also

  • Execution Approvalexec_tier + the action-aware approval gate
  • Pairing — device and channel-sender trust establishment
  • Sandboxing — OS-level isolation
  • IPC — WebSocket control plane + JSON-RPC

On this page