Commit Graph

96 Commits

Author SHA1 Message Date
Paul Payne
282d255c0a Gitignore .envrc, add .envrc.example
.envrc contains local config (data dir paths) and should not be tracked.
Add .envrc.example documenting available env vars with defaults.
2026-07-14 15:12:57 +00:00
Paul Payne
519df610e8 Use standard ports for dev: API 5055, NATS 4222 (same as prod) 2026-07-14 15:01:32 +00:00
Paul Payne
93700d7956 Separate Central data dir from Cloud — use /var/lib/wild-central (prod default) 2026-07-14 14:33:40 +00:00
Paul Payne
6f85d25362 Add login screen for API token authentication
AuthGate component wraps the app layout:
- On load, checks if saved token is valid by calling a protected endpoint
- If auth not required (dev mode), passes through immediately
- If auth required and no valid token, shows a clean login screen
- Token input with password field, stored in localStorage on success
- Shows guidance: "Find your token in secrets.yaml under api.bearerToken"
- Handles connection errors gracefully (shows app anyway)
2026-07-14 13:21:34 +00:00
Paul Payne
f5a030fd44 Add bearer token API authentication
- Auto-generate random 32-char bearer token on first startup, stored in
  secrets.yaml as api.bearerToken
- BearerAuthMiddleware checks Authorization: Bearer <token> on all /api/
  endpoints except /health, /health/reconcile, /events (SSE), and non-API
  paths (frontend static files)
- Development mode (WILD_CENTRAL_ENV=development) skips auth entirely
- Web app ApiClient: add setToken/clearToken/hasToken methods, persist
  token in localStorage, automatically include Authorization header on
  all API requests
- Token can be found in secrets.yaml for CLI/automation use
2026-07-14 12:45:22 +00:00
Paul Payne
60f3ca4a3a Security hardening: input validation, secrets protection, NATS auth
Config injection prevention:
- Add FQDN validation for domain names (RFC 1123) in Register/Update —
  rejects newlines, spaces, shell metacharacters that could inject into
  HAProxy/dnsmasq configs
- Add backend address validation (valid host:port format, valid IP or
  hostname, port 1-65535). DNS-only backends allow bare IPs.
- Add header key/value validation — keys must be HTTP token chars only,
  values must not contain newlines or NULs
- Add WireGuard peer name validation (alphanumeric + hyphens + underscores)
- Add defense-in-depth domain validation in certbot Provision()

Secrets protection:
- Remove ?raw=true bypass on GET /api/v1/secrets — secrets are now always
  redacted in API responses regardless of query parameters
- Update test to verify redaction cannot be bypassed

NATS authentication:
- Generate random auth token on first startup, store in secrets.yaml
- Pass token to embedded NATS server via Authorization option
- Internal client connects with the same token
- External NATS clients (Wild Cloud) must now authenticate

Security headers:
- Add X-Content-Type-Options: nosniff
- Add X-Frame-Options: DENY
- Add Cache-Control: no-store
2026-07-14 12:38:17 +00:00
Paul Payne
4500a1a45e Add certbot status to dashboard services and sidebar nav indicator
- Add certbot to getDaemonStatus: checks binary availability and version
- Dashboard: add Certificates card to services grid
- Sidebar: show green/red status dot for certbot on Certificates nav item
2026-07-14 12:18:07 +00:00
Paul Payne
8dd9e117bc Fix cert auto-provisioning to check wildcard coverage before provisioning
The auto-provisioner was checking for individual cert files (e.g.,
git.civilsociety.dev.pem) without checking if a wildcard cert already
covers the domain (*.civilsociety.dev.pem). This caused redundant
individual certs to be provisioned for domains already covered by
a wildcard.

Now uses hasCertForDomain() which checks both individual AND wildcard
cert coverage before deciding a cert is missing.
2026-07-14 12:10:49 +00:00
Paul Payne
947991da7f Add TLS Certificates page to Advanced section of web UI
New page at /advanced/certificates showing:
- Overview card with central domain, cert count, Cloudflare token status
- Auto-provision readiness badge (requires CF token + operator email)
- Table of all domains needing TLS with: domain, source, expiry date,
  issuer, and status badges (days left / covered by wildcard / missing)
