diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 82ef890..dc9fb91 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -3,7 +3,9 @@ layer: to-be status: designed code: - mesh-control internal/catalogue/filtering.go - - mesh-host internal/apply (the service that reflects it) + - mesh-control internal/identity/authority.go + - mesh-host internal/identity/serving.go + - mesh-host internal/apply (the service that reflects a rule set) updated: 2026-08-31 decisions: - 02-DECISIONS/0005-the-node-host.md @@ -315,7 +317,7 @@ something: | **flush the ruleset** when loading | that empties every table on the machine, the runtime's among them. Only the mesh's own table is replaced, and it is declared empty first so the replacement works on a machine loading one for the first time | | carry a **command to load itself** | the link may not carry an action ([ADR 0005](../../02-DECISIONS/0005-the-node-host.md)). A service is declared to reflect the file instead, so replacing it restarts what loads it — the shape that rule leaves, used here for the first time for its real purpose | -**And it is enforced, which is what separates this from `scope:`.** Proven on two real machines: +**And it is enforced, which is what separates this from `scope:`.** Checked on two real machines: two ports opened, one declared, and from the other machine the declared one answers and the undeclared one does not — then the module is removed and the port closes with nobody editing a rule. *A rule set that is written but never loaded passes every check that reads the file, which @@ -348,6 +350,34 @@ fingerprint in its token ([ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-j so nothing needs the CA before membership. It certifies internal names afterwards, and that is all it does. +### What was built + +*2026-08-31.* + +**A node generates a fourth key**, and the reason is the one the other three already give: *a key +used for two purposes is one rotation away from breaking the other.* The identity key would work +for TLS and reusing it would mean rotating a node's identity every time its certificate is +replaced. The private half never leaves the machine; the mesh is told the public half at +enrolment. + +**So there is no certificate request and nothing to seal.** The mesh signs a statement binding a +public key to a name it alone assigns, which is the whole of what a certificate authority does. +It issues rather than stores: the node's key does not change, so signing again produces an equally +valid certificate and there is nothing to keep in step. + +**A machine with no name inside the mesh is refused**, not given a certificate for nothing. A +certificate for a name nothing resolves is a certificate nothing can check. + +**And the key is stored in the format a server reads** — PKCS#8 PEM, not the host's own encoding. +That is not an implementation detail of whoever writes the file: the file exists *because +something else reads it*, so the format is the interface +([04-ISSUES/014](../../04-ISSUES/014-a-key-that-is-present-and-unusable/00-report.md)). + +*Checked by a real handshake between two machines: one serves on its internal name with the key it +generated, the other verifies against the mesh's authority and nothing else. Every cheaper check +passed while the server could not start — the key was present, the certificate was valid, and +nothing read either the way a server would.* + ## What this removes The list is worth having in one place, because it is most of the argument: diff --git a/04-ISSUES/014-a-key-that-is-present-and-unusable/00-report.md b/04-ISSUES/014-a-key-that-is-present-and-unusable/00-report.md new file mode 100644 index 0000000..e02abb0 --- /dev/null +++ b/04-ISSUES/014-a-key-that-is-present-and-unusable/00-report.md @@ -0,0 +1,48 @@ +--- +status: resolved +opened: 2026-08-31 +located-in: [mesh-host] +fixed-by: mesh-host — a node's serving key is stored in the format a server reads +amended-design: +--- + +# 014 — A node's serving key was present, correct, and unusable + +## Symptom + +A node generates the key it serves TLS with, the mesh certifies the public half, and the +certificate arrives on the machine as an ordinary file. Everything about that worked. But the host +stored the private half in its own encoding — base64 of the raw key — and **nothing that serves +TLS can read it**: not a web server's `ssl_certificate_key`, not Go's `LoadX509KeyPair`, not +`openssl s_server -key`. + +The file was there, owned by root, mode 0600, holding the right key. The certificate beside it was +valid and chained to the mesh's authority. The server would not start. + +## Why this matters + +**Every check that reads the file passes.** The key exists, the certificate exists, the mesh +recorded the public half, the machine reports the declaration applied. The failure surfaces only +when something connects — the worst place to find out, and the place the certificate work was +specifically designed to move away from. + +It is the same shape as [013](../013-a-file-arrives-after-the-service-that-needs-it/00-report.md) +and worth naming as a class: **two halves of one mechanism designed separately, each correct +about its own half.** The control plane issues PEM because that is what a certificate is. The host +stored the key in whatever was convenient, because nothing in the host reads it back — the whole +point of the file is that *something else* does, and that something else was not in view. + +**The generalisation:** where a file exists so a third party can read it, the format is not an +implementation detail of whoever writes it. It is the interface, and it needs a check that reads +it the way that third party will. + +## What was done + +PKCS#8 PEM, which is what every TLS server reads. A key in the old encoding is refused **by name** +rather than reported as corrupt — it is intact, and the remedy is to enrol again, which is a +different action from repairing a damaged file. + +*Checked by writing a key, decoding the file as PEM, parsing it as PKCS#8, and asserting it is the +same key — and, in the lab, by a real handshake from a second machine that verifies against the +mesh's authority and nothing else. A key that parses is not a key a server can use, which is why +the lab check connects.*