CVE-2026-55678: Arc: Unauthenticated cluster node admission when `cluster.shared_secret` is unset
Summary
Arc Enterprise clustering accepts cluster join requests without authentication when cluster.enabled=true but cluster.sharedsecret is not configured. The coordinator validates HMAC authentication only if a shared secret is non-empty; otherwise, a network attacker who can reach the coordinator port can send a join request with attacker-controlled node addresses and role. Accepted nodes are marked healthy, registered locally or added as Raft voters, and can be selected by the cluster router for forwarded authenticated requests.
Details
Cluster defaults include an empty shared secret and TLS disabled:
- internal/config/config.go:943-950 defaults cluster.enabled=false, cluster.clustername="arc-cluster", and cluster.coordinatoraddr=":9100". - internal/config/config.go:1001-1005 defaults cluster.sharedsecret="" and cluster.tlsenabled=false.
Startup requires a shared secret only for file replication, not for all clustering/join/routing use:
- cmd/arc/main.go:1258-1265 hard-fails without cluster.sharedsecret only when cluster.replicationenabled is true.
The join request contains attacker-supplied node identity, role, and addresses:
- internal/cluster/protocol/messages.go:127-142 defines JoinRequest fields including nodeid, role, raftaddr, apiaddr, coordaddr, plus optional auth fields.
The coordinator validates HMAC only when the configured shared secret is non-empty:
- internal/cluster/coordinator.go:1066-1081 wraps all HMAC checks in if c.cfg.SharedSecret != "" { ... }. - If the secret is empty, the join request proceeds after only the cluster-name check.
An accepted join creates a healthy node from attacker-controlled fields and adds it to cluster trust state:
- internal/cluster/coordinator.go:1101-1107 creates a node from request fields, sets attacker-provided coordinator/API addresses, and marks it healthy. - internal/cluster/coordinator.go:1108-1129 adds the attacker-provided raftaddr as a Raft voter and stores node info when Raft is configured. - internal/cluster/coordinator.go:1133-1138 registers the node locally when Raft is not configured.
The router uses healthy nodes from this registry and forwards authenticated requests to their advertised API addresses:
- internal/cluster/registry.go:263-270 returns healthy writers/readers. - internal/cluster/router.go:154-177 routes writes to healthy writer nodes. - internal/cluster/router.go:203-227 routes queries to healthy readers, or writers if no readers exist. - internal/cluster/router.go:327-357 builds the forwarding target from node.APIAddress and copies all original request headers to the peer, including Authorization and x-api-key. - cmd/arc/main.go:1887-1897 wires the cluster router into MessagePack, line protocol, TLE, and query handlers when the cluster coordinator exists.
A related lower-severity issue is that heartbeat messages are also unauthenticated:
- internal/cluster/protocol/messages.go:175-180 defines Heartbeat without HMAC fields. - internal/cluster/coordinator.go:1220-1232 records heartbeats and updates node state based only on supplied nodeid and state.
Proof of concept
Safe local lab reproduction only; do not target external infrastructure.
Prerequisites:
- Enterprise clustering enabled in a lab deployment. - cluster.sharedsecret intentionally left empty. - Network access to the coordinator TCP port, default 9100. - The attacker knows or guesses the cluster name; default is arc-cluster.
Steps:
1. Start an Arc cluster node with:
toml [cluster] enabled = true clustername = "arc-cluster" coordinatoraddr = ":9100" sharedsecret = "" tlsenabled = false
2. Start an attacker-controlled HTTP listener that records request method, path, and headers, for example on 127.0.0.1:18080.
3. Send a framed cluster join request to the victim coordinator. The protocol uses a 4-byte big-endian length prefix, followed by a 1-byte message type (MsgJoinRequest == 1), followed by JSON. The payload should include attacker-controlled node fields and omit authnonce, authtimestamp, and authhmac:
json { "nodeid": "evil-reader-1", "nodename": "evil-reader", "role": "reader", "clustername": "arc-cluster", "raftaddr": "127.0.0.1:19020", "apiaddr": "127.0.0.1:18080", "coordaddr": "127.0.0.1:19010", "version": "lab", "corecount": 1 }
Expected vulnerable result: the coordinator accepts the join instead of rejecting it for missing authentication.
4. Trigger a forwarded operation from a node that cannot handle the operation locally. Examples depend on cluster roles:
- Join as reader and trigger a query through a node that routes queries to readers. - Join as writer and trigger ingestion through a non-writer node that routes writes to writers.
Expected vulnerable result: the attacker-controlled HTTP listener receives forwarded requests. Because forwardRequest copies all original headers, the listener can observe authentication headers such as bearer tokens or API keys along with request paths and bodies.
5. In a Raft-enabled lab, observe that the attacker-provided raftaddr is submitted to AddVoter, demonstrating unauthorized membership mutation.
Impact
In affected cluster deployments, an unauthenticated network attacker can become a trusted cluster node. Practical impacts include:
- Interception of forwarded authenticated HTTP requests, including Authorization and x-api-key headers. - Exposure of query bodies, ingestion data, database/measurement names, and operational metadata. - Unauthorized cluster membership mutation, including attempted Raft voter addition when Raft is configured. - Potential data integrity impact if the rogue node returns forged query/write responses or accepts/diverts writes. - Potential availability impact by blackholing or delaying forwarded operations.
This is not reachable in the default standalone configuration because cluster.enabled=false, but it is a critical trust-boundary issue for Enterprise cluster deployments where clustering is enabled without a shared secret. The code already treats cluster.sharedsecret as mandatory for replication, which suggests unauthenticated cluster membership should also fail closed.
Suggested fix
- Fail startup when cluster.enabled=true and cluster.sharedsecret is empty, not only when cluster.replicationenabled=true. - Reject all trust-mutating coordinator messages when no cluster authentication is configured, including join, heartbeat/state update, forward apply, and file replication messages. - Require HMAC or mutual TLS before processing any join/heartbeat message. - Bind authentication to node identity and advertised addresses to reduce replay and address-substitution risks. - Do not forward end-user Authorization/x-api-key headers to a peer unless the peer identity has been authenticated and authorized. - Add tests proving unauthenticated join and heartbeat requests fail when clustering is enabled.
References / evidence
- internal/config/config.go:943-950 - internal/config/config.go:1001-1005 - cmd/arc/main.go:1258-1265 - internal/cluster/protocol/messages.go:127-142 - internal/cluster/coordinator.go:1066-1081 - internal/cluster/coordinator.go:1101-1138 - internal/cluster/router.go:154-177 - internal/cluster/router.go:203-227 - internal/cluster/router.go:327-357 - cmd/arc/main.go:1887-1897
Other sources
Arc is an open, SQL-native time-series database for telemetry. From 26.02.1 until 26.06.2, Arc Enterprise clustering accepts cluster join requests without authentication when cluster.enabled is true but cluster.sharedsecret is not configured. The defaults in internal/config/config.go set cluster.enabled to false, cluster.clustername to arc-cluster, cluster.coordinatoraddr to :9100, cluster.sharedsecret to an empty value, and cluster.tlsenabled to false, while cmd/arc/main.go requires cluster.sharedsecret only when cluster.replicationenabled is true. JoinRequest in internal/cluster/protocol/messages.go accepts attacker-controlled nodeid, role, raftaddr, apiaddr, and coordaddr values, plus optional authnonce, authtimestamp, and authhmac fields. The join path in internal/cluster/coordinator.go validates HMAC authentication only when the configured shared secret is non-empty and otherwise proceeds after only the cluster-name check. An accepted node is marked healthy, added as a Raft voter or registered locally, and becomes available through internal/cluster/registry.go to the routing logic in internal/cluster/router.go. The forwardRequest path in internal/cluster/router.go builds its target from node.APIAddress and copies Authorization and x-api-key headers with the request, so a rogue node selected for a forwarded query or write can receive authentication headers, request bodies, database and measurement names, and operational metadata. Heartbeat in internal/cluster/protocol/messages.go also lacks HMAC fields, and internal/cluster/coordinator.go updates node state from supplied nodeid and state values without authentication. An unauthenticated network attacker who can reach the coordinator port and knows the cluster name can therefore become a trusted cluster node, mutate cluster membership, be submitted as a Raft voter, intercept topology-dependent forwarded requests, divert or forge operations, and blackhole or delay traffic. The default standalone configuration is not reachable because cluster.enabled is false, but Enterprise cluster deployments with clustering enabled and no shared secret are affected. This issue is fixed in version 26.06.2.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/basekick-labs/arcto a version that resolves this vulnerability.Fixed in 0.0.0-20260615160325-38402ad2ebdd - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 26.06.2 - Configuration
Ensure clustering is enabled only together with a configured shared secret (cluster.shared_secret must not be empty). The described vulnerability affects Enterprise clustering when cluster.enabled=true and cluster.shared_secret is unset/empty.
Arc Enterprise clustering (cluster) cluster.enabled = true - Configuration
Set a non-empty shared secret. The coordinator wraps HMAC checks only when c.cfg.SharedSecret != ""; when it is empty, join requests proceed after only the cluster-name check. Fix also requires failing startup when cluster.enabled=true and cluster.shared_secret is empty (not only when cluster.replication_enabled=true).
Arc Enterprise clustering (cluster) cluster.shared_secret = <non-empty> - Configuration
Enable TLS for clustering to use mutual TLS for join/heartbeat authentication (as suggested: 'Require HMAC or mutual TLS before processing any join/heartbeat message'). Current default in internal/config/config.go is cluster.tls_enabled=false.
Arc Enterprise clustering (cluster) cluster.tls_enabled = true - Compensating control
Limit network access to the coordinator TCP port (default ':9100') so only trusted peers can reach it, reducing the chance an unauthenticated network attacker can send join/heartbeat messages.
Event History
Frequently Asked Questions
Which deployments are exposed?
Deployments are exposed when cluster.enabled is true and cluster.shared_secret is empty. The default configuration has clustering disabled, but enabling clustering without setting a shared secret leaves join requests unauthenticated.
What does an attacker need to exploit this?
An attacker needs network access to the coordinator port and the ability to send a cluster join request. No shared-secret authentication is required when the configured secret is empty.
What can a successfully joined rogue node do?
The attacker controls the joining node's identity, role, and addresses. Accepted nodes can be registered locally or added as Raft voters and may be selected by the cluster router to receive forwarded authenticated requests.
What can be done before applying the fix?
Configure a non-empty cluster.shared_secret on affected clustered deployments so that HMAC validation is performed for join requests. File replication already refuses to start without this setting, but other clustering and routing uses do not.
How can administrators identify an affected configuration?
Check whether cluster.enabled is set to true while cluster.shared_secret is unset or empty. The default coordinator address is :9100, so deployments using that default should also verify that the coordinator is not reachable by untrusted networks.