A file the mesh can deliver and cannot read
Everything else in a declaration is visible to whatever carried it. The message is signed so it cannot be forged, and signing does not make it unreadable — a password in `content` is a password the broker sees, which is the transitive trust this design refuses everywhere else. So a node generates a third key at enrolment and reports the public half, exactly as it does for its identity and its overlay key. A file may arrive `sealed` instead of `content`; the host opens it with that key and writes the result. The control plane can then store a credential it cannot use, and the broker relays a blob it cannot read. A third key rather than reusing one of the two. The identity key signs and is Ed25519; the overlay key is WireGuard's and is tied to being on the private network, which a machine may not be. A key used for two purposes is one rotation away from breaking the other. Details that are not incidental: - sealed and content together is refused, so "was this the secret or the placeholder" is answerable by looking - a sealed file defaults to 0600 rather than 0644, because the consequence differs; an explicit mode still wins - a node with no sealing key refuses the file rather than skipping it. A machine that quietly omits the one resource carrying a credential looks configured and cannot connect - what is recorded is a digest of what was written, so drift on a credential is still detected without the node keeping the value, and the report that goes back over the broker carries neither The key is made at enrolment rather than on first use. One made later is one the mesh was never told about, so nothing could ever be sealed to it, and the node would look fine and receive nothing. This is why sealing was borrowed from another mesh's mistakes rather than its design: there, credentials sit encrypted in the control plane's database — which guards the database file and nothing else, since the same value is also in each node's environment file in plain text and inside every connection string composed from it. Its own tooling has to search by value rather than by name to find the copies, and says the ones inside composed URLs are usually the only copies in use.
This commit is contained in:
@@ -83,8 +83,25 @@ type File struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
|
||||
// Sealed is content encrypted to this node's sealing key, for a file the mesh must deliver
|
||||
// without being able to read.
|
||||
//
|
||||
// The one thing here the host cannot simply write. Everything else in a declaration is
|
||||
// visible to whatever carried it — the broker relays the message, and the message is signed
|
||||
// so it cannot be forged, but signing does not make it unreadable. A password travelling in
|
||||
// `content` would be a password the broker sees, which is the transitive trust the design
|
||||
// refuses everywhere else (novox/hq ADR 0004).
|
||||
//
|
||||
// Exclusive with Content: a file is one or the other, so that "was this secret" is answerable
|
||||
// by looking rather than by knowing which field won.
|
||||
Sealed string `json:"sealed,omitempty"`
|
||||
}
|
||||
|
||||
// Secret reports whether this file arrived sealed, which is what decides both that it must be
|
||||
// opened before writing and that its contents must never appear in a report.
|
||||
func (f *File) Secret() bool { return f.Sealed != "" }
|
||||
|
||||
func (f *File) Identity() string { return f.ID }
|
||||
func (f *File) Kind() Type { return TypeFile }
|
||||
func (f *File) Target() string { return f.Path }
|
||||
@@ -94,6 +111,11 @@ func (f *File) validate(where string, _ bool) []string {
|
||||
if f.Path == "" {
|
||||
problems = append(problems, where+": a file needs a path")
|
||||
}
|
||||
if f.Content != "" && f.Sealed != "" {
|
||||
problems = append(problems, where+
|
||||
": a file has content or is sealed, not both — otherwise nobody can tell by looking "+
|
||||
"whether what landed on the machine was the secret or the placeholder")
|
||||
}
|
||||
return append(problems, checkMode(where, f.Mode)...)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user