// 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" } } ] }