- Per-domain "Provision" button for missing certs when auto-provision ready
- "Provision" and "Renew All" action buttons
- Guidance alert when prerequisites are missing

Also fixes pre-existing type errors:
- AppSidebar: guard daemon index access when undefined
- DomainsComponent: fix circular type reference in filter state
- DomainTopology: remove unused isWildcard variable
2026-07-14 12:08:43 +00:00
Paul Payne
a85bfbfe9e Auto-provision TLS certificates during reconciliation
Replace the passive "log warning about missing certs" approach with active
auto-provisioning:

- When Cloudflare token and operator email are configured, automatically
  provision missing certs via certbot DNS-01 during reconciliation
- When credentials aren't available, log actionable guidance (no longer
  references a non-existent "Certificates page")
- Track TLS health in reconciler Health struct (ok/degraded with missing
  cert list)
- Broadcast tls:recovered SSE event when all certs become available
- Add CertManager interface and GetCloudflareToken callback to reconciler

The convergence loop (5 min) continuously retries failed provisions,
so transient DNS-01 failures self-heal on the next tick.
2026-07-14 11:59:22 +00:00
Paul Payne
2bb162a794 Add per-subsystem config validation for wireguard, authelia, nftables, tunnel
- wireguard: ValidateConfig checks ListenPort range, Address/LanCIDR CIDR
  format. SaveConfig now validates before writing.
- authelia: ValidateConfigOpts checks required fields (Domain, JWTSecret,
  SessionSecret) and StorageEncKey minimum length (20 chars). GenerateConfig
  now validates before generating.
- nftables: ValidateWANInterface checks interface exists via net.InterfaceByName
  before generating rules that reference it.
- tunnel: ValidateConfig checks TunnelID, PublicDomain, GatewayDomain are set
  and credentials file exists. WriteConfig now validates before generating.
2026-07-14 11:54:58 +00:00
Paul Payne
a0ab8faa1c Add SafeApply lifecycle, health tracking, convergence loop, and startup checks
SafeApply pattern (validate → backup → write → reload → verify → rollback):
- HAProxy: SafeApply wraps existing validate+write+reload with backup and
  post-reload health check; rolls back to .bak on failure
- dnsmasq: SafeApply validates via dnsmasq --test, backs up, atomic writes,
  restarts, verifies daemon is active; rolls back on failure
- nftables: SafeApply validates via nft -c, backs up, atomic writes, applies
  to kernel, verifies table loaded; rolls back on failure

Health tracking:
- Add SubsystemHealth and Health structs to reconciler
- Track per-subsystem status (ok/degraded/error) after each reconcile
- Detect recovery: previous error → current ok broadcasts recovery event
- GET /api/v1/health/reconcile endpoint exposes health state
- HAProxy tracks excluded domains as "degraded" state

SSE error events:
- Broadcast haproxy:error, dnsmasq:error on SafeApply failure
- Broadcast haproxy:recovered, dnsmasq:recovered on recovery from error

Convergence loop:
- 5-minute periodic reconcile drives system toward desired state
- Catches config drift, daemon crashes, transient failures
- Serialized by reconcile mutex — no race with event-driven reconciles

Startup:
- CheckPrerequisites verifies required (dnsmasq, haproxy) and optional
  (wg, authelia, cscli, cloudflared, nft) binaries before first reconcile
2026-07-14 11:44:44 +00:00
Paul Payne
88acd437a1 Add resiliency primitives: atomic writes, reconcile mutex, state backup
- Add storage.WriteFileAtomic (temp + rename) and storage.CopyFile helpers
- Convert all 8 production config writers to atomic writes: config/state.yaml,
  dnsmasq, nftables, domains, wireguard (config + secrets + peers + wg0.conf),
  tunnel/cloudflared
- Add sync.Mutex to Reconciler to serialize concurrent Reconcile() calls
  triggered by domain registration goroutines
- Add state.yaml backup (.bak) before every write; LoadState falls back to
  backup if primary is corrupted
