Commit Graph

25 Commits

Author SHA1 Message Date
Paul Payne
1fca2b8032 fix: Pass Subdomains field through to HAProxy InstanceRoute
The reconciliation was building InstanceRoutes without setting the
Subdomains field, so all services were exact-match only — breaking
wildcard subdomain routing for *.cloud.payne.io and *.cloud2.payne.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:45:25 +00:00
Paul Payne
9de2311357 refactor: Remove RegisterSelf — Central domain from config
Central no longer self-registers as a service. Its own domain comes
from config (cloud.central.domain) and is injected directly into
reconciliation as an HTTPRoute. This is cleaner — Central's own
services are config-driven, not registration-driven.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:30:34 +00:00
Paul Payne
548be903e7 feat: Fix HAProxy ACL ordering + Subdomains field
Rewrite HAProxy config generation with correct ACL ordering:
1. L7 HTTP exact matches first (central, wild-cloud app)
2. L4 exact matches (payne.io, civilsociety.dev)
3. L4 wildcard matches (*.cloud.payne.io) — only when subdomains:true
4. Default → L7 termination

Key changes:
- InstanceRoute: removed ExtraDomains, added Subdomains bool
- GenerateOpts: removed CentralDomain (Central is now just another
  HTTPRoute injected by reconciliation), renamed WildcardCert→CertsDir
- Central's domain comes from config, injected as HTTPRoute with
  the actual running port (no more hardcoded 5055)
- Added API.SetPort() so reconciliation knows the running port
- subdomains:false → exact match only (no *.domain shadowing)

New tests: TestGenerate_WithSubdomains, TestGenerate_ACLOrdering
(verifies L7 exact matches come before L4 wildcards).

Fixes: BUG 1 (central routing to wrong port), BUG 2 (wild-cloud
getting traefik cert), BUG 3 (*.payne.io too broad).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:29:30 +00:00
Paul Payne
8874bc6281 feat: Domain-keyed service registration model
Rewrite the service registration model per the architecture plan:

Model changes:
- Domain is the unique key (removed Name field)
- Removed ExtraDomains — each domain is its own registration
- Removed SourceNode, BackendStatic, ReachOff
- Added Subdomains bool for wildcard matching control
- Files stored as <domain_with_underscores>.yaml
- API routes: /services/{domain:.+} (regex for dots in path)
- Added DeregisterBySource for cleanup before re-registration

Reconciliation changes:
- No ExtraDomains iteration — each service IS one domain
- reach:internal → sets InternalDomain (generates local=/)
- reach:public → sets Domain (no local=/)
- tcp-passthrough → DNS points to backend IP
- http → DNS points to Central IP

All 22 tests pass (9 manager + 8 handler + 5 config/cert).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:23:11 +00:00
Paul Payne
170500e173 fix: Use configured dnsmasq IP instead of auto-detect
The auto-detect picks the default route interface (wlan0 at
192.168.8.152) but dnsmasq is bound to eth0 (192.168.8.151).
Now uses cloud.dnsmasq.ip from config as the Central IP for DNS
entries, falling back to auto-detect only if not configured.

Fixes DNS resolution failures when dnsmasq config points services
to the wrong interface IP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 19:24:13 +00:00
Paul Payne
900d25bab2 fix: Reload dnsmasq instead of restart to avoid DNS downtime
Use systemctl reload-or-restart instead of restart. dnsmasq supports
SIGHUP to reload config without dropping DNS service, preventing
transient DNS resolution failures during reconciliation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 15:54:34 +00:00
Paul Payne
ed0691d56f feat: Split DNS with correct IPs per service type
Fix dnsmasq reconciliation to use the correct DNS target IP:
- tcp-passthrough (k8s instances): DNS → k8s LB IP (LAN direct)
- http/static (Central apps): DNS → Central IP (HAProxy terminates)

Also handle ExtraDomains properly:
- Internal domains (internal.cloud.payne.io) get local=/ entries
  to prevent upstream DNS forwarding
- Extra domains get their own dnsmasq address= entries
- HAProxy already propagated ExtraDomains for SNI ACLs

Verified: cloud.payne.io → 192.168.8.240 (direct to k8s),
cloud2.payne.io → 192.168.8.80, central.payne.io → 192.168.8.151.
SNI routing through HAProxy works for both instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 15:46:35 +00:00
Paul Payne
7e8f660d11 feat: Per-service certs with opt-in wildcard provisioning
Remove automatic wildcard cert assumption. Each service gets its own
cert by default. Wildcards are user-initiated via the Certificates page.

Backend:
- HAProxy L7 frontend uses cert directory (/etc/haproxy/certs/)
  instead of single wildcard file — loads all PEMs, serves by SNI
- Cert status API shows every registered service individually with
  coveredBy field when a wildcard cert covers the domain
