Networking & Security

Cacophony is secure by default. Remote node communication uses mutual TLS and never depends on implicit network trust.

Listener Model

Every daemon has two listeners. The local listener speaks plain HTTP on loopback (127.0.0.1 by default) and accepts the node bearer token from $CACOPHONY_DIR/tokens/node.token; the CLI, TUI, caco-web, local agents and MCP servers use it. The cluster listener speaks HTTPS with mutual TLS on the node's configured host and cluster port and only trusts certificates signed by the cluster CA. Static peers reach it on the private host and cluster port, public or dynamic callers on public_host and public_cluster_port, and relay-mode dynamic nodes through a relay-eligible peer. Before a node has a certificate, it talks only to the bootstrap authority's HTTPS join endpoint with a bootstrap token.
Two listenersLocal callers use loopback HTTP with the node token; peers use HTTPS with mutual TLS on a private, public or relayed route.
AspectLocal ListenerCluster Listener
ProtocolHTTP (plain)HTTPS
Bind AddressLoopback onlyConfigured host interface
AuthenticationBearer tokenMutual TLS
ConsumersCLI, TUI, local agents, MCPPeer daemons

Node-scoped overrides tune bind scope: nodes[].services.caco-daemon.local_bind_host sets the local API's bind address (default 127.0.0.1) and bind_host the cluster listener's, which keeps mTLS either way. The literal tailnet alias is currently refused by config validation for both; use a concrete address. Ingress-mediated dynamic nodes advertise an external peer port via public_cluster_port separate from the local cluster_port, and github_ssh_routing (default_port / ssh_over_443) selects GitHub SSH routing per project or node. Daemonless client_nodes are certificate identities only, not daemon peers.

Local Authorization

Every local API request must include the node bearer token. Prefer first-party caco commands for routine inspection because they load that token from disk without exposing it in shell history or process arguments:

caco status --json true
caco node show --json true

Raw HTTP is intended for trusted local debugging only. If you use curl, keep the bearer value out of shared logs and remember that shell-expanded headers can be visible to local process inspection while the request runs.

The token is read from $CACOPHONY_DIR/tokens/node.token. The daemon generates this file on first bootstrap if it does not exist, using an atomic create-new write so concurrently starting services converge on one shared token instead of racing different in-memory values. If another process has just created the file but not finished writing it, readers briefly retry empty reads before treating the token as invalid. The file must have owner-read or owner-read-write permissions only.

All local clients — CLI, TUI, helper processes, and agents — use this same token.

Cluster Mutual TLS

PKI Files

FilePath
Cluster CA$CACOPHONY_DIR/pki/ca.pem
Node certificate$CACOPHONY_DIR/pki/nodes/<node>.crt
Node private key$CACOPHONY_DIR/pki/nodes/<node>.key

Certificate Requirements

Common API Headers

Required Headers

HeaderPurpose
AuthorizationBearer token for local auth
X-Caco-Request-IdUnique request identifier

Recommended Headers

HeaderPurpose
X-Caco-CallerCanonical caller identity

If X-Caco-Caller is omitted on a local request, the daemon infers it. The daemon returns an error if caller identity is required and inference fails.

Caller Identity

Callers are identified using the format <node>:<project>:<actor>:

localhost:cacophony:sunny-owl     # Agent
worker-1:cacophony:operator          # Human user
beadshost:cacophony:beads-host     # System service

Response Envelopes

Success Response

{
  "ok": true,
  "data": { ... },
  "meta": {
    "request_id": "req-...",
    "node": "localhost"
  }
}

Error Response

{
  "ok": false,
  "error": {
    "code": "not_found",
    "message": "Agent not found",
    "details": {}
  },
  "meta": {
    "request_id": "req-..."
  }
}

Streaming (SSE)

Real-time subscriptions use Server-Sent Events with JSON payloads:

EndpointPurpose
/api/v1/feed/streamRaw feed event stream
/api/v1/ui/streamUI aggregate stream (consumed by TUI/choices)
/api/v1/logs/streamLog stream

Cluster Routing Modes

Every node has a resolved routing mode that determines how peers discover and connect to its cluster endpoint:

ModeDescriptionConfig
privateThe selected route uses host:cluster_port. Static private-mesh peers prefer this route even if the target also advertises a public endpoint.Default selected route for static peers in the configured private mesh.
publicThe selected route uses an externally routable endpoint that differs from the private address.Static nodes may set public_host and/or public_cluster_port for public/dynamic callers. Dynamic direct_mesh nodes supply advertised_host at join time.
relayedNo direct endpoint. Traffic forwarded through a relay peer.Dynamic nodes with transport: relay.

The selected routing mode is surfaced in GET /api/v1/nodes, GET /api/v1/nodes/{node}, and GET /api/v1/config/peers. When a distinct public endpoint is configured, responses may also include public_endpoint alongside the selected cluster_addr so operators can spot stale peers still using the public route unnecessarily.

Bootstrap vs Steady-State Routing

pki.bootstrap is optional. When configured, pki.bootstrap.public_url is used only for pre-mTLS join requests (/v1/bootstrap/join) and must not be used as the ordinary peer address. Steady-state static peer routing uses the private host:cluster_port; public/dynamic callers may use public_host:public_cluster_port when configured. Independent or self-contained clusters can run their own CA/bootstrap surface and should not inherit an unrelated root-cluster bootstrap endpoint.

Relay-Eligible Nodes

Static direct_mesh nodes can declare relay_eligible: true to serve as relay peers for relay-mode dynamic nodes. This is surfaced in node inspection surfaces.

nodes:
  - name: edge-gateway
    host: edge.example.com
    public_host: edge.example.com
    relay_eligible: true