- Reconciler refuses to use empty config on corruption (only on first run
  when no state file exists yet)
2026-07-14 11:37:33 +00:00
Paul Payne
3172e56288 Standardize codebase consistency: naming, JSON tags, logging, error wrapping
- JSON tags: fix snake_case to camelCase in dnsmasq (configFile, domainsConfigured,
  lastRestart), crowdsec Machine (lastPush, lastHeartbeat), network (primaryIP,
  primaryInterface). cscli raw parsing structs keep snake_case to match CLI output.
- Error wrapping: fix %v to %w in enableAuthelia for proper error chain preservation
- Naming: rename dnsmasq.ConfigGenerator to dnsmasq.Manager (matches all other packages),
  rename ServiceStatus to Status in dnsmasq and haproxy (matches authelia, crowdsec, etc.)
- Logging: standardize all slog calls to use "component" key instead of message prefixes.
  Affects reconcile, dnsfilter, ddns — now consistent with dnsmasq, haproxy, nftables, sse.
2026-07-14 04:38:48 +00:00
Paul Payne
428d47f876 Refactor architecture: extract reconciler, add interfaces, reduce complexity, improve test coverage
Architecture:
- Extract reconcileNetworking into internal/reconcile package with 7 consumer-side interfaces
- Add locked modifyState helper to fix state.yaml read-modify-write race condition
- Extract CrowdSec and VPN handler groups with interfaces documenting dependency surface
- Replace raw map[string]any YAML manipulation with typed AddDHCPStaticLease/RemoveDHCPStaticLease
- Extract Cloudflare API functions into cfClient struct, eliminating repeated auth boilerplate

Complexity reduction:
- haproxy.GenerateWithOpts: 50 → 6 (extracted 8 focused helpers)
- reconcile.Reconcile: 42 → 11 (extracted buildRoutes, buildDNSEntries, writeHAProxyConfig)
- AutheliaUpdateConfig: 25 → 10 (extracted enableAuthelia/disableAuthelia)

