From 6a3435629ef8277f54481dfc132cdb816854f5a5 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 27 Sep 2026 16:39:32 +0200 Subject: [PATCH] A foundation template that raises the mesh on the bus being built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same twelve steps, with the difference that matters: the mesh composes its own user list and at genesis there is none, so this carries the first one — the controller's account at a bootstrap password, rotated with the store's and replaced by the controller's own composition from its first start. The server's settings and the user list are separate files in one directory. Separate because the settings belong to whoever raises the server and the users belong to the mesh; in one directory of necessity, because an include path resolves relative to the including file's own directory, so an absolute one sends the server looking underneath that directory and it refuses to start. No `verify` in the TLS block. That makes the server demand a client certificate and nothing in the mesh presents one — a host pins this server's exact certificate and authenticates with a password. The controller's permissions here are checked against what the controller derives, by a test in its own repository reading this file. They are two statements of one fact, and a template that granted less than the controller needs would produce a mesh that comes up and is refused on its first act. --- examples/foundation-first-node-nats.lock | 191 +++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 examples/foundation-first-node-nats.lock diff --git a/examples/foundation-first-node-nats.lock b/examples/foundation-first-node-nats.lock new file mode 100644 index 0000000..50fefd1 --- /dev/null +++ b/examples/foundation-first-node-nats.lock @@ -0,0 +1,191 @@ +// foundation-first-node-nats.lock — what a machine must be before a mesh exists, on the bus being +// built (novox/hq ADR 0106, design 25). +// +// The same twelve steps as foundation-first-node.lock, with one difference that matters: **the mesh +// writes its own user list, and at genesis there is no mesh yet to write it.** So this carries the +// first one — the controller's own account, at a well-known bootstrap password, exactly as the store +// is reached at `postgres:bootstrap` and the old bus at `guest:guest`. It is rotated with those, and +// from the controller's first composition onward the file is the controller's to write. +// +// The accounts file is its own file beside the server's configuration, because the server's own +// settings belong to whoever raises it and the users belong to the mesh (design 25 §4). Both live in +// one directory, of necessity: an include path is resolved relative to the including file's own +// directory, so a server given an absolute one looks for it underneath that directory and refuses to +// start. +// +// No `verify` on the TLS block, deliberately — that setting makes the server demand a *client* +// certificate, and nothing in the mesh presents one: a host pins this server's exact certificate and +// authenticates with a password (ADR 0004, design 25 §4). + +{ + "declaration": 1, + "resources": [ + { + "id": "container-runtime", + "type": "package", + "package": "docker" + }, + { + "id": "container-runtime-running", + "type": "service", + "unit": "docker.service", + "state": "running", + "boot": "enabled" + }, + // **A filter before anything listens** (novox/hq issue 054, ADR 0088). The store and the + // broker are adopted as modules later and so bind to every interface from the moment they + // start; the packet filter that governs who may reach them is a module too, installed a + // dozen steps later. Between the two, a control-node facing the network had its store and + // its bus open to anyone who could reach the machine. So the foundation carries a filter of + // its own — the same table the filter module will replace wholesale once it can derive one: + // drop by default, keep loopback, replies, ssh and the mesh's own ports (the bus a node + // enrols over, the registry a node pulls from), and let the container runtime's own + // networks through the forward chain so containers keep working. A published container port + // is forwarded, never input (issue 047), which is why the forward chain is where the store's + // and broker's ports are refused from outside — and a container on this machine dialling a + // port this machine publishes reaches it through the runtime's proxy, which IS input, which + // is why the bus and the registry are opened in both chains, exactly as the derived ruleset + // does. + { + "id": "base-filter-package", + "type": "package", + "package": "nftables" + }, + { + "id": "base-filter", + "type": "file", + "path": "/etc/nftables.conf", + "mode": "0644", + "content": "#!/usr/sbin/nft -f\n# the foundation's own filter, until the mesh derives one (novox/hq issue 054)\ntable inet mesh {}\ndelete table inet mesh\n\ntable inet mesh {\n\tchain input {\n\t\ttype filter hook input priority filter; policy drop;\n\t\tct state established,related accept\n\t\tct state invalid drop\n\t\tiif lo accept\n\t\ticmp type echo-request accept\n\t\ticmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } accept\n\t\t# ssh, from anywhere — never closed\n\t\ttcp dport 22 accept\n\t\t# the mesh's own, from anywhere: the bus a node enrols over and a container on this machine reaches through the proxy, the registry a node pulls from\n\t\ttcp dport 5671 accept\n\t\ttcp dport 5000 accept\n\t}\n\tchain output {\n\t\ttype filter hook output priority filter; policy accept;\n\t}\n\tchain forward {\n\t\ttype filter hook forward priority filter; policy drop;\n\t\tct state established,related accept\n\t\tct state invalid drop\n\t\t# the container runtime's bridge networks, and the networks its compose files are given\n\t\tip saddr 172.16.0.0/12 accept\n\t\tip saddr 192.168.128.0/17 accept\n\t\t# the mesh's own: the bus a node enrols over, the registry a node pulls from\n\t\tct original proto-dst 5671 accept\n\t\tct original proto-dst 5000 accept\n\t}\n}\n" + }, + { + "id": "base-filter-loaded", + "type": "service", + "unit": "nftables.service", + "state": "running", + "boot": "enabled", + "restart-on": ["base-filter"] + }, + { + "id": "store", + "type": "container", + "name": "mesh-store", + "image": "192.0.2.250:5000/postgres@sha256:7abf537131b66ed5af448d90653abf1679b0c7e9a1f07efdd4c3108a401b259a", + "env": { + "POSTGRES_PASSWORD": "bootstrap", + "PGDATA": "/var/lib/postgresql/data/pgdata" + }, + "ports": ["5432:5432"], + "volumes": ["mesh-store-data:/var/lib/postgresql/data"] + }, + // Over TCP, not the socket. While the store initialises it runs a temporary server on the + // socket ONLY, then stops it and starts the real one — so a socket check passes, the action + // exits happy, and the verify a moment later lands in the gap and fails. The action and its + // verify must ask the same question, or the action can succeed into a state verify rejects. + { + "id": "store-ready", + "type": "action", + "in": "mesh-store", + "command": ["sh", "-c", "for i in $(seq 1 180); do pg_isready -h 127.0.0.1 -U postgres >/dev/null 2>&1 && exit 0; sleep 1; done; echo 'the store did not answer within 180s; its own last words follow'; pg_isready -h 127.0.0.1 -U postgres; tail -n 20 /var/lib/postgresql/data/log/*.log 2>/dev/null; exit 1"], +"verify": ["pg_isready", "-h", "127.0.0.1", "-U", "postgres"] + }, + { + "id": "inventory-database", + "type": "action", + "in": "mesh-store", + "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE inventory'"], + "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw inventory"] + }, + { + "id": "identity-database", + "type": "action", + "in": "mesh-store", + "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE identity'"], + "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw identity"] + }, + // Each context owns its own database (novox/hq ADR 0008). A third one is a third database, + // created the same way and named the same way — which is the whole of adding a context to the + // bootstrap, and is why the count is not something the foundation has an opinion about. + { + "id": "licences-database", + "type": "action", + "in": "mesh-store", + "command": ["sh", "-c", "psql -U postgres -c 'CREATE DATABASE licences'"], + "verify": ["sh", "-c", "psql -U postgres -lqt | cut -d'|' -f1 | grep -qw licences"] + }, + { + "id": "context-schemas", + "type": "action", + "command": ["docker", "run", "--rm", "--network", "container:mesh-store", + "-e", "MESH_STORE_INVENTORY=postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable", + "-e", "MESH_STORE_IDENTITY=postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable", + "-e", "MESH_STORE_LICENCES=postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable", + "192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace", + "migrate"], + "verify": ["sh", "-c", "docker exec mesh-store psql -U postgres -d inventory -tAc \"select to_regclass('public.node')\" | grep -qx node && docker exec mesh-store psql -U postgres -d identity -tAc \"select to_regclass('public.signing_key')\" | grep -qx signing_key && docker exec mesh-store psql -U postgres -d licences -tAc \"select to_regclass('public.licence')\" | grep -qx licence"] + }, + { + "id": "bus-certificate", + "type": "action", + "command": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls", + "192.0.2.250:5000/nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927", + "-c", "test -f /tls/tls.crt || (openssl req -x509 -newkey rsa:2048 -nodes -keyout /tls/tls.key -out /tls/tls.crt -days 3650 -subj '/CN=mesh-broker' -addext 'subjectAltName=DNS:mesh-broker,IP:127.0.0.1' >/dev/null 2>&1 && chmod 644 /tls/tls.crt && chmod 600 /tls/tls.key)"], + "verify": ["docker", "run", "--rm", "--entrypoint", "sh", "-v", "mesh-broker-tls:/tls", + "192.0.2.250:5000/nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927", + "-c", "test -s /tls/tls.crt && openssl x509 -in /tls/tls.crt -noout"] + }, + { + "id": "bus-conf-dir", + "type": "directory", + "path": "/var/lib/mesh-bus-conf", + "mode": "0700" + }, + { + "id": "bus-conf", + "type": "file", + "path": "/var/lib/mesh-bus-conf/nats.conf", + "mode": "0644", + "content": "port: 4222\nhttp: 127.0.0.1:8222\n\ntls {\n cert_file: \"/tls/tls.crt\"\n key_file: \"/tls/tls.key\"\n}\n\njetstream {\n store_dir: \"/data\"\n}\n\ninclude accounts.conf\n" + }, + { + "id": "bus-accounts", + "type": "file", + "path": "/var/lib/mesh-bus-conf/accounts.conf", + "mode": "0600", + "content": "// The first user list, carried by the installer because at genesis there is no mesh to\n// compose one. A bootstrap credential, rotated with the store's and replaced by the\n// controller's own composition from its first start onward.\naccounts {\n MESH {\n users = [\n { user: \"controller\", password: \"$2a$10$AHqJgOifIVbU41KmATiMhuXFs8xa7Wl2HuN4UVBCXdN2jIQzjqApy\", permissions: {\n publish: { allow: [\"$JS.API.>\", \"$JS.ACK.CONTROL.controller.>\", \"$JS.ACK.EVENTS.controller.>\", \"_INBOX.enrol.>\", \"mesh.control.>\", \"mesh.node.>\", \"mesh.seat.mesh-build-machine.accept.>\"] }\n subscribe: { allow: [\"$JS.API.>\", \"_INBOX.controller.>\", \"mesh.control.>\", \"mesh.mod.mesh-catalog.event.catching-up\", \"mesh.mod.mesh-catalog.event.upgraded\", \"mesh.seat.mesh-build-machine.event.built\"] }\n allow_responses: { max: 1, ttl: \"1m\" }\n } }\n ]\n }\n}\n" + }, + { + "id": "broker", + "type": "container", + "name": "mesh-broker", + "image": "192.0.2.250:5000/nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927", + "ports": ["5671:4222", "127.0.0.1:8222:8222"], + "volumes": ["mesh-broker-data:/data", "mesh-broker-tls:/tls:ro", "/var/lib/mesh-bus-conf:/etc/nats:ro"], + "args": ["-c", "/etc/nats/nats.conf", "-js"] + }, + { + "id": "broker-ready", + "type": "action", + "command": ["sh", "-c", "for i in $(seq 1 60); do docker run --rm --network host --entrypoint sh 192.0.2.250:5000/nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927 -c 'nc -z 127.0.0.1 5671' >/dev/null 2>&1 && exit 0; sleep 1; done; exit 1"], + "verify": ["sh", "-c", "docker run --rm --network host --entrypoint sh 192.0.2.250:5000/nats@sha256:b83efabe3e7def1e0a4a31ec6e078999bb17c80363f881df35edc70fcb6bb927 -c 'nc -z 127.0.0.1 5671'"] + }, + { + "id": "control-plane", + "type": "container", + "name": "mesh-controller", + "image": "192.0.2.250:5000/mesh-controller@sha256:c67db38439ff0aee242b467486765467bb95801f52175fc5727cc4e437338ace", + "network": "host", + "args": ["serve"], + "volumes": ["mesh-broker-tls:/broker-tls:ro"], + "env": { + "MESH_STORE_INVENTORY": "postgres://postgres:bootstrap@127.0.0.1:5432/inventory?sslmode=disable", + "MESH_STORE_IDENTITY": "postgres://postgres:bootstrap@127.0.0.1:5432/identity?sslmode=disable", + "MESH_STORE_LICENCES": "postgres://postgres:bootstrap@127.0.0.1:5432/licences?sslmode=disable", + "MESH_BUS_NATS": "nats://controller:bootstrap@127.0.0.1:5671", + "MESH_BROKER_ADDRESS": "192.0.2.10:5671", + "MESH_BROKER_CERTIFICATE": "/broker-tls/tls.crt" + } + } + + ] +}