carlos / platform Public

edges file / console: deny entry at the config layer, the door's normaliser, a once-per-name withheld line (#287 round 2)

The tunnel-host deny entry moves to cmd/console's denyDomains — the
package never read env and should not start; the plain alias branch
uses NormalizeAliasHost, the door's own check, not the weaker shape
test; an alias withheld for dns_status says so once per process so a
region EIP missing from PLATFORM_EDGE_IPS is not a silent home-box-only
domain. Stale comments about the operator listing the tunnel host
corrected.

Depends on #303/#304: the pointing stamp must be written on the HOME
box, which only the per-box health pass does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 2130c5533b800cf7a6acc98b565cd60f941c8878 parent b6468b0
5 files changed, +51 −22
  • cmd/console/main.go +14 −5
  • internal/console/apidomains.go +3 −11
  • internal/domains/aliases.go +6 −5
  • internal/host/edgesfile.go +17 −1
  • internal/host/edgesfile_test.go +11 −0
diff --git a/cmd/console/main.go b/cmd/console/main.go
index 335177c32..a70e25246 100644
--- a/cmd/console/main.go
+++ b/cmd/console/main.go
@@ -678,11 +678,11 @@ func redirectHosts() []string {
// denyDomains parses CONSOLE_DENY_DOMAINS (comma-separated) — hostnames, and
// everything under them, that `carlos domains attach` refuses (issue #151).
// This is the operator's list of names the console cannot derive on its own:
-// the edge's reserved tunnel hostname, retired console origins, any other
-// platform-managed domain. The console's own origin host and
-// PLATFORM_APPS_DOMAIN are added by the door itself (deniedDomains,
-// internal/console/apidomains.go), so an unset variable is a legal — and
-// still safe — deployment. Blank entries are dropped: an empty string in the
+// retired console origins, any other platform-managed domain. The edge's
+// reserved tunnel hostname is folded in below from CARLOS_TUNNEL_HOST; the
+// console's own origin host and PLATFORM_APPS_DOMAIN are added by the door
+// itself (deniedDomains, internal/console/apidomains.go), so an unset
+// variable is a legal — and still safe — deployment. Blank entries are dropped: an empty string in the
// list would be a deny entry no hostname could avoid matching.
func denyDomains() []string {
var out []string
@@ -691,6 +691,15 @@ func denyDomains() []string {
out = append(out, d)
}
}
+ // The fleet hub's reserved hostname (CARLOS_TUNNEL_HOST — the edge's
+ // variable, but host.env is one file per box) is Allowed by the edge and
+ // is no registry route, so no derived deny entry names it; a tenant must
+ // not attach it. #151 made it the operator's job to list; folding it in
+ // here makes the MUST hold without anyone remembering. Env is read at
+ // this layer only, as every other console setting is.
+ if h := strings.ToLower(strings.TrimSpace(os.Getenv("CARLOS_TUNNEL_HOST"))); h != "" {
+ out = append(out, strings.TrimSuffix(h, "."))
+ }
return out
}
diff --git a/internal/console/apidomains.go b/internal/console/apidomains.go
index 04a6d099a..192c0f220 100644
--- a/internal/console/apidomains.go
+++ b/internal/console/apidomains.go
@@ -40,7 +40,6 @@ import (
"log"
"net/http"
"net/url"
- "os"
"sort"
"strconv"
"strings"
@@ -53,8 +52,9 @@ import (
)
// deniedDomains is the deny list every attach is checked against: the
-// operator's own CONSOLE_DENY_DOMAINS entries, plus the three names this
-// console can derive without being told (spec §5: "all derived from
+// operator's CONSOLE_DENY_DOMAINS entries (cmd/console folds the tunnel
+// hostname into those), plus the names this console can derive without
+// being told (spec §5: "all derived from
// console/deployment config, not hardcoded").
//
// The derived three are not optional and not the operator's to forget:
@@ -98,14 +98,6 @@ func (s *Server) deniedDomains() []string {
deny = append(deny, h)
}
}
- // The fleet hub's reserved hostname (CARLOS_TUNNEL_HOST, the edge's env —
- // host.env is shared with the console on the same box) is Allowed by the
- // edge but is no registry route, so nothing else here names it; a tenant
- // must not attach it (#151 said "MUST be in CONSOLE_DENY_DOMAINS" —
- // this makes it so without relying on the operator remembering).
- if h := normalizeHost(os.Getenv("CARLOS_TUNNEL_HOST")); h != "" {
- deny = append(deny, h)
- }
return deny
}
diff --git a/internal/domains/aliases.go b/internal/domains/aliases.go
index 79b52473f..1a11a9b1a 100644
--- a/internal/domains/aliases.go
+++ b/internal/domains/aliases.go
@@ -65,11 +65,12 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist
c.Host, c.Route, release.RouteAccount(rt), rt.App, c.Account, c.App)
} else if _, taken := byHost[c.Host]; taken {
log.Printf("domains: alias %s skipped: a real route owns that host", c.Host)
- } else if !validHostShape(c.Host) {
- // Same check the wildcard branch below applies: both doors
- // normalise, but this row now becomes a key in a file every
- // edge folds (#287), so a hand-written claim must not reach it.
- log.Printf("domains: alias %s refused: invalid host shape", c.Host)
+ } else if n, ok := NormalizeAliasHost(c.Host); !ok || n != c.Host {
+ // The DOOR's own check (NormalizeAliasHost: shape plus an
+ // interior dot, already lower-case): both doors normalise,
+ // but this row now becomes a key in a file every edge folds
+ // (#287), so a hand-written claim must not reach it.
+ log.Printf("domains: alias %s refused: not a normalised hostname", c.Host)
} else {
desired[c.Host] = true
if err := reg.UpsertAlias(registry.RouteAlias{
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go
index bfa9e6d18..715c7467d 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -9,6 +9,7 @@ import (
"math/rand/v2"
"os"
"strings"
+ "sync"
"time"
"github.com/carlosframework/platform/internal/registry"
@@ -103,6 +104,10 @@ type edgesFile struct {
// of the change gate like every other field except Generation: a region
// relabel is a real change and must write, not be swallowed as a
// timestamp-only refresh would be.
+// withheldAliasSaid throttles the not-published line in edgesFilePass to
+// once per alias per process (cleared when the alias publishes).
+var withheldAliasSaid sync.Map
+
func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, box, eip, region string, last *[]byte) error {
routes, err := reg.List()
if err != nil {
@@ -158,12 +163,23 @@ func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry,
return fmt.Errorf("edges: list aliases: %w", err)
}
for _, a := range aliases {
- if a.Alias == "" || strings.HasPrefix(a.Alias, "*.") || a.DNSStatus != "pointing" {
+ if a.Alias == "" || strings.HasPrefix(a.Alias, "*.") {
continue
}
if _, served := hosts[a.Host]; !served {
continue
}
+ if a.DNSStatus != "pointing" {
+ // Withheld, and said so ONCE per name per process: an alias
+ // whose DNS resolves to an address outside PLATFORM_EDGE_IPS
+ // (a region EIP missing from that list is the usual cause) is
+ // otherwise served on its home box only, silently.
+ if _, said := withheldAliasSaid.LoadOrStore(a.Alias, true); !said {
+ log.Printf("edges: alias %s not published — dns_status %q, want \"pointing\" (does PLATFORM_EDGE_IPS list every armed edge?)", a.Alias, a.DNSStatus)
+ }
+ continue
+ }
+ withheldAliasSaid.Delete(a.Alias)
if _, taken := hosts[a.Alias]; taken {
continue
}
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go
index b29c01bbe..b56d0392b 100644
--- a/internal/host/edgesfile_test.go
+++ b/internal/host/edgesfile_test.go
@@ -3,6 +3,8 @@ package host
import (
"context"
"encoding/json"
+ "log"
+ "os"
"path/filepath"
"strings"
"testing"
@@ -663,6 +665,9 @@ func TestEdgesFilePublishesPointingAliasesOfServedPrimaries(t *testing.T) {
}
return f.Hosts
}
+ var buf strings.Builder
+ log.SetOutput(&buf)
+ t.Cleanup(func() { log.SetOutput(os.Stderr) })
hosts := publish()
h, ok := hosts["www.customer.com"]
if !ok || h.Role != "home" || !h.Alias || h.Platform {
@@ -676,6 +681,12 @@ func TestEdgesFilePublishesPointingAliasesOfServedPrimaries(t *testing.T) {
if h := hosts["real.example.net"]; !h.Platform || h.Alias {
t.Errorf("an alias colliding with a real route displaced the route's entry: %+v", h)
}
+ // A withheld alias says so once per process, not once per pass.
+ publish()
+ publish()
+ if n := strings.Count(buf.String(), "unpointed.customer.com not published"); n != 1 {
+ t.Errorf("withheld line logged %d times over 3 passes; want 1:\n%s", n, buf.String())
+ }
// Detach: gone on the next pass.
if err := reg.DeleteAlias("www.customer.com"); err != nil {
t.Fatal(err)