diff --git a/modules/nats/entrypoint.sh b/modules/nats/entrypoint.sh index f117cc1..8523f7b 100644 --- a/modules/nats/entrypoint.sh +++ b/modules/nats/entrypoint.sh @@ -3,9 +3,9 @@ # configuration. # # **Why this exists inside the module** (novox/hq design 25 §5). The controller composes every -# account and permission into one configuration file, and that file changes whenever a module is -# added, reassigned, or a person's access is granted or revoked — which is often, and on the one -# server everything else depends on. The host has no way to say "reload this container": a +# account and permission into one file, and that file changes whenever a module is added, +# reassigned, or a person's access is granted or revoked — which is often, and on the one server +# everything else depends on. The host has no way to say "reload this container": a # container resource has `restart-on` and nothing else, and a container's `restart-on` means # *recreate* — every connection dropped and every in-flight JetStream ack lost, mid-flight, for a # permission change. `reload-on` is real but it is a *service* field, not a container's. @@ -19,21 +19,29 @@ # here is declared `restart-on` or `reload-on`. set -eu +# **Two files, and only one of them is the mesh's** (novox/hq design 25 §4, task 1.7). CONF is this +# module's own — ports, TLS, JetStream — declared in its manifest, because those are properties of +# the container this module raises. USERS is every account and permission, composed by the +# controller, and CONF includes it. So what is watched here is the mesh's half: the module's own +# does not change without a new declaration, and that recreates the container anyway. CONF="${MESH_NATS_CONF:-/etc/nats/nats.conf}" +USERS="${MESH_NATS_USERS:-/etc/nats/accounts.conf}" POLL="${MESH_NATS_CONF_POLL_SECONDS:-5}" -# The controller writes the configuration as part of the same declaration that creates this -# container, but the two are not ordered against each other. Waiting is correct and starting -# without one is not: nats-server would come up with its compiled-in defaults — no TLS, no -# accounts, every subject open to anyone who can reach the port — and then be reloaded into -# correctness a moment later. A bus that is briefly open to everything is not a bus that is -# briefly wrong; it is an open bus. -while [ ! -s "$CONF" ]; do - echo "[nats] waiting for the mesh to compose $CONF" - sleep 1 +# Both are written as part of the same declaration that creates this container, but none of the +# three are ordered against each other. Waiting is correct and starting without them is not: +# nats-server given a configuration whose include is missing refuses to start, and one given no +# configuration at all comes up with its compiled-in defaults — no TLS, no accounts, every subject +# open to anyone who can reach the port. A bus that is briefly open to everything is not a bus that +# is briefly wrong; it is an open bus. +for needed in "$CONF" "$USERS"; do + while [ ! -s "$needed" ]; do + echo "[nats] waiting for the mesh to write $needed" + sleep 1 + done done -digest() { sha256sum "$CONF" 2>/dev/null | cut -d' ' -f1; } +digest() { sha256sum "$USERS" 2>/dev/null | cut -d' ' -f1; } nats-server --config "$CONF" "$@" & server=$! @@ -52,7 +60,7 @@ while kill -0 "$server" 2>/dev/null; do [ -n "$now" ] || continue if [ "$now" != "$last" ]; then last=$now - echo "[nats] configuration changed; reloading in place" + echo "[nats] the mesh's user list changed; reloading in place" kill -HUP "$server" || true fi done diff --git a/modules/nats/module.json b/modules/nats/module.json index 44adaff..5a0f695 100644 --- a/modules/nats/module.json +++ b/modules/nats/module.json @@ -13,6 +13,7 @@ "scope": "mesh" } ], + "bus-users": "/var/lib/nats-module/conf/accounts.conf", "capabilities": [ "container-runtime" ], @@ -42,6 +43,13 @@ "path": "/var/lib/nats-module/conf", "mode": "0700" }, + { + "id": "server-conf", + "type": "file", + "path": "/var/lib/nats-module/conf/nats.conf", + "content": "# The nats module's own server settings. Declared by the module, because a port, a TLS path\n# and a store directory are properties of the container this module raises: they live in its\n# image and its mounts and change when it does.\n#\n# The mesh writes accounts.conf beside this one and nothing else. A controller that wrote the\n# whole file would have to be kept in step with a Dockerfile it never sees.\n\nport: 4222\nhttp: 127.0.0.1:8222\n\ntls {\n cert_file: \"/tls/tls.crt\"\n key_file: \"/tls/tls.key\"\n ca_file: \"/tls/ca.crt\"\n}\n\n# **No `verify`, deliberately, and it was `verify: true` until a probe ran this image.** That\n# setting makes the server demand a *client* certificate, and nothing in the mesh presents one: a\n# host pins this server's exact certificate and authenticates with the password the mesh minted\n# (novox/hq ADR 0004, design 25 \u00a74), and so does a module's runtime. With it on, every connection\n# in the mesh is refused at the TLS handshake, before any password is looked at \u2014 and the error is\n# \"client didn't provide a certificate\", which reads as a client fault.\n#\n# TLS is still required: a tls block is what makes it required, and verify only decides whether\n# client certificates are checked. What is given up is a second factor the mesh has no machinery\n# to issue or rotate \u2014 a certificate per module per node \u2014 and what is kept is stronger than a\n# name check in both directions: an exact pin outward, a per-user password inward.\n\njetstream {\n store_dir: \"/data\"\n}\n\n# Every user of the mesh, composed by the controller and rewritten whenever a module is\n# assigned, a node enrols or a person's access changes.\n#\n# **Relative, and in this same directory, because it has to be.** An absolute include path is\n# resolved relative to the including file's directory, not from the root: nats-server given\n# `include /etc/nats/accounts.conf` from /etc/nats-server/nats.conf looks for\n# /etc/nats-server/etc/nats/accounts.conf and refuses to start. Verified against the server.\ninclude accounts.conf\n", + "mode": "0644" + }, { "id": "server", "type": "container",