Completes the naming separation: nftables table, rules file, dnsmasq config,
resolved config, systemd unit, sudoers rule, and temp files now all use
the wild-central name.
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)
- 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
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
- 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
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.
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
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.
- 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.
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
- 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)
- 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.
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).
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).
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.
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.
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.
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.
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.
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.
- 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
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>
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>