ADR 0020: Connection Security — mTLS, Proxies, and Network Overlays
Status
Implemented (2026-02-18)
Update (2026-02-17): Phase 0 (WebSocket transport + cookie auth) implemented in BT-683. ADR accepted based on validated implementation.
Update (2026-02-18): Phases 0–2 complete (BT-683, BT-691, BT-692). Phase 3 (SPIFFE/SPIRE) remains future.
Update (2026-03-15): TLS distribution support removed (PR #1401). beamtalk workspace attach and beamtalk transcript moved under beamtalk workspace subcommand. References below reflect the original design; current commands are beamtalk workspace attach and beamtalk workspace transcript.
Context
Beamtalk has several TCP communication channels that need security consideration:
| Channel | Transport | Current Security | Users |
|---|---|---|---|
| REPL ↔ Workspace | WebSocket ws://127.0.0.1:{port} (OS-assigned ephemeral) | Loopback + cookie handshake (BT-683) | beamtalk repl |
| CLI ↔ Compiler | OTP Port (stdin/stdout) | Process isolation (ADR 0022) | beamtalk build, LSP |
| Distributed Erlang | Erlang distribution protocol | Cookie file (chmod 600) | Workspace-to-workspace |
| Web terminal | Not yet implemented | — | Browser-based REPL |
| Remote attach | Not yet implemented | — | beamtalk workspace attach prod@host |
Threat Model
Local development (single machine, single user):
- REPL bound to
127.0.0.1— only local processes can connect - Compiler uses OTP Port (stdin/stdout) — process isolation, no network exposure
- Erlang cookie — prevents accidental cross-workspace connections
- Threat level: low. The OS provides adequate isolation.
Local development (shared machine, CI runner, Docker --net=host):
- Multiple users/processes share the loopback interface
- Any local process can connect to
127.0.0.1:{port}— but cookie handshake (BT-683) requires knowledge of the workspace cookie file (chmod 600) - The REPL is an arbitrary code execution endpoint — a valid cookie = full RCE as the workspace owner
- Erlang cookie protects distributed Erlang; workspace cookie protects the REPL WebSocket protocol
- Threat level: medium. Loopback binding + cookie auth provides adequate isolation on shared machines where filesystem permissions are enforced.
Web terminal (browser on same or different machine):
- Browser cannot connect to raw TCP — needs an HTTP/WebSocket proxy
- If proxy listens on
0.0.0.0or a non-loopback interface, the workspace is network-exposed - CSRF, session hijacking, DNS rebinding attacks become real threats
- Token leakage via URL (browser history, Referer headers, screen sharing)
- A stolen token = full RCE on the host, not just "session hijack"
- Threat level: high. The REPL executes arbitrary code — standard web security is the minimum, not the ceiling.
Remote attach (production debugging, multi-machine clusters):
- Distributed Erlang cookies are symmetric shared secrets with no rotation
- Erlang distribution protocol is unencrypted by default
- Anyone who can reach the port and knows the cookie has full control
- Threat level: high. Network encryption + strong identity required.
Why WebSocket (Not Unix Socket or Raw TCP) for the REPL
Update (2026-02-17): The REPL now uses WebSocket over TCP (BT-683), not raw TCP. The original reasoning for choosing TCP over Unix sockets still applies — WebSocket inherits TCP's network capability while adding HTTP-upgrade compatibility for browser access and standard proxy support.
- The workspace is an Erlang/OTP process. Cowboy (HTTP/WebSocket server) is native to the BEAM ecosystem and well-supported. Unix socket support in Erlang requires workarounds (
afunixor NIF-based solutions) and isn't portable to Windows. - Remote attach needs network transport. The REPL protocol must work over the network for
beamtalk workspace attach. WebSocket is HTTP-upgrade-compatible, so standard proxies (Caddy, nginx) can terminate TLS without custom bridging code. - Browser access requires HTTP. Raw TCP cannot be reached from a browser. WebSocket provides bidirectional communication over HTTP, enabling browser-based workspaces (ADR 0017).
- The compiler is now an OTP Port (ADR 0022). The Unix socket daemon was eliminated — the compiler runs as a child process of the workspace, communicating via stdin/stdout.
Trade-off acknowledged: WebSocket on localhost is less secure than Unix sockets on shared machines. We mitigate this with cookie-based authentication on WebSocket connect (see Layer 1 below).
Design Principles
- Local stays local.
127.0.0.1binding is the default and cannot be overridden by accident. No "listen on all interfaces" flag. - Security at the right layer. Don't reinvent TLS inside the beamtalk protocol. Use the transport layer (OTP
ssl, network overlays, reverse proxies). - Zero-config for dev, explicit opt-in for remote. Local REPL should Just Work™. Remote access requires deliberate setup.
- Composable with existing infrastructure. Support Tailscale, WireGuard, SPIFFE, and standard reverse proxies — don't mandate any single approach.
Decision
Layer 1: Local connections (127.0.0.1 + cookie handshake)
Local REPL connections remain loopback-only. The compiler uses an OTP Port (stdin/stdout, no network). The REPL protocol uses WebSocket as its sole transport, with a cookie handshake on connection to authenticate the client on shared machines.
┌──────────────┐ WS ws://127.0.0.1:{port} ┌──────────────────┐
│ beamtalk │ ──────────────────────────── │ Workspace Node │
│ repl (CLI) │ 1. WebSocket connect │ (BEAM + cowboy) │
│ │ 2. Send cookie message │ │
│ │ 3. Server validates │ │
│ │ 4. JSON protocol begins │ │
└──────────────┘ └──────────────────┘
Single transport: WebSocket. The workspace uses cowboy (standard Erlang HTTP/WebSocket server) instead of gen_tcp. The REPL protocol is the same JSON messages, carried over WebSocket frames instead of newline-delimited TCP. This means:
- CLI connects via
ws://127.0.0.1:{port}(usingtungstenitecrate in Rust) — port discovered from~/.beamtalk/workspaces/{id}/portfile written by the workspace on startup - Browser connects via
ws://localhost:{port}directly — no proxy needed for local dev - Remote/TLS — standard reverse proxy (Caddy, nginx) in front, zero custom code
Why WebSocket only (not TCP + WebSocket): One transport means one implementation to test and maintain. WebSocket is HTTP-upgrade-compatible, so standard proxies (Caddy, nginx, envoy) can terminate TLS, handle auth headers, and forward without custom bridging code. This follows nREPL's principle of transport-agnostic protocol design — the JSON messages are the protocol; WebSocket is the transport.
Cookie handshake protocol:
→ {"type": "auth", "cookie": "<workspace cookie>"}
← {"type": "auth_ok"}
or on failure:
← {"type": "auth_error", "message": "Invalid cookie"}
→ [connection closed by server]
The workspace cookie already exists at ~/.beamtalk/workspaces/{id}/cookie with chmod 600. The CLI reads it from disk; the workspace validates it on first WebSocket message. This adds one message to connection setup.
Why this matters: On single-user machines, loopback binding is sufficient. On shared machines (CI runners, Docker --net=host, multi-user servers), any local process can connect to the port. The cookie handshake ensures only processes with read access to the cookie file can eval code.
Rationale for cookie (not mTLS): Any process that can read cert files can also read the cookie file. Cookie auth is simpler and provides equivalent security to mTLS for the localhost threat model. The OS filesystem ACL (chmod 600) is the actual authentication boundary.
Cookie lifecycle: The workspace cookie persists across workspace restarts, following the Erlang cookie model (~/.erlang.cookie). Cookie rotation is a separate concern — a future beamtalk workspace rotate-cookie command can regenerate it when needed, but automatic rotation on restart would break reconnecting clients unnecessarily.
Layer 2: Web terminal via standard reverse proxy
Since the workspace speaks WebSocket natively, the browser can connect directly for local dev. For remote access or TLS, use a standard reverse proxy (Caddy, nginx, envoy) — zero custom proxy code.
Local dev (no proxy needed):
┌────────────┐ ws://localhost:{port} ┌────────────────────┐
│ Browser │ ────────────────────────── │ Workspace Node │
│ (xterm.js)│ (WebSocket, direct) │ (cowboy) │
└────────────┘ └────────────────────┘
The browser loads a static xterm.js page (served by beamtalk web from http://localhost) and connects to ws://localhost:{port} (port discovered from the workspace port file). The cookie handshake authenticates the connection. The page must be served over HTTP (not loaded from file://) so that the Origin header is present for WebSocket validation.
Remote/TLS (standard proxy):
┌────────────┐ wss:// ┌───────┐ ws://127.0.0.1 ┌────────────┐
│ Browser │ ────────────────────── │ Caddy │ ────────────────── │ Workspace │
│ │ (TLS, encrypted) │ │ (loopback) │ (cowboy) │
└────────────┘ └───────┘ └────────────┘
# Caddyfile — entire proxy config
localhost:8443 {
reverse_proxy localhost:{port} # port from ~/.beamtalk/workspaces/{id}/port
}
# Or with Tailscale:
my-laptop.tail12345.ts.net:8443 {
reverse_proxy localhost:{port}
}
Authentication options:
| Method | When | How |
|---|---|---|
| Cookie handshake | Default (local) | Browser sends workspace cookie in first WebSocket message (same as CLI). User provides cookie via the xterm.js page UI. |
| Session token | Convenience | beamtalk web generates a one-time URL token, exchanged for HttpOnly; Secure; SameSite=Strict cookie. Prevents leakage via browser history. |
| Proxy-level auth | Remote/team | Caddy handles OAuth/OIDC, mTLS, or basic auth before forwarding to workspace. |
DNS rebinding mitigation: The cowboy WebSocket handler in the workspace must:
- Validate the
Originheader on WebSocket upgrade — reject cross-origin connections - Require the cookie handshake before accepting any eval commands
The workspace always binds to 127.0.0.1 by default. Network exposure is handled by the reverse proxy, not by changing the workspace bind address. For overlay networks (Tailscale), the workspace can optionally bind to the overlay IP:
# NOTE: The following commands illustrate proposed CLI UX for Phases 1-2.
# Some subcommands/flags (e.g. `beamtalk web`, `--bind tailscale`) are not
# available in the current CLI yet.
# Default: local only (browser connects directly, no proxy needed)
beamtalk repl
# → Workspace listens on ws://127.0.0.1:{OS-assigned port}
# Serve xterm.js web terminal (proposed UX — Phase 1, not yet implemented)
beamtalk web
# → Opens browser to http://localhost page that connects to ws://localhost:{port}
# Remote via Caddy (workspace stays on loopback)
caddy reverse-proxy --from :8443 --to localhost:{port}
# Remote via Tailscale (workspace binds to overlay IP) — proposed UX, Phase 2
beamtalk run server.bt --bind tailscale
# → ws://100.64.x.x:{port}, only reachable by Tailscale peers
# Safety check for non-loopback binding — proposed UX, Phase 2
beamtalk run server.bt --bind 0.0.0.0
# ERROR: Binding to all interfaces exposes the workspace to the network.
# Use --confirm-network to proceed, or use --bind tailscale for secure remote access.
Layer 3: Remote attach via mTLS or network overlay
For beamtalk workspace attach prod@host, two approaches are supported. The user chooses based on their infrastructure.
Option A: Network overlay (Tailscale, WireGuard, VPN)
The simplest path. The overlay provides encryption and identity. Beamtalk treats the overlay network as "local":
┌──────────────┐ WireGuard tunnel ┌──────────────────┐
│ Developer │ ────────────────────────── │ Production │
│ 100.64.0.1 │ (encrypted) │ 100.64.0.2 │
└──────────────┘ └──────────────────┘
│ │
beamtalk workspace attach prod@100.64.0.2 beamtalk repl
(connects to 100.64.0.2:{port}) (listens on 100.64.0.2:{port})
Changes needed:
- Workspace can bind to a Tailscale/WireGuard IP instead of
127.0.0.1 - No application-level protocol changes — same WebSocket over TCP, same JSON messages, same semantics
- Authentication relies on the overlay's identity (Tailscale ACLs, WireGuard keys)
# Start workspace listening on Tailscale interface
beamtalk run server.bt --bind tailscale
# → Binds to Tailscale IP (100.64.x.x), only reachable by Tailscale peers
# Connect from another machine on same tailnet
beamtalk workspace attach my-server.tail12345.ts.net
Tailscale-specific integration:
- Auto-detect Tailscale IP via
tailscale status --json - Use Tailscale's MagicDNS for discovery (
beamtalk workspace attach my-server) - Leverage Tailscale ACLs for authorization (who can attach to what)
--bind tailscaleas shorthand for "bind to my Tailscale IP"
Option B: mTLS on Erlang distribution
For environments without a network overlay, enable OTP's built-in TLS for distributed Erlang:
┌──────────────┐ TLS (mTLS) ┌──────────────────┐
│ Developer │ ────────────────────────── │ Production │
│ BEAM node │ (client+server certs) │ BEAM node │
└──────────────┘ └──────────────────┘
Erlang's ssl_dist module provides mTLS for the Erlang distribution protocol with minimal configuration:
%% vm.args (or generated by beamtalk CLI)
-proto_dist inet_tls
-ssl_dist_optfile ~/.beamtalk/tls/ssl_dist.conf
%% ssl_dist.conf
[{server, [
{certfile, "server.pem"},
{keyfile, "server-key.pem"},
{cacertfile, "ca.pem"},
{verify, verify_peer},
{fail_if_no_peer_cert, true}
]},
{client, [
{certfile, "client.pem"},
{keyfile, "client-key.pem"},
{cacertfile, "ca.pem"},
{verify, verify_peer}
]}].
Certificate management — auto-generated per workspace:
~/.beamtalk/tls/
├── ca.pem # Self-signed CA (generated once)
├── ca-key.pem # CA private key
├── workspaces/
│ └── {id}/
│ ├── server.pem # Workspace server cert (signed by CA)
│ ├── server-key.pem # Workspace server key
│ ├── client.pem # Client cert (for attaching)
│ └── client-key.pem # Client key
# Auto-generate CA on first use
beamtalk tls init
# → Creates ~/.beamtalk/tls/ca.pem (self-signed, long-lived)
# Auto-generate workspace certs
beamtalk workspace create my-feature --tls
# → Creates per-workspace server + client certs signed by CA
# Attach with mTLS
beamtalk workspace attach prod@host --tls
# → Uses client cert from ~/.beamtalk/tls/workspaces/{id}/
Option C: SPIFFE/SPIRE (future — cloud-native deployments)
For Kubernetes and service-mesh environments, SPIFFE provides automatic workload identity:
┌──────────────┐ ┌──────────────────┐
│ SPIRE Agent │ ─── issues x509-SVID ──── │ Workspace Node │
│ (per node) │ (auto-rotated) │ SPIFFE ID: │
└──────────────┘ │ spiffe://bt/ │
│ workspace/prod │
└──────────────────┘
This is deferred to a future ADR when Kubernetes deployment becomes a priority. The mTLS infrastructure from Option B provides a natural integration point — SPIRE-issued certs can replace the self-signed CA certs with zero protocol changes.
Summary: What secures what
| Scenario | Transport Security | Identity/Auth | Config |
|---|---|---|---|
beamtalk repl (local) | Loopback binding | Cookie handshake | Default, zero config |
| Browser (local) | Direct WebSocket | Cookie handshake | beamtalk web serves xterm.js |
| Browser (remote) | Caddy/nginx TLS | Cookie + proxy auth (OAuth, mTLS) | Standard reverse proxy |
beamtalk workspace attach via Tailscale | WireGuard | Tailscale identity + cookie | --bind tailscale |
beamtalk workspace attach via mTLS | TLS (ssl_dist) | Client certificate | --tls flag |
| Future: K8s/SPIFFE | mTLS (SVID) | SPIFFE workload identity | SPIRE agent |
Prior Art
Jupyter Notebook
- Local server generates a random token, printed to terminal
- User accesses
http://localhost:8888/?token=abc123 - No TLS for localhost by default; relies on loopback binding
- JupyterHub adds OAuth/OIDC for multi-user
Erlang/OTP ssl_dist
- Built-in mTLS for distributed Erlang since OTP 18
- Battle-tested in production Erlang systems (RabbitMQ, CouchDB)
-proto_dist inet_tlsflag + SSL config file- Replaces the cookie as primary auth (cookie becomes secondary)
Tailscale / WireGuard
- Network-level encryption transparent to applications
- Identity tied to device/user, not certificates the app manages
- Tailscale ACLs provide authorization rules
- Used by Grafana, Gitpod, VS Code tunnels for remote dev
SPIFFE/SPIRE
- CNCF project for workload identity
- x509-SVIDs are short-lived mTLS certs (auto-rotated ~1hr)
- Used by Istio, Linkerd, SPIRE for service mesh identity
- No existing Erlang integration (would need custom Workload API client)
LiveBook (Elixir)
- Similar architecture: Phoenix app connecting to BEAM node
- Uses Erlang distribution for node-to-node communication
- Authentication via token in URL (like Jupyter)
- Supports clustering via
dns_clusteror manual node connection
nREPL (Clojure)
- Network REPL with pluggable transports (TCP bencode, EDN, WebSocket via community)
- Protocol is transport-agnostic — messages are the contract, transport is plumbing
- No built-in TLS/auth — relies on localhost binding
- Influenced our decision to use a single transport (WebSocket) with the protocol independent of framing
User Impact
Newcomer
- No friction.
beamtalk replworks exactly as before — cookie handshake is automatic (CLI reads cookie from disk transparently). - Web terminal provides a familiar browser-based experience.
beamtalk webprints a clickable URL. - Potential confusion: "Why do I need a token for the web terminal but not the CLI?" — document that the CLI reads the cookie file directly.
Smalltalk developer
- Familiar model. Smalltalk images are single-user by design. The local-first, zero-config approach matches their expectations.
- Web terminal fills the gap left by Smalltalk's native IDE — browser-based code editing and inspection.
Erlang/BEAM developer
- Recognizable patterns. Cookie auth mirrors Erlang's
--setcookiepattern.ssl_distis standard OTP. beamtalk workspace attachparallelserl -remshwith better security defaults.- May expect: Distributed Erlang features (connecting nodes,
:rpc) — need docs on how security applies to node-to-node communication.
Operator (production)
- Clear upgrade path. Start with Tailscale (zero code change), graduate to mTLS for stricter environments.
beamtalk workspace attachrequires explicit--bind tailscaleor--tls— no accidental exposure.- Audit logging (see Consequences) enables compliance and incident response.
- Team environments: OAuth/OIDC on web terminal for shared workspaces with SSO integration.
Steelman Analysis
"Just use mTLS everywhere, including localhost"
- Best argument (security engineer): Defense in depth. On shared machines, a compromised local process can connect to the REPL port and execute arbitrary code. mTLS ensures only cert-holding processes can connect.
- Counter: The cookie handshake provides equivalent authentication — any process that can read the cert can also read the cookie. The actual security boundary is filesystem permissions (
chmod 600), not the transport layer. Jupyter, LiveBook, and VS Code all use unencrypted localhost with similar reasoning. - Tension point: Security engineers prefer defense in depth; developers prefer zero-config simplicity. We side with simplicity for local dev, with the cookie handshake as a middle ground.
"Use Unix sockets for local REPL, TCP only for remote"
- Best argument (BEAM developer): Filesystem permissions on Unix sockets provide stronger isolation than TCP + cookie. This eliminates port conflicts and shared-machine risks entirely.
- Counter: Erlang's
gen_tcpis native and well-supported; Unix socket support requiresafunixor NIFs and isn't portable. Maintaining two transports (UDS local, TCP remote) doubles the protocol surface. The cookie handshake on TCP achieves comparable security with a single transport. - Tension point: Simplicity of implementation vs. security purity. If Erlang had first-class Unix socket support, we'd likely use it.
"Tailscale is a vendor dependency"
- Best argument (operator): Relying on
tailscale status --jsoncouples the CLI to a proprietary tool. Open-source WireGuard exists without Tailscale. - Counter: We support any WireGuard/overlay network.
--bind tailscaleis a convenience shortcut, not a requirement.--bind <ip>works with any network. SPIFFE support is planned for vendor-neutral cloud identity.
"SPIFFE should be the primary approach"
- Best argument (platform engineer): It's the CNCF standard for workload identity. Building custom cert management now creates migration debt when SPIFFE is adopted later.
- Counter: SPIFFE requires infrastructure (SPIRE agents on every machine). It's the right answer for Kubernetes deployments but overkill for a developer running
beamtalk replon their laptop. The mTLS cert infrastructure we build now is SPIFFE-compatible — SPIRE-issued SVIDs can replace self-signed certs with no protocol changes.
Alternatives Considered
Do nothing (status quo)
- Keep loopback binding with no authentication on the REPL protocol
- Defer web terminal and remote attach security until those features are built
- Rejected: The REPL is an arbitrary code execution endpoint. On shared machines, any local process can connect and eval code. Adding cookie auth now is low-cost and prevents a real attack vector. Deferring web terminal security is acceptable; deferring local auth is not.
Unix sockets for local REPL
- Switch REPL transport from TCP to Unix socket (
~/.beamtalk/workspaces/{id}/repl.sock) - Filesystem permissions provide auth without any protocol changes
- Rejected: Erlang lacks first-class Unix socket support (requires
afunixdriver or NIF). Maintaining two transports (UDS local, TCP remote) doubles protocol surface. Cookie auth on TCP provides comparable security. See "Why TCP" in Context section.
SSH tunneling for remote attach
beamtalk workspace attachcould SSH to the remote host and forward the REPL port- Rejected: Adds SSH as a dependency, doesn't help with web terminal, doesn't integrate with Erlang distribution. Fine as a user-level workaround but not a platform feature.
Token-only auth (no TLS)
- Simple bearer token in first JSON message
- Rejected for remote: Token transmitted in cleartext over the network. Acceptable for localhost (where the OS prevents sniffing), but insufficient for any network-exposed scenario. For local connections, the cookie handshake is effectively this approach.
Keep TCP + add WebSocket (dual transport)
- Maintain existing
gen_tcpfor CLI backward compatibility, addcowboyWebSocket for browsers - Rejected: Two transports doubles the protocol surface, testing matrix, and bug surface. The WebSocket protocol carries the same JSON messages. The CLI migration from
TcpStreamtotungsteniteis straightforward. One transport is simpler.
Custom encryption protocol
- Roll our own encryption for the JSON protocol
- Rejected: "Don't roll your own crypto." OTP's
sslmodule, Tailscale, and SPIFFE are battle-tested. We gain nothing by implementing our own.
Consequences
Positive
- Local dev remains zero-config and fast (cookie handshake is one WebSocket message)
- Web terminal works with direct WebSocket — no custom proxy needed for local dev
- Remote/TLS handled by standard proxies (Caddy, nginx) — zero custom proxy code
- Clear, graduated security model from local → remote → cloud
- Composable with existing infrastructure (Tailscale, VPN, SPIFFE, any reverse proxy)
- mTLS via
ssl_distis an OTP built-in — minimal implementation effort - Cookie handshake closes the shared-machine attack vector on the REPL
- Single transport (WebSocket) simplifies testing and maintenance
Negative
- WebSocket migration is a breaking change to the REPL protocol (Phase 0)
cowboydependency added to workspace OTP apptungstenitedependency added to CLI Rust crate- Certificate management (even auto-generated) adds user-facing complexity for Phase 3
- Multiple security approaches (cookie, proxy auth, mTLS) mean more documentation
Neutral
- Tailscale integration is convenience, not dependency — works without it
- SPIFFE deferred to future ADR — no current implementation cost
- Audit logging (connection events, auth failures) is recommended for production deployments but not mandated for local dev
- Cert rotation/revocation deferred to follow-up issue — acceptable for v1
Implementation
Phase 0: WebSocket transport + cookie handshake
- Replace
gen_tcpinbeamtalk_repl_server.erlwithcowboyWebSocket handler - Update
crates/beamtalk-cli/src/commands/protocol.rsfromTcpStreamtotungsteniteWebSocket client - Add cookie handshake as first WebSocket message (validate before accepting eval)
- Update
docs/repl-protocol.mdwith WebSocket transport and auth handshake spec - Components:
beamtalk_repl_server.erl(cowboy),protocol.rs(tungstenite),repl-protocol.md - Dependencies:
cowboy(Erlang, already common in BEAM ecosystem),tungstenite(Rust crate) - Tests: Runtime unit tests for auth accept/reject, E2E test for REPL connection with cookie
Phase 1: Network binding + standard proxy documentation
--bind tailscale/--bind <ip>for workspace- Tailscale auto-detection (
tailscale status --json) - Safety checks for non-loopback binding (
--confirm-network) - Document Caddy/nginx reverse proxy configuration for TLS and remote access
- Components: CLI argument handling,
beamtalk_repl_server.erlbind config, docs - Tests: CLI argument validation, error on
--bind 0.0.0.0without--confirm-network
Phase 2: mTLS for Erlang distribution
beamtalk tls init— generate self-signed CA- Auto-generate per-workspace certs on
workspace create --tlsflag forbeamtalk workspace attachandbeamtalk run- Wire up OTP
ssl_distconfiguration - v1 scope: Auto-generated certs with no rotation or revocation. Cert lifecycle management is a follow-up issue.
- Components: CLI commands, cert generation,
vm.argstemplating - Tests: mTLS connection acceptance/rejection, cert generation and validation
Phase 3: SPIFFE/SPIRE integration (future)
- Workload API client for fetching SVIDs
- Feed SPIRE-issued certs to
ssl_distconfiguration - Kubernetes deployment manifests with SPIRE sidecar
- Components: New Erlang module, deployment docs
Migration Path
The WebSocket migration (Phase 0) is a breaking change to the REPL transport layer. Since beamtalk is pre-1.0, there is no backward compatibility obligation, but the transition should be handled cleanly:
Phase 0 migration (WebSocket transport + cookie handshake)
- CLI and workspace must be updated together. A new CLI using
tungstenitecannot connect to a workspace still usinggen_tcp, and vice versa. Both sides ship in the same release. - No version negotiation. The old TCP protocol has no version handshake, so there is no way to gracefully negotiate transport. If a user runs a new CLI against an old workspace (or vice versa), the connection will fail immediately with a clear error message (e.g., "Connection refused — workspace may be running an older version. Run
beamtalk workspace restartto upgrade."). - Running workspaces must be restarted. Existing workspace processes speak the old TCP protocol and cannot be hot-upgraded to WebSocket. Users must stop and restart workspaces after updating:
beamtalk workspace stop && beamtalk repl. - Cookie file creation is automatic. If a workspace has no cookie file (
~/.beamtalk/workspaces/{id}/cookie), one is generated on first startup. Existing workspaces get a cookie on next restart.
User-visible steps
# After updating beamtalk CLI:
beamtalk workspace stop # Stop old workspace
beamtalk repl # Starts new workspace with WebSocket + cookie
Later phases (no migration needed)
Phases 1–3 (network binding, mTLS, SPIFFE) are additive features. They do not change the core protocol and require no migration from users already on WebSocket transport. Browser UI is handled by ADR 0017.
Scope Boundaries
This ADR does not cover:
- Web IDE security (Phoenix LiveView) —
docs/beamtalk-ide.mdshould have its own security section referencing this ADR's proxy model - Distributed actor security (workspace-to-workspace messaging) — deferred until multi-workspace communication is designed
- Audit logging implementation — recommended but not specified; a separate issue for production readiness
- Code deployment authorization (who can hot-reload code) — separate from REPL eval authorization
Implementation Tracking
Epic: BT-685 Status: Phases 0–2 Done
| Phase | Issue | Title | Size | Status |
|---|---|---|---|---|
| 0 | BT-683 | Migrate REPL transport from TCP to WebSocket with cookie auth | L | Done |
| 1 | BT-691 | Network bind options and reverse proxy documentation | M | Done |
| 2 | BT-692 | mTLS for Erlang distribution | L | Done |
| 3 | BT-693 | SPIFFE/SPIRE workload identity integration | XL | Future |
References
- Related ADRs: ADR 0004 — Persistent Workspace Management, ADR 0009 — OTP Application Structure
- Related docs: REPL Protocol, Beamtalk IDE Design
- Erlang
ssl_dist: https://www.erlang.org/doc/apps/ssl/ssl_distribution.html - SPIFFE specification: https://spiffe.io/docs/latest/spiffe-about/overview/
- Tailscale: https://tailscale.com/kb/
- LiveBook security: https://github.com/livebook-dev/livebook#security
- Jupyter token auth: https://jupyter-notebook.readthedocs.io/en/stable/security.html
- DNS rebinding attacks: https://en.wikipedia.org/wiki/DNS_rebinding