feat(mesh): NATS TLS + token auth client support

- CastleNATSClient accepts a token; a tls:// server URL makes nats-py verify the
  broker against the system CA (trusts the wildcard's Let's Encrypt issuer, no
  custom CA needed)
- config: CASTLE_API_NATS_TOKEN; main wires it through

Server side (instance): nats-server.conf gains tls{} (wildcard cert via
expose.tcp.tls) + authorization{token}; clients use tls://castle-nats.<domain>.
This commit is contained in:
2026-07-07 05:52:57 -07:00
parent 781a3bed14
commit e82de4fd50
3 changed files with 8 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ class Settings(BaseSettings):
# Mesh coordination (all off by default — single-node works without them) # Mesh coordination (all off by default — single-node works without them)
nats_enabled: bool = False nats_enabled: bool = False
nats_url: str = "nats://localhost:4222" nats_url: str = "nats://localhost:4222"
nats_token: str | None = None
mdns_enabled: bool = False mdns_enabled: bool = False
model_config = { model_config = {

View File

@@ -68,6 +68,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
local_hostname=registry.node.hostname, local_hostname=registry.node.hostname,
local_registry=registry, local_registry=registry,
servers=settings.nats_url, servers=settings.nats_url,
token=settings.nats_token,
) )
await nats_client.start() await nats_client.start()
app.state.nats_client = nats_client app.state.nats_client = nats_client

View File

@@ -50,10 +50,12 @@ class CastleNATSClient:
local_hostname: str, local_hostname: str,
local_registry: NodeRegistry, local_registry: NodeRegistry,
servers: str | list[str] = "nats://localhost:4222", servers: str | list[str] = "nats://localhost:4222",
token: str | None = None,
) -> None: ) -> None:
self._local_hostname = local_hostname self._local_hostname = local_hostname
self._local_registry = local_registry self._local_registry = local_registry
self._servers = servers self._servers = servers
self._token = token or None
self._nc: nats.NATS | None = None self._nc: nats.NATS | None = None
self._kv = None self._kv = None
self._presence_kv = None self._presence_kv = None
@@ -77,9 +79,13 @@ class CastleNATSClient:
async def start(self) -> None: async def start(self) -> None:
"""Connect, publish our registry, seed state, and start watchers.""" """Connect, publish our registry, seed state, and start watchers."""
# A `tls://` server URL makes nats-py verify the server against the system
# CA bundle — which trusts the wildcard's Let's Encrypt issuer, so no custom
# CA is needed. `token` authenticates this node to the broker.
self._nc = await nats.connect( self._nc = await nats.connect(
self._servers, self._servers,
name=f"castle-{self._local_hostname}", name=f"castle-{self._local_hostname}",
token=self._token,
max_reconnect_attempts=-1, # reconnect forever — nodes come and go max_reconnect_attempts=-1, # reconnect forever — nodes come and go
) )
js = self._nc.jetstream() js = self._nc.jetstream()