Networking & Security
Cacophony is secure by default. Remote node communication uses mutual TLS and never depends on implicit network trust.
Listener Model
| Aspect | Local Listener | Cluster Listener |
|---|---|---|
| Protocol | HTTP (plain) | HTTPS |
| Bind Address | Loopback only | Configured host interface |
| Authentication | Bearer token | Mutual TLS |
| Consumers | CLI, TUI, local agents, MCP | Peer 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
| File | Path |
|---|---|
| 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
- Server certificate subject or SAN must match the configured node host
- Client certificate subject identifies the connecting node name
- Daemons trust only the configured cluster CA
- Sidecars use the same PKI model
Common API Headers
Required Headers
| Header | Purpose |
|---|---|
Authorization | Bearer token for local auth |
X-Caco-Request-Id | Unique request identifier |
Recommended Headers
| Header | Purpose |
|---|---|
X-Caco-Caller | Canonical 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:
| Endpoint | Purpose |
|---|---|
/api/v1/feed/stream | Raw feed event stream |
/api/v1/ui/stream | UI aggregate stream (consumed by TUI/choices) |
/api/v1/logs/stream | Log stream |
Cluster Routing Modes
Every node has a resolved routing mode that determines how peers discover and connect to its cluster endpoint:
| Mode | Description | Config |
|---|---|---|
private | The 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. |
public | The 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. |
relayed | No 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