carlos / platform Public

Enforce the reserved sidecar suffix at every route-minting door

Paul Campbell pushed by paul@keymail.dev 95b713ce3764d5d9ebab18182d43996f5a7c3982 parent 089eb0e
6 files changed, +377 −2
  • cli.go +53 −0
  • cli_test.go +172 −0
  • internal/console/domainattach.go +10 −0
  • internal/console/domainattach_test.go +19 −0
  • internal/domains/aliases.go +33 −2
  • internal/domains/aliases_test.go +90 −0
diff --git a/cli.go b/cli.go
index 102ffb7df..186c87d30 100644
--- a/cli.go
+++ b/cli.go
@@ -86,6 +86,9 @@ func runAdd(args []string) {
log.Fatal("usage: carlos add [flags] <host>")
}
host := fs.Arg(0)
+ if err := addRail(host, *kind); err != nil {
+ log.Fatalf("add %s: %v", host, err)
+ }
if reservedSlugs()[*slug] {
log.Fatalf("slug %q is reserved", *slug)
}
@@ -655,6 +658,9 @@ func runRoute(args []string) {
if rt == nil {
log.Fatalf("route: no route for host %q", *host)
}
+ if err := suffixRail(*rt); err != nil {
+ log.Fatalf("route: %v", err)
+ }
switch {
case grantAxis:
runRouteGrant(r, *rt, *grant, *revoke, *dryRun)
@@ -677,6 +683,9 @@ func runRoute(args []string) {
if rt == nil {
log.Fatalf("route: no route for host %q", *host)
}
+ if err := suffixRail(*rt); err != nil {
+ log.Fatalf("route: %v", err)
+ }
candidates = []registry.Route{*rt}
} else {
routes, err := r.List()
@@ -712,6 +721,15 @@ func runRoute(args []string) {
warnf(os.Stdout, "%s: skipped — platform-minted route; use -host %s to change it explicitly\n", rt.Host, rt.Host)
continue
}
+ if err := suffixRail(rt); err != nil {
+ // A real provisioned sidecar's App equals its owning app's, so
+ // this is not hypothetical: an -app sweep legitimately meets
+ // these rows. Skipped like a Platform route, not fatal — one
+ // row under the reserved suffix must not abort the rest of
+ // the batch.
+ warnf(os.Stdout, "%s: skipped — %v; use -host %s to see the refusal explicitly\n", rt.Host, err, rt.Host)
+ continue
+ }
candidates = append(candidates, rt)
}
if len(candidates) == 0 {
@@ -1175,6 +1193,41 @@ func capsLabel(col string) string {
return col
}
+// addRail refuses the two things `carlos add` must never mint: a host under
+// the reserved sidecar suffix, and a sidecar row itself. Sidecars are
+// provisioned through their own record family (carlos sidecars create), so
+// an operator minting one by hand would produce a row no reconcile pass
+// owns and no record explains. Defence in depth behind Task 3's structural
+// refusal to SERVE a Kind=="sidecar" row (edge.go's Reload): this rail
+// guards the OTHER direction — an ordinary-kind row planted under a
+// sidecar's reserved name, which edge would serve without a second thought.
+func addRail(host, kind string) error {
+ if strings.HasSuffix(host, provision.SidecarSuffix) {
+ return fmt.Errorf("%s is under the reserved suffix %s — sidecar hosts are minted by `carlos sidecars create`, never by hand", host, provision.SidecarSuffix)
+ }
+ if kind == "sidecar" {
+ return errors.New("carlos add does not mint sidecars — use `carlos sidecars create -app <app> <name>`")
+ }
+ return nil
+}
+
+// suffixRail is `carlos route`'s side of the same guarantee addRail closes
+// at `carlos add`: this verb cannot MINT a row under the reserved sidecar
+// suffix (r.Get returns nil for one, and every axis below refuses before
+// opening the registry for writing) — but it CAN keep an existing one alive,
+// repoint its channel, or grant it capabilities, and that is the door this
+// rail closes. It applies to every axis this verb writes (-channel,
+// -environment, -tunnel, -grant/-revoke, -hibernate, -backing, -unit): the
+// row it guards against is not a genuine Kind=="sidecar" row (edge.go's
+// Reload never serves those regardless) but an ordinary-kind row wearing a
+// sidecar's reserved name, which this verb must never keep converging.
+func suffixRail(rt registry.Route) error {
+ if strings.HasSuffix(rt.Host, provision.SidecarSuffix) {
+ return fmt.Errorf("%s is under the reserved suffix %s — this verb does not touch routes under it; sidecar hosts are owned by their own record family, never this one", rt.Host, provision.SidecarSuffix)
+ }
+ return nil
+}
+
// grantRail is everything a -grant/-revoke can be refused for from the ROW
// alone: the token's own validity, plus the two structural refusals. Pure, so
// every refusal is table-testable without a registry.
diff --git a/cli_test.go b/cli_test.go
index b86b37ef7..0a06d4439 100644
--- a/cli_test.go
+++ b/cli_test.go
@@ -15,6 +15,7 @@ import (
"testing"
"github.com/carlosframework/platform/internal/caps"
+ "github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
)
@@ -1049,3 +1050,174 @@ func TestNotesFileAndNotesConflict(t *testing.T) {
t.Fatalf("refusal does not name -notes-file:\n%s", out)
}
}
+
+// --- Task 4: the reserved sidecar suffix, enforced at every minting door ---
+//
+// Task 3 already refuses to SERVE a Kind=="sidecar" row structurally
+// (edge.go's Reload skips it at the top of the loop). These rails are
+// defence in depth behind that guarantee, and they close the OTHER
+// direction: a row of a DIFFERENT kind minted (by hand, or before this
+// suffix was reserved) with a Host under provision.SidecarSuffix, which
+// would be served — undermining the very reservation Task 3 depends on.
+
+// TestAddRefusesSidecarSuffixAndKind pins addRail's predicate exactly as the
+// task-4 brief specifies it.
+func TestAddRefusesSidecarSuffixAndKind(t *testing.T) {
+ if err := addRail("bot.eleven.bdm.sidecar.internal", "instance"); err == nil {
+ t.Fatal("a host under the reserved suffix must be refused")
+ }
+ if err := addRail("app.example.com", "sidecar"); err == nil {
+ t.Fatal("carlos add must not mint a sidecar row — sidecars are provisioned, not added")
+ }
+ if err := addRail("app.example.com", "instance"); err != nil {
+ t.Fatalf("an ordinary add must still work: %v", err)
+ }
+}
+
+// TestAddRefusesSidecarSuffixWiredIntoRunAdd drives `carlos add` as a
+// subprocess: addRail's own test proves the predicate, this proves runAdd
+// actually calls it, before any registry write.
+func TestAddRefusesSidecarSuffixWiredIntoRunAdd(t *testing.T) {
+ bin := buildCarlos(t)
+ tmp := t.TempDir()
+ scratch := t.TempDir()
+ regPath := filepath.Join(tmp, "registry.db")
+
+ run := func(args ...string) (string, error) {
+ t.Helper()
+ cmd := exec.Command(bin, args...)
+ cmd.Env = carlosTestEnv(t)
+ cmd.Dir = scratch
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+ }
+
+ host := "bot.eleven.bdm" + provision.SidecarSuffix
+ out, err := run("add", "-registry", regPath, "-app", "eleven", host)
+ if err == nil {
+ t.Fatalf("carlos add %s: want non-zero exit, got 0\n%s", host, out)
+ }
+ if !strings.Contains(out, provision.SidecarSuffix) {
+ t.Fatalf("refusal does not name the reserved suffix:\n%s", out)
+ }
+ reg, rerr := registry.Open(regPath)
+ if rerr != nil {
+ t.Fatalf("open registry: %v", rerr)
+ }
+ defer reg.Close()
+ if rt, gerr := reg.Get(host); gerr != nil || rt != nil {
+ t.Fatalf("a refused add still wrote a row: %+v, %v", rt, gerr)
+ }
+
+ out, err = run("add", "-registry", regPath, "-app", "eleven", "-kind", "sidecar", "app.example.com")
+ if err == nil {
+ t.Fatalf("carlos add -kind sidecar: want non-zero exit, got 0\n%s", out)
+ }
+ if !strings.Contains(out, "sidecar") {
+ t.Fatalf("refusal does not mention sidecar:\n%s", out)
+ }
+ if rt, gerr := reg.Get("app.example.com"); gerr != nil || rt != nil {
+ t.Fatalf("a refused -kind sidecar add still wrote a row: %+v, %v", rt, gerr)
+ }
+}
+
+// TestSuffixRailRefusesReservedSuffix: `carlos route` cannot MINT a row
+// under the reserved suffix (r.Get returns nil and the verb refuses before
+// any rail runs) — but it CAN keep an existing one alive or grant it
+// capabilities, which is the door this rail closes. Pure and table-tested
+// for the same reason grantRail/backingRail are: a rail nobody can exercise
+// without running the whole process is a rail nobody tests.
+func TestSuffixRailRefusesReservedSuffix(t *testing.T) {
+ under := registry.Route{Host: "bot.eleven.bdm" + provision.SidecarSuffix, Kind: "instance", App: "eleven", Account: "bdm"}
+ err := suffixRail(under)
+ if err == nil {
+ t.Fatalf("suffixRail(%+v) accepted; want a refusal", under)
+ }
+ if !strings.Contains(err.Error(), provision.SidecarSuffix) {
+ t.Fatalf("refusal %q does not name the reserved suffix", err)
+ }
+ ordinary := registry.Route{Host: "app.example.com", Kind: "instance", App: "eleven", Account: "bdm"}
+ if err := suffixRail(ordinary); err != nil {
+ t.Fatalf("suffixRail(%+v) = %v, want accepted", ordinary, err)
+ }
+}
+
+// TestRouteRefusesReservedSuffixWiredIn drives `carlos route` as a
+// subprocess against a row planted directly in the registry — the only way
+// such a row can exist post-Task-4, since `carlos add` now refuses to mint
+// one — proving suffixRail is wired into every axis this verb writes, not
+// only provable in isolation.
+func TestRouteRefusesReservedSuffixWiredIn(t *testing.T) {
+ bin := buildCarlos(t)
+ tmp := t.TempDir()
+ scratch := t.TempDir()
+ regPath := filepath.Join(tmp, "registry.db")
+ host := "bot.eleven.bdm" + provision.SidecarSuffix
+
+ reg, err := registry.Open(regPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: host, Kind: "instance", App: "eleven", Account: "bdm", Status: "ready",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ reg.Close()
+
+ run := func(args ...string) (string, error) {
+ t.Helper()
+ cmd := exec.Command(bin, args...)
+ cmd.Env = carlosTestEnv(t)
+ cmd.Dir = scratch
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+ }
+
+ for _, args := range [][]string{
+ {"route", "-registry", regPath, "-host", host, "-channel", "canary/x"},
+ {"route", "-registry", regPath, "-host", host, "-grant", "port:25"},
+ {"route", "-registry", regPath, "-host", host, "-hibernate"},
+ {"route", "-registry", regPath, "-host", host, "-backing", registry.BackingUnit},
+ {"route", "-registry", regPath, "-host", host, "-unit", "console.service"},
+ } {
+ out, err := run(args...)
+ if err == nil {
+ t.Fatalf("route %v: want non-zero exit, got 0\n%s", args[3:], out)
+ }
+ if !strings.Contains(out, provision.SidecarSuffix) {
+ t.Fatalf("route %v: refusal does not name the reserved suffix:\n%s", args[3:], out)
+ }
+ }
+
+ // The -app bulk sweep must not abort on a suffix row swept in alongside
+ // ordinary ones (a real provisioned sidecar's App equals its owning
+ // app's, so this is not hypothetical) — it is skipped, like a Platform
+ // route, while its ordinary sibling still moves.
+ reg2, err := registry.Open(regPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := reg2.Upsert(registry.Route{
+ Host: "eleven.bdm.oncarlos.com", Kind: "instance", App: "eleven", Account: "bdm", Status: "ready",
+ }); err != nil {
+ t.Fatal(err)
+ }
+ reg2.Close()
+ // -environment "-" (clear) is a no-op for a fresh route (Environment is
+ // already "") and needs no bucket/pubkey setup — chosen so this exercises
+ // the -app sweep's suffix skip without dragging in the channel axis's
+ // store/signature machinery, which is not what this test is about.
+ out, err := run("route", "-registry", regPath, "-app", "eleven", "-account", "bdm", "-environment", "-")
+ if err != nil {
+ t.Fatalf("route -app eleven -environment -: %v\n%s", err, out)
+ }
+ if !strings.Contains(out, "eleven.bdm.oncarlos.com") {
+ t.Fatalf("the ordinary sibling route was not moved:\n%s", out)
+ }
+ // The suffix row is named in a SKIP line (like a Platform route's own
+ // skip), not silently dropped — but it must never appear as WRITTEN.
+ if !strings.Contains(out, host+": skipped") {
+ t.Fatalf("the suffix row must be named in a skip line, not silently dropped:\n%s", out)
+ }
+}
diff --git a/internal/console/domainattach.go b/internal/console/domainattach.go
index 40dce96a9..5914cb816 100644
--- a/internal/console/domainattach.go
+++ b/internal/console/domainattach.go
@@ -93,6 +93,16 @@ func (s *Server) attachDomain(ctx context.Context, acct *Account, app, host, kin
return attachResult{}, &doorRefusal{http.StatusUnprocessableEntity,
"that hostname is platform-managed"}
}
+ // The reserved sidecar suffix, refused at the same policy stage and with
+ // the same status as the two checks above: this door must never mint a
+ // claim (let alone a route record) under a name provision.SidecarSuffix
+ // reserves. Defence in depth behind Task 3's structural refusal to SERVE
+ // a Kind=="sidecar" row (edge.go's Reload) — this closes the other
+ // direction, a member attaching an unrelated app's domain under it.
+ if strings.HasSuffix(host, provision.SidecarSuffix) {
+ return attachResult{}, &doorRefusal{http.StatusUnprocessableEntity,
+ "that hostname is reserved for sidecar processes"}
+ }
route, selfChannel, err := s.readyRouteHost(acct.Sqid, app, host)
if err != nil {
log.Printf("console: attach %s: registry: %v", host, err)
diff --git a/internal/console/domainattach_test.go b/internal/console/domainattach_test.go
index 93b90940c..6ad039f83 100644
--- a/internal/console/domainattach_test.go
+++ b/internal/console/domainattach_test.go
@@ -632,6 +632,25 @@ func TestAttachPlatformSpaceRefused(t *testing.T) {
}
}
+// TestAttachRefusesReservedSidecarSuffix: the attach door must refuse a
+// custom domain under provision.SidecarSuffix — never a panic, the family's
+// ordinary 422 (the same status HostDenied's platform-space refusal above
+// uses), and no claim left behind either. Defence in depth behind Task 3's
+// structural refusal to SERVE a Kind=="sidecar" row: this stops the OTHER
+// direction, a member attaching an unrelated app under a reserved name.
+func TestAttachRefusesReservedSidecarSuffix(t *testing.T) {
+ fx, acct := attachFixture(t)
+ shipVersion(t, fx, "story", "v1")
+ seedChannel(t, fx, "story", "canary/live", "v1")
+
+ host := "bot.eleven.bdm" + provision.SidecarSuffix
+ _, ref := fx.srv.attachDomain(context.Background(), acct, "story", host, "", "", "paul@keymail.dev")
+ if ref == nil || ref.Status != 422 {
+ t.Fatalf("attach %q: refusal = %+v, want 422", host, ref)
+ }
+ claimAbsent(t, fx, host)
+}
+
func TestDetachFirstRouteDeletesRecordThenClaim(t *testing.T) {
fx, acct := attachFixture(t)
shipVersion(t, fx, "story", "v1")
diff --git a/internal/domains/aliases.go b/internal/domains/aliases.go
index 27e0e7c61..9ff0e344b 100644
--- a/internal/domains/aliases.go
+++ b/internal/domains/aliases.go
@@ -12,6 +12,7 @@ package domains
import (
"context"
"log"
+ "strings"
"time"
"github.com/carlosframework/platform/internal/registry"
@@ -24,6 +25,21 @@ import (
// order); drops are logged, never silent.
const maxWildcardDomains = 32
+// sidecarSuffix mirrors provision.SidecarSuffix, pinned here rather than
+// imported — the same reason claims.go's hostLabelRe gives: internal/provision
+// (its reconcile.go) already imports internal/domains for account/route
+// materialization, so importing provision back would be a cycle.
+//
+// A sidecar host is refused a route at the structural level (edge.go's
+// Reload skips Kind=="sidecar"), but that guarantee protects only the
+// SIDECAR row itself. This constant guards the other direction, at the ONE
+// place that can mint a route_aliases row from nothing: a claim naming a
+// reserved-suffix host as either end of an alias would insert that host
+// into the edge's routing map serving an ORDINARY route's upstream — which
+// Reload's Kind check never sees, because the row it reads is the alias's
+// primary, not a Kind=="sidecar" row at all.
+const sidecarSuffix = ".sidecar.internal"
+
// ReconcileAliases derives route_aliases and wildcard_domains from the
// global claim records. Both tables are wholly pass-owned: every row traces
// to a claim, so removal needs no ownership flag. The authoritative tenancy
@@ -58,7 +74,15 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist
// derivation below.
if c.Route != "" {
rt, ok := byHost[c.Route]
- if !ok {
+ if strings.HasSuffix(c.Route, sidecarSuffix) || strings.HasSuffix(c.Host, sidecarSuffix) {
+ // Refused on EITHER end: a reserved-suffix primary must
+ // never be given a routable name, and an alias whose OWN
+ // name is under the suffix would insert a sidecar
+ // hostname into the routing map serving an ordinary
+ // route's upstream — the direction edge.go's Kind check
+ // cannot see, because this row's Kind is never "sidecar".
+ log.Printf("domains: alias %s refused: reserved sidecar suffix (primary %s)", c.Host, c.Route)
+ } else if !ok {
// primary not on this box — another box's claim
} else if rt.App != c.App || release.RouteAccount(rt) != c.Account {
log.Printf("domains: alias %s refused: route %s belongs to %s/%s, claim says %s/%s",
@@ -129,7 +153,14 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist
}
if c.Catchall {
star := "*." + c.Host
- if _, taken := byHost[star]; taken {
+ if strings.HasSuffix(star, sidecarSuffix) || strings.HasSuffix(primaryHost, sidecarSuffix) {
+ // Same both-ends guard as the plain-alias derivation above,
+ // through this route_aliases writer's OTHER call site: a
+ // catch-all's alias name is star, and star inherits c.Host's
+ // suffix, so this also catches a wildcard host minted under
+ // the reserved suffix.
+ log.Printf("domains: catch-all %s refused: reserved sidecar suffix (primary %s)", star, primaryHost)
+ } else if _, taken := byHost[star]; taken {
log.Printf("domains: catch-all %s skipped: a real route owns that host", star)
} else {
desired[star] = true
diff --git a/internal/domains/aliases_test.go b/internal/domains/aliases_test.go
index 58f39f12d..b4762ecb3 100644
--- a/internal/domains/aliases_test.go
+++ b/internal/domains/aliases_test.go
@@ -472,3 +472,93 @@ func TestReconcileWildcardEmptyRouteClaim(t *testing.T) {
t.Fatalf("want *.story.example -> story.example, got %+v", aliases)
}
}
+
+// --- Task 4: the reserved sidecar suffix, enforced at the alias door -----
+//
+// Task 3 already refuses to SERVE a Kind=="sidecar" row (edge.go's Reload).
+// ReconcileAliases must never mint a route_aliases row that names a
+// reserved-suffix host on EITHER end: as the primary (Host), which would
+// keep an unroutable sidecar name "alive" as somebody's alias target, and —
+// the direction the Task 3 review caught, and the one most likely to be
+// missed — as the alias's OWN name (Alias), which would insert a sidecar
+// hostname into the edge's routing map serving an ORDINARY route's upstream,
+// making Allowed() return true for it (an ACME order on a sidecar hostname).
+
+// TestReconcileAliasesRefusesReservedSuffixPrimary: the claim's Route names
+// a host under the reserved suffix. Even though such a route can exist in
+// the registry (a real provisioned sidecar's own row, Kind=="sidecar"),
+// ReconcileAliases must never let anything alias it.
+func TestReconcileAliasesRefusesReservedSuffixPrimary(t *testing.T) {
+ s := release.NewDirStore(t.TempDir())
+ reg := aliasRegWith(t, registry.Route{
+ Host: "bot.eleven.bdm.sidecar.internal", App: "eleven", Account: "bdm", Kind: "sidecar", Status: "ready",
+ })
+ putClaim(t, s, Claim{
+ Host: "tickets.example.com", Account: "bdm", App: "eleven", Route: "bot.eleven.bdm.sidecar.internal",
+ })
+ if err := ReconcileAliases(context.Background(), s, reg, aliasNow()); err != nil {
+ t.Fatal(err)
+ }
+ aliases, err := reg.ListAliases()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(aliases) != 0 {
+ t.Fatalf("got %d aliases, want 0 (a reserved-suffix primary must never be aliased): %+v", len(aliases), aliases)
+ }
+}
+
+// TestReconcileAliasesRefusesReservedSuffixAliasName is the end the brief's
+// wording alone would miss: the PRIMARY is an ordinary route, but the
+// ALIAS's own name is under the reserved suffix. Nothing about tenancy or
+// route existence would refuse this — only a check on the alias name
+// itself does, and this is the exact row the Task 3 review named: inserted
+// into edge's routing map, it would serve the primary's upstream under a
+// sidecar hostname and make Allowed() true for it.
+func TestReconcileAliasesRefusesReservedSuffixAliasName(t *testing.T) {
+ s := release.NewDirStore(t.TempDir())
+ reg := aliasRegWith(t, registry.Route{
+ Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
+ })
+ putClaim(t, s, Claim{
+ Host: "bot.eleven.bdm.sidecar.internal", Account: "bab", App: "kass", Route: "canary/live",
+ })
+ if err := ReconcileAliases(context.Background(), s, reg, aliasNow()); err != nil {
+ t.Fatal(err)
+ }
+ aliases, err := reg.ListAliases()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(aliases) != 0 {
+ t.Fatalf("got %d aliases, want 0 (an alias name under the reserved suffix must never be minted): %+v", len(aliases), aliases)
+ }
+}
+
+// TestReconcileAliasesRefusesReservedSuffixCatchall: the same alias-name-end
+// danger, through the OTHER route_aliases writer (the catch-all "*.<host>"
+// derivation off a Wildcard+Catchall claim) — a second call site, so it
+// needs its own guard, not just the plain-alias one above.
+func TestReconcileAliasesRefusesReservedSuffixCatchall(t *testing.T) {
+ ctx := context.Background()
+ s := release.NewDirStore(t.TempDir())
+ reg := aliasRegWith(t)
+ addRoute(t, reg, "app.bab.oncarlos.com", "bab", "app")
+ putClaim(t, s, Claim{
+ Host: "sub.eleven.bdm.sidecar.internal", Account: "bab", App: "app",
+ Route: "app.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z",
+ Wildcard: true, Catchall: true,
+ })
+ if err := ReconcileAliases(ctx, s, reg, aliasNow()); err != nil {
+ t.Fatal(err)
+ }
+ aliases, err := reg.ListAliases()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, a := range aliases {
+ if strings.HasPrefix(a.Alias, "*.") {
+ t.Fatalf("a catch-all alias under the reserved suffix must never be minted: %+v", a)
+ }
+ }
+}