Test coverage improvements:
- reconcile: 4.5% → 56.8% (stub-based tests for route building, DNS entries, orchestration)
- dnsfilter: 10.7% → 45.6% (Manager.Compile, AddList, ToggleList, custom entries)
- config: 67.3% → 89.8% (DHCP static lease mutation tests)
2026-07-14 04:21:30 +00:00
Paul Payne
1e7d93256e Support custom domains. Fix host-record/address resolution. 2026-07-13 00:26:13 +00:00
Paul Payne
65c7e56b0a Fix APT repository configuration and update signing key instructions 2026-07-13 00:25:17 +00:00
Paul Payne
d0b645b9b5 Add error handling for Cloudflare API response status codes 2026-07-12 21:55:43 +00:00
Paul Payne
1d1d6c605c Adds dist. 2026-07-12 21:40:15 +00:00
Paul Payne
518cdbbce5 Add DNS filtering with dnsmasq address=/ directives for wildcard blocking
Adds Pi-hole-style DNS filtering using dnsmasq's native address=/ directives,
which block domains and all subdomains. Users subscribe to blocklists by URL
or upload files, with suggested lists from Hagezi, Steven Black, and OISD.
Background runner refreshes lists on a configurable interval (default 24h).
2026-07-12 12:44:19 +00:00
Paul Payne
134c01fe5e Add SMTP support for Authelia notifications and fix config persistence
SMTP: Add host, port, username, sender, and password fields to the
Authelia configuration. Uses modern address format (submission:// for
STARTTLS on 587, smtps:// for implicit TLS on 465) with explicit TLS
server name. Falls back to filesystem notifier when SMTP is not
configured.

Fixes: Config changes now persist before attempting service restart,
so a restart failure (e.g. bad SMTP credentials) no longer prevents
saving. The specific Authelia error is extracted from the journal and
shown in the UI.

Frontend: SMTP fields added to the Authentication config card. Form
no longer continuously resets from server state while user is editing.
OIDC client edit UI added (pencil icon).
2026-07-12 12:42:36 +00:00
Paul Payne
d796a79f79 Add Authelia as centralized authentication and OIDC provider
Authelia runs as a managed native service on Wild Central, providing
two integration patterns for network services:

- Forward-auth via HAProxy for apps without native SSO (Lua auth-request
  script intercepts requests, redirects unauthenticated users to login portal)
- OIDC provider for apps with native support (Gitea, Grafana, etc.)

Backend: new internal/authelia/ package with service manager, config
generation, file-based user management (argon2id), and OIDC client
management. API endpoints for status, config, users CRUD, and OIDC
clients CRUD.

HAProxy: config generator extended with lua-load, auth-request
directives, and Authelia backend. Auth directives scoped to session
cookie domain — only subdomains of the auth portal's parent are
eligible for forward-auth.

Frontend: Authentication page with enable/disable, user management,
OIDC client management (add/edit/delete), and protected domains
toggles. Advanced subsystem page for raw config view. Dashboard
service card and sidebar entries.
2026-07-12 11:47:37 +00:00
Paul Payne
24bb976652 Replace generic cloud-lightning logo with custom network nexus mark
The new icon represents Wild Central's role as a network hub —
a central node radiating connections to services. Updated favicon,
sidebar logo component, theme color, and PWA manifest name.
2026-07-12 04:25:41 +00:00
Paul Payne
43d407bf2e UX improvements. 2026-07-12 00:40:19 +00:00
Paul Payne
994c9fbfdf Better UX on domain forms. 2026-07-11 23:50:24 +00:00
Paul Payne
a48d955dc0 Remove unused DDNS functionality from DashboardComponent 2026-07-11 23:45:59 +00:00
Paul Payne
6490fef5d5 Added domain form func. 2026-07-11 23:40:56 +00:00
Paul Payne
9e8f23aab7 Update routing and sidebar links to remove '/central' prefix 2026-07-11 23:40:34 +00:00
Paul Payne
ff6d6bbd0b Refactor certificate output parsing and add unit tests for parseCertOutput function 2026-07-11 23:25:47 +00:00
Paul Payne
5d8fe6a754 Removes more Wild Cloud cruft. 2026-07-11 23:21:23 +00:00
Paul Payne
ac66ba653d Removes Wild Cloud cruft. 2026-07-11 23:05:17 +00:00
Paul Payne
f9d87ff975 Replace InstanceConfig with DNSEntry for dnsmasq config generation
InstanceConfig was a ~50-field struct designed for Wild Cloud k8s instances,
but only 3 fields were ever read by dnsmasq (the sole consumer). The
reconciliation bridge constructed fake InstanceConfig objects from registered
domains just to satisfy this interface.

Replace with DNSEntry{Domain, IP} — a 2-field struct purpose-built for
dnsmasq. All domains get local=/ directives to prevent AAAA queries from
leaking to upstream DNS (Happy Eyeballs / RFC 8305 latency on LAN).

Removed dead code: InstanceConfig, NodeConfig, LoadCloudConfig,
SaveCloudConfig, DeepMerge, LoadMergedInstanceConfig, EnsureInstanceConfig,
instance path helpers, instanceLoadBalancerIP, GenerateInstanceConfig,
and all modular per-instance dnsmasq methods.
2026-07-11 20:42:33 +00:00
Paul Payne
1f3fcf50b4 Advanced pages. Model-driven controls. 2026-07-11 02:31:14 +00:00
Paul Payne
cd2f28df34 Remove dead CentralState types (replaced by resource-specific types) 2026-07-10 22:48:40 +00:00
Paul Payne
79f38d2750 Replace state blob with resource-oriented API endpoints
Split /api/v1/state into dedicated resource endpoints:
- /api/v1/operator, /api/v1/central-domain
- /api/v1/ddns/config, /api/v1/dns/settings
- /api/v1/dhcp/config (moved from /dnsmasq/dhcp/)
- /api/v1/nftables/config, /api/v1/haproxy/routes

Each page now talks to its own resource instead of deep-merging
a blob. Removes useConfig hook, CentralState type, and legacy
config methods.
2026-07-10 22:45:36 +00:00
Paul Payne
79c0c32b98 services -> domains 2026-07-10 20:46:22 +00:00
Paul Payne
3c99d26830 Auto-detect Central domain for CORS allowed origins
Read the configured Central domain from state.yaml and add it as an
HTTPS origin so the web UI at e.g. https://central.payne.io works
without manually setting WILD_CENTRAL_CORS_ORIGINS.
2026-07-10 19:12:27 +00:00
Paul Payne
6aa1e7d438 Resilient HAProxy config: isolate broken services on validation failure
When haproxy -c fails, parse ALERT line numbers, map to service domains
via # service: comments in generated config, exclude broken services,
and regenerate. One retry — prevents a single bad registration from
blocking all config updates.
2026-07-10 07:00:27 +00:00
Paul Payne
68d6fde80d Show routes, TLS certs, and port-forwarding on services page
- Add Routes model to services UI (paths, headers, IP whitelisting per route)
- Show TLS cert info per service with inline provision/renew actions
- Remove TLS Certificates section from dashboard (now on services page)
- Make gateway router port list dynamic from config + VPN state
- Add TODO for header validation in HAProxy config generation
2026-07-10 06:10:40 +00:00
Paul Payne
e78a1d548a feat: Add instructions for registering new CrowdSec agents 2026-07-10 05:22:01 +00:00
Paul Payne
43253ca120 Support routes. 2026-07-10 05:21:43 +00:00
Paul Payne
5c26c7530a It's state, not config. 2026-07-10 05:21:03 +00:00
Paul Payne
4feaa63da0 refactor: Replace 'reach' field with 'public' boolean in service registration and related components 2026-07-10 02:13:37 +00:00
Paul Payne
fffc84e14c refactor: Remove routing summary — controls already communicate it
The backend field + Public toggle already tell you where traffic
routes. Removed redundant "Routes to X on LAN, Y externally" line
and unused ddns/publicIp/backendHost vars.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-10 00:17:52 +00:00
Paul Payne
e1582edfec refactor: Remove redundant status table from service cards
The toggles (Public, Subdomains, Central TLS) already communicate
everything the table was restating. Replaced with a single routing
summary line: "Routes to X on LAN, Y externally."

Cleaner, no redundancy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:58:01 +00:00
Paul Payne
7c9e8e51d7 feat: All service fields editable inline
Service cards now have full inline editing:
- Backend address: editable input, updates on blur
- Public/Private: toggle switch, updates immediately
- Subdomains: toggle switch, updates immediately
- TLS (Central TLS / Passthrough): toggle switch, updates
  both tls mode and backend type together
- Deregister button

Add form also uses toggle for TLS instead of dropdown.
Removed unused Badge and Select imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:53:20 +00:00
Paul Payne
98385449b4 feat: Service cards with inline toggles + updated docs
Service cards now have:
- Public/Private toggle (switches reach)
- Subdomains toggle (include *.domain)
- TLS badge (passthrough vs terminate, read-only for now)
- Inline status details (DNS, Proxy, TLS status)
- Deregister button
- Add Service form with toggles instead of dropdowns

The card speaks the user's language (public/private, subdomains on/off)
not implementation details (tcp-passthrough vs http, reach: internal).

Also updated docs/registrations.md:
- Added "User-facing concepts" section mapping API fields to toggles
- Added batch deregister endpoint
- Added backend.health field
- Cleaner examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:50:05 +00:00
Paul Payne
c9732ffa7f style: Move VPN above Firewall in sidebar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:42:34 +00:00
Paul Payne
b0a3e2f1f1 fix: Allow multiple service cards expanded simultaneously
Changed from single expandedDomain string to a Set<string> so
multiple cards can be open at once.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:38:02 +00:00
Paul Payne
c96af69189 fix: TLS status dot colors — grey/red/green
- na (passthrough, Central doesn't handle TLS): grey checkmark
- error (terminate but cert missing): red alert icon
- ok (terminate and cert exists): green checkmark

Previously passthrough showed green (wrong — Central isn't doing
anything) and missing certs showed amber (should be red — broken).

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