- Reconciliation filters L7 routes to services that have a cert
- No auto-provisioning in reconciliation (just warnings)

Frontend:
- Certificates page shows per-service cert status
- "Provision" button for individual certs
- "Add Wildcard" form for opt-in wildcard provisioning
- Fixed CloudflareComponent type errors from cert API changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:47:00 +00:00
Paul Payne
447c7e51a3 feat: Add Certificates page + stop auto-provisioning certs
New dedicated Certificates page in Wild Central UI showing:
- All tracked certs (central, wildcard, per-service)
- Status (valid with days remaining, or missing)
- Per-domain Provision button
- Renew All button

Cert provisioning is now user-initiated only — reconciliation logs
warnings about missing certs but does NOT auto-provision. This
prevents unexpected wildcard cert requests on startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:26:48 +00:00
Paul Payne
9eea061e9b feat: Enhanced cert status API + TLS certificates UI
API changes:
- /api/v1/cert/status now returns all relevant certs (central,
  wildcard, per-service) with existence status and expiry
- /api/v1/cert/provision accepts ?domain= param for per-domain
  provisioning
- Removed stale syncHAProxy references, uses reconcileNetworking

UI changes:
- Added TLS Certificates card to Cloudflare page showing cert status
  for each domain with provision/renew actions
- Updated useCert hook and cert API types for new response format

Next: extract into dedicated Certificates page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:23:08 +00:00
Paul Payne
7d5e4f4e29 fix: Skip L7 routes and central TLS in HAProxy when certs don't exist
HAProxy validation fails if the config references cert files that
don't exist (wildcard.pem for L7 routes, domain.pem for central TLS).

Now reconciliation checks for cert existence before including:
- L7 HTTP routes: only generated when wildcard cert exists
- Central domain TLS frontend: only generated when domain cert exists

This ensures HAProxy never enters a failure state due to missing
certs. Once certs are provisioned (via certbot), the next
reconciliation picks them up automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:15:32 +00:00
Paul Payne
53a640c34f fix: Auto-recover NATS JetStream from stale store on startup
If the NATS server fails to start (stale lock files, corrupt store
from a crash), automatically clean the data directory and retry once.
This prevents the frustrating "NATS server failed to start" error
after unclean shutdowns.

