A key that is present and unusable, and what the certificate work became

Issue 014: the node's serving key was stored in the host's own encoding, so
every check that reads the file passed and no server could start. Same shape as
013 — two halves of one mechanism designed separately, each correct about its
own half. Where a file exists so a third party can read it, the format is the
interface.
This commit is contained in:
2026-08-31 00:42:13 +02:00
parent 778efaba8b
commit a6872ac099
2 changed files with 80 additions and 2 deletions
+32 -2
View File
@@ -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:
@@ -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.*