From 0e9035d4792b850a866a8aefe74b9850199192c8 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 17 Sep 2026 23:05:23 +0200 Subject: [PATCH] The network carries the registry trust (ADR 0082, issues 042/048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Being on the private network is what grants a machine the right to pull from the mesh's artifact store, so the module that puts a machine on the network writes the runtime's trust — a merged /etc/docker/daemon.json naming the store's internal name under insecure-registries, and a docker.service restart when that fact first lands. The registry speaks plain HTTP because every path to it is already inside the overlay's encryption; the provider is found, not configured — whichever module serves artifact-store, on whichever machine holds it — and with no store on the network nothing is written, which is genesis. https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx --- cmd/mesh-controller/network.go | 45 ++++++++++++++++++++++++ internal/overlay/generator.go | 33 +++++++++++++++++- internal/overlay/generator_test.go | 56 ++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 internal/overlay/generator_test.go diff --git a/cmd/mesh-controller/network.go b/cmd/mesh-controller/network.go index 7c561b8..e883298 100644 --- a/cmd/mesh-controller/network.go +++ b/cmd/mesh-controller/network.go @@ -152,6 +152,15 @@ func network(ctx context.Context, inv *inventory.Inventory, on map[string]bool, return overlay.Empty(), nil } g, err := overlay.From(nodes, overlayCIDR(), "") + if g != nil { + // The artifact store, as this network reaches it. Found rather than configured: the + // provider is whichever module offers it, on whichever machine holds that module — and if + // nothing does yet (genesis raises the registry before the catalogue knows it), there is + // no trust to write and nothing is written (novox/hq ADR 0082). + if at, port, found := artifactStoreOnNetwork(ctx, inv, on); found { + g.TrustRegistry(overlay.InternalName(at) + ":" + port) + } + } if err != nil && len(refused) > 0 { // The network is missing something, and some machines could not be resolved at all. Those // are almost always the same fact: a node that does not resolve contributes nothing, so @@ -383,3 +392,39 @@ func namesInTheMesh(ctx context.Context, inv *inventory.Inventory) (map[string]s } return out, nil } + +// artifactStoreOnNetwork is the machine and port the mesh's artifact store answers on, when a +// module providing it is assigned to a machine that is on the private network. +func artifactStoreOnNetwork(ctx context.Context, inv *inventory.Inventory, + on map[string]bool) (node, port string, found bool) { + + shelf, err := inv.Catalogue(ctx) + if err != nil || shelf == nil { + return "", "", false + } + providers := map[string]string{} // module -> served port + for name, m := range shelf { + served, offers := m.Serves[catalogue.ArtifactStoreProvision] + if !offers { + continue + } + if p, ok := served["port"]; ok { + providers[name] = fmt.Sprintf("%v", p) + } + } + if len(providers) == 0 { + return "", "", false + } + for machine := range on { + assigned, err := inv.Assigned(ctx, machine) + if err != nil { + continue + } + for _, a := range assigned { + if p, ok := providers[a]; ok { + return machine, p, true + } + } + } + return "", "", false +} diff --git a/internal/overlay/generator.go b/internal/overlay/generator.go index 3b10895..54995ed 100644 --- a/internal/overlay/generator.go +++ b/internal/overlay/generator.go @@ -83,8 +83,17 @@ type Generator struct { // keyPath is where each node keeps the private half it generated. Named rather than carried: // the mesh has never seen it and never will. keyPath string + // registry is the mesh's artifact store as the network reaches it (host:port), or empty when + // the mesh has none. Being on the network is what grants a machine the right to pull from it + // (novox/hq ADR 0082), so the module that puts a machine on the network is what writes the + // runtime's trust — the same reasoning that has it write /etc/hosts. + registry string } +// TrustRegistry names the artifact store this network's machines pull from in the clear — +// the overlay is the transport security (ADR 0082). +func (g *Generator) TrustRegistry(hostPort string) { g.registry = hostPort } + // From builds a generator over the machines that are part of the network. // // The nodes given are the ones assigned the module — not every node the mesh knows. A machine @@ -123,7 +132,29 @@ func (g *Generator) Resources(node string) ([]map[string]any, bool, error) { if err := json.Unmarshal(raw, &parsed); err != nil { return nil, false, err } - return parsed.Resources, true, nil + resources := parsed.Resources + if g.registry != "" { + trust, err := json.Marshal(map[string]any{"insecure-registries": []string{g.registry}}) + if err != nil { + return nil, false, err + } + resources = append(resources, + map[string]any{ + // Merged, not owned: the runtime's daemon file is the machine's, and this states + // one fact into it. The registry speaks plain HTTP because every path to it is + // already inside the overlay's encryption (ADR 0082) — this line is the runtime + // being told what the mesh already means. + "id": "registry-trust", "type": "file", "path": "/etc/docker/daemon.json", + "content": string(trust) + "\n", "mode": "0644", "merge": "json", + }, + map[string]any{ + // The runtime reloads nothing for this setting, so it is restarted when the fact + // changes — once, at joining, before the machine runs anything that would mind. + "id": "registry-trust-reload", "type": "service", "unit": "docker.service", + "state": "running", "restart-on": []string{"registry-trust"}, + }) + } + return resources, true, nil } // Nodes are the machines this generator was built over, so a caller can say who is on the network. diff --git a/internal/overlay/generator_test.go b/internal/overlay/generator_test.go new file mode 100644 index 0000000..0cc935a --- /dev/null +++ b/internal/overlay/generator_test.go @@ -0,0 +1,56 @@ +package overlay + +import ( + "strings" + "testing" +) + +func TestTheNetworkCarriesRegistryTrust(t *testing.T) { + // novox/hq ADR 0082: being on the network is what grants a machine the right to pull from the + // mesh's artifact store in the clear, so the network module writes the runtime's trust — and + // writes nothing when the mesh has no store to trust. + nodes := []Node{ + {Name: "anchor", Site: "lab", Hub: true, Endpoint: "192.0.2.10:51820", Key: "k1", Address: "10.42.0.1"}, + {Name: "node2", Site: "lab", Key: "k2", Address: "10.42.0.2"}, + } + g, err := From(nodes, "10.42.0.0/16", "") + if err != nil { + t.Fatal(err) + } + plain, _, err := g.Resources("node2") + if err != nil { + t.Fatal(err) + } + for _, r := range plain { + if r["id"] == "registry-trust" { + t.Fatal("trust was written with no artifact store to trust") + } + } + + g.TrustRegistry("anchor.internal:5000") + trusted, part, err := g.Resources("node2") + if err != nil || !part { + t.Fatalf("resources: %v part=%v", err, part) + } + var file, service map[string]any + for _, r := range trusted { + switch r["id"] { + case "registry-trust": + file = r + case "registry-trust-reload": + service = r + } + } + if file == nil || service == nil { + t.Fatalf("the trust file or its reload is missing: %v", trusted) + } + if file["path"] != "/etc/docker/daemon.json" || file["merge"] != "json" { + t.Fatalf("the trust is not a merged daemon.json: %v", file) + } + if content, _ := file["content"].(string); !strings.Contains(content, `"anchor.internal:5000"`) { + t.Fatalf("the trust does not name the store: %v", file["content"]) + } + if service["unit"] != "docker.service" { + t.Fatalf("the reload does not restart the runtime: %v", service) + } +}