The NATS KV data (registered services) is also stored as YAML files
in the services directory, so cleaning the NATS store loses nothing
— services are re-registered on the next startup or API call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:12:29 +00:00
Paul Payne
e4e1a15f92 fix: Prevent broken dnsmasq entries for services without internal domain
Services registered via the service API have a domain but no internal
domain (that's a Wild Cloud instance concept). The dnsmasq generator
was producing broken entries like `local=//` and `address=//`.

Fix: skip internal domain entries when InternalDomain is empty, in
both config.go and config_modular.go. Also fix DNS entries to point
to Central's IP (where HAProxy listens) rather than the backend
address — all traffic flows through Central.

Added 3 tests covering: service-only, mixed services+instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:11:24 +00:00
Paul Payne
c7a0227e03 feat: Wire up TLS cert provisioning in reconciliation
When HTTP services are registered (where Central terminates TLS),
reconciliation now checks for and provisions TLS certificates:

- Derives gateway domain from central domain (e.g., payne.io)
- Checks for existing wildcard cert (*.payne.io)
- If no wildcard exists and Cloudflare token + operator email are
  configured, provisions one via certbot DNS-01
- For services outside the gateway domain, provisions individual certs
- Services with TLS passthrough (k8s) are skipped (backend handles TLS)
- Graceful: skips silently if no Cloudflare token or email configured

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 13:36:49 +00:00
Paul Payne
57a597fe5a feat: Self-register Wild Central on startup
On startup, Wild Central registers its own domain (from config
cloud.central.domain) as an HTTP service with itself. This creates
the DNS entry, proxy route, and TLS cert needed to access the
Central UI via its domain name.

Registration is idempotent — safe on every restart. Skips silently
if no domain is configured. Logs a warning if registration fails.

Verified: after startup, /api/v1/services shows wild-central
registered with domain central.payne.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 05:40:27 +00:00
Paul Payne
909527f060 feat: Add tests for config, secrets, services, and cloudflare endpoints
Fix config endpoint to return wrapped { configured, config } response
that the web UI expects. Fix services handler to return stored version
with defaults applied.

New tests:
- GetGlobalConfig: wrapped response, empty/not-configured case
- GetGlobalSecrets: leaf redaction preserves structure, raw mode
- UpdateGlobalConfig: write and verify
- CloudflareVerify: no-token case
- Services: register, list, get, update, deregister, TCP passthrough
  defaults, validation errors

15 new test cases, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 04:45:55 +00:00
Paul Payne
d9c152f044 fix: Preserve nested structure when redacting secrets
The secrets endpoint was flattening nested maps (e.g., cloudflare.apiToken)
to a single "********" string. The UI checks for nested keys to determine
if credentials are configured, so the structure must be preserved.

Now redactSecrets() recursively walks the map and only redacts leaf values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 04:40:47 +00:00
Paul Payne
f066dd7e99 feat: Add Cloudflare token verification endpoint
Add /api/v1/cloudflare/verify, /api/v1/cloudflare/zone endpoints
so the Central UI's Cloudflare page can verify tokens and manage
zone configuration. This was a missing endpoint from the extraction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 04:34:03 +00:00
Paul Payne
5defc27f63 feat: Wire up service registration to networking reconciliation
When a service is registered, updated, or deregistered, Central now
automatically regenerates HAProxy routes and dnsmasq DNS entries to
match the current set of registered services.

- L4 tcp-passthrough services (Wild Cloud) → HAProxy SNI routes
- L7 http services (Wild Works) → HAProxy Host-based reverse proxy
- All services with reach != off → dnsmasq DNS entries
- SSE events broadcast on config changes
- Reconciliation runs asynchronously (non-blocking)

This closes the loop: service registration is no longer inert — it
actually updates the networking stack.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 03:45:51 +00:00
Paul Payne
060a68b601 fix: Use production config file paths as defaults
The default paths for dnsmasq and nftables config files should match
the existing production layout (wild-cloud.conf, wild-cloud.nft) so
Wild Central works as a drop-in when reading from the production data
directory. These can be overridden via env vars for fresh installs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 03:39:51 +00:00
Paul Payne
a3b3339254 feat: Add Cloudflare Tunnel support (ported from Castle)
Port Castle's tunnel config generator to Go. Central can now manage
cloudflared ingress configuration for public service exposure.

Services with reach=public get projected at <subdomain>.<publicDomain>
with Host/SNI rewriting to the internal domain so the gateway's wildcard
cert validates and existing routing works unchanged.

- Locally-managed config.yml (not remotely-managed token tunnel)
- Public surface is exactly the set of reach=public services
- Auto-removes config when tunnel is disabled or no public services
- 7 tests covering basic generation, subdomain override, edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-08 23:41:00 +00:00
Paul Payne
c47689c3f2 feat: Add L7 HTTP reverse proxy mode to HAProxy
Extend HAProxy config generation to support L7 HTTP routes alongside
existing L4 SNI passthrough routes. This enables Wild Works services
to be reverse-proxied with TLS termination by Central while Wild Cloud
k8s instances continue using L4 SNI passthrough.

- Add HTTPRoute type: domain, backend address, optional health path
- Add GenerateWithOpts for full config options (HTTP routes, wildcard cert)
- L4 SNI frontend routes known Cloud domains to k8s LBs; unknown SNIs
  fall through to L7 TLS-termination frontend for Host-based routing
- L7 frontend terminates TLS with wildcard cert, routes by Host header
- Health check support via HAProxy httpchk
- Backward compatible: existing Generate() works unchanged
- 3 new tests (L7-only, mixed L4+L7, backward compat)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-08 23:39:30 +00:00
Paul Payne
6b58ebb692 feat: Add embedded NATS JetStream coordination bus
Central now starts an embedded NATS JetStream server as its core
coordination bus. This provides:

- wild-services KV bucket: service registrations from Cloud/Works
- wild-presence KV bucket: node liveness with TTL-based keys
- wild-events stream: events from all sources (24h retention)

NATS is started in main.go before the API and shut down on SIGTERM.
Cloud and Works will connect to Central's NATS (port 4222) to register
services and maintain presence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-08 23:36:40 +00:00
Paul Payne
e936102eab feat: Add service registration API
Add the service registration abstraction — the key contract between
Central and its consumers (Wild Cloud, Wild Works). Services register
with Central to get DNS, gateway routing, TLS, and public exposure.

- internal/services/manager.go: CRUD for service registrations stored
  as YAML files, with reach model (off/internal/public), backend types
  (tcp-passthrough/http/static), and TLS modes
- internal/api/v1/handlers_services.go: HTTP endpoints
  POST/GET/PATCH/DELETE /api/v1/services
- Full test coverage for registration, update, deregister, validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-08 23:33:30 +00:00
Paul Payne
beb643f76f feat: Extract Wild Central as standalone Go service
Extract the Central networking functionality from wild-cloud/api into
a standalone service. Wild Central manages DNS (dnsmasq), gateway
(HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot),
security (CrowdSec), and DDNS — all the network appliance concerns.

All Cloud-specific code (instances, clusters, nodes, apps, backups,
operations, kubectl/talosctl tooling) has been removed. The API struct
and route registration contain only Central endpoints. Tests updated
to match the new API signature.

Builds and all tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-08 23:31:16 +00:00