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>
This commit is contained in:
2026-07-09 20:29:30 +00:00
parent 8874bc6281
commit 548be903e7
5 changed files with 123 additions and 100 deletions

View File

@@ -55,19 +55,18 @@ func (api *API) reconcileNetworking() {
}
}
// Determine central domain for HAProxy — only if its cert exists
centralDomain := ""
// Inject Central's own domain as an HTTP route (from config, not registration).
// Central is just another L7 service from HAProxy's perspective.
if d := globalCfg.Cloud.Central.Domain; d != "" {
if _, err := os.Stat(certbot.HAProxyCertPath(d)); err == nil {
centralDomain = d
} else {
slog.Debug("reconcile: skipping central domain in HAProxy (no cert)", "domain", d)
}
centralPort := api.getRunningPort()
httpRoutes = append([]haproxy.HTTPRoute{{
Name: "wild-central",
Domain: d,
Backend: fmt.Sprintf("127.0.0.1:%d", centralPort),
}}, httpRoutes...)
}
// Only include L7 HTTP routes for services that have a cert.
// HAProxy uses a cert directory — each service needs its own <domain>.pem
// or be covered by a wildcard cert in the same directory.
certsDir := "/etc/haproxy/certs/"
var activeHTTPRoutes []haproxy.HTTPRoute
for _, route := range httpRoutes {
@@ -80,9 +79,8 @@ func (api *API) reconcileNetworking() {
// Generate and write HAProxy config
haproxyCfg := api.haproxy.GenerateWithOpts(instanceRoutes, nil, haproxy.GenerateOpts{
CentralDomain: centralDomain,
HTTPRoutes: activeHTTPRoutes,
WildcardCert: certsDir,
HTTPRoutes: activeHTTPRoutes,
CertsDir: certsDir,
})
if err := api.haproxy.WriteConfig(haproxyCfg); err != nil {