From e82de4fd50dd994314575b0c344bcfda36d73539 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Tue, 7 Jul 2026 05:52:57 -0700 Subject: [PATCH] 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.. --- castle-api/src/castle_api/config.py | 1 + castle-api/src/castle_api/main.py | 1 + castle-api/src/castle_api/nats_client.py | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/castle-api/src/castle_api/config.py b/castle-api/src/castle_api/config.py index e85a8b0..c1433d4 100644 --- a/castle-api/src/castle_api/config.py +++ b/castle-api/src/castle_api/config.py @@ -17,6 +17,7 @@ class Settings(BaseSettings): # Mesh coordination (all off by default — single-node works without them) nats_enabled: bool = False nats_url: str = "nats://localhost:4222" + nats_token: str | None = None mdns_enabled: bool = False model_config = { diff --git a/castle-api/src/castle_api/main.py b/castle-api/src/castle_api/main.py index f291f39..3df72b4 100644 --- a/castle-api/src/castle_api/main.py +++ b/castle-api/src/castle_api/main.py @@ -68,6 +68,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: local_hostname=registry.node.hostname, local_registry=registry, servers=settings.nats_url, + token=settings.nats_token, ) await nats_client.start() app.state.nats_client = nats_client diff --git a/castle-api/src/castle_api/nats_client.py b/castle-api/src/castle_api/nats_client.py index e3255b4..b0ccb9e 100644 --- a/castle-api/src/castle_api/nats_client.py +++ b/castle-api/src/castle_api/nats_client.py @@ -50,10 +50,12 @@ class CastleNATSClient: local_hostname: str, local_registry: NodeRegistry, servers: str | list[str] = "nats://localhost:4222", + token: str | None = None, ) -> None: self._local_hostname = local_hostname self._local_registry = local_registry self._servers = servers + self._token = token or None self._nc: nats.NATS | None = None self._kv = None self._presence_kv = None @@ -77,9 +79,13 @@ class CastleNATSClient: async def start(self) -> None: """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._servers, name=f"castle-{self._local_hostname}", + token=self._token, max_reconnect_attempts=-1, # reconnect forever — nodes come and go ) js = self._nc.jetstream()