The mesh certifies names inside it
08-connectivity keeps two authorities apart on purpose: a public one for names the outside world reaches, and the mesh's own for names only the mesh knows. Nothing implemented the second, so anything between machines was plaintext or trust-on-first-use — which the design refuses everywhere else. A node now generates a fourth key at enrolment and reports the public half. A fourth, because a key used for two purposes is one rotation away from breaking the other: the identity key signs messages to the mesh and would do for TLS, and reusing it would mean rotating a node's identity every time its certificate is replaced. **Nothing secret travels and nothing is sealed.** A certificate authority says "this name belongs to the holder of this key", so the mesh signs a public half it cannot use, and the certificate it issues is public. A module asks for one and is given the certificate and, if it wants, the mesh's own — the private key is a path to a file the machine already has, the same arrangement the private network's key uses. Asserted by verifying rather than inspecting, because a certificate that parses and does not chain fails at the moment something connects: - what the mesh issues verifies against the mesh, for the name asked for - the name is in the subject alternative names, since a certificate carrying it only in the common name is refused by every modern client - it certifies the key the node generated and no other - another mesh's certificate does not verify, which is the whole point of two authorities being separate - the authority cannot sign another authority — one that could is one that can be delegated without anybody deciding to - two control planes starting together agree on one authority, or a mesh has certificates half its machines refuse Certificates last ten years, which is a choice: a short life needs something to renew it, and a renewal that fails silently is a mesh that stops trusting itself on a date nobody wrote down. What makes one replaceable is that the mesh reissues on demand, not that it expires.
This commit is contained in:
@@ -44,6 +44,11 @@ type Grant struct {
|
||||
|
||||
// Rendering is everything needed to turn a resolution into the declaration a node is sent.
|
||||
type Rendering struct {
|
||||
// Certificate is what the mesh issued for this machine's internal name, and the mesh's own
|
||||
// certificate. Both public — the key they belong to never left the machine.
|
||||
Certificate string
|
||||
Authority string
|
||||
|
||||
// Needed is each module's own secrets, sealed to this node, keyed by module and then by the
|
||||
// name the module gave it.
|
||||
Needed map[string]map[string]string
|
||||
@@ -78,6 +83,26 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
||||
var out []map[string]any
|
||||
for _, m := range r.Modules {
|
||||
resources := m.Resources
|
||||
if c := m.Certificate; c != nil {
|
||||
if with.Certificate == "" {
|
||||
// Asked for and not issued. Refused rather than skipped: a module that serves TLS
|
||||
// with no certificate does not start, and the reason is somewhere else entirely.
|
||||
return nil, fmt.Errorf(
|
||||
"%s wants a certificate for this machine and none was issued", m.Module)
|
||||
}
|
||||
resources = append(append([]map[string]any{}, resources...), map[string]any{
|
||||
"id": CertificateID(), "type": "file", "path": c.Into,
|
||||
// Public. It travels in the open like any other file, because it is a statement
|
||||
// about a key rather than the key.
|
||||
"content": with.Certificate, "mode": "0644",
|
||||
})
|
||||
if c.Authority != "" {
|
||||
resources = append(resources, map[string]any{
|
||||
"id": AuthorityID(), "type": "file", "path": c.Authority,
|
||||
"content": with.Authority, "mode": "0644",
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, name := range sortedKeys(m.Needs) {
|
||||
sealed := with.Needed[m.Module][name]
|
||||
if sealed == "" {
|
||||
|
||||
@@ -212,6 +212,18 @@ type Manifest struct {
|
||||
// module, in a file anybody can read, for ever.
|
||||
Needs map[string]string `json:"needs,omitempty"`
|
||||
|
||||
// Certificate is where this module wants a certificate for its machine's name inside the
|
||||
// mesh, and where the key that goes with it can be found.
|
||||
//
|
||||
// **The key is named, not delivered.** The node generated it at enrolment and keeps it; the
|
||||
// mesh only ever signs the public half. So what arrives is a certificate, which is public,
|
||||
// and a path to a file the machine already has.
|
||||
//
|
||||
// Two authorities are kept apart on purpose (novox/hq 08-connectivity): this is the mesh's,
|
||||
// for names only the mesh knows. A name the outside world reaches is a different authority
|
||||
// and a different problem.
|
||||
Certificate *Certificate `json:"certificate,omitempty"`
|
||||
|
||||
// Grants is a directory this module wants the credentials of its consumers written into, per
|
||||
// provision it offers — one file per consumer, named for it, holding the value alone.
|
||||
//
|
||||
@@ -262,6 +274,19 @@ const (
|
||||
ArtifactUpstream = "upstream"
|
||||
)
|
||||
|
||||
// Certificate says where a module wants what the mesh issued for its machine.
|
||||
type Certificate struct {
|
||||
// Into is where the certificate is written.
|
||||
Into string `json:"into"`
|
||||
// Authority is where the mesh's own certificate is written, so something connecting to this
|
||||
// machine can be told what to believe. Optional: a module that only serves does not need it.
|
||||
Authority string `json:"authority,omitempty"`
|
||||
}
|
||||
|
||||
// CertificateID and AuthorityID are the resource identities of what the mesh issued.
|
||||
func CertificateID() string { return "certificate" }
|
||||
func AuthorityID() string { return "certificate-authority" }
|
||||
|
||||
// NeedID is the resource identity of the file a module's own secret lands in.
|
||||
func NeedID(name string) string { return "needs-" + name }
|
||||
|
||||
@@ -404,6 +429,17 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
"%s binds %q and does not require it", m.Module, to))
|
||||
}
|
||||
}
|
||||
if c := m.Certificate; c != nil {
|
||||
if !strings.HasPrefix(c.Into, "/") {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s wants its certificate at %q, which is not an absolute path", m.Module, c.Into))
|
||||
}
|
||||
if c.Authority != "" && !strings.HasPrefix(c.Authority, "/") {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s wants the authority at %q, which is not an absolute path",
|
||||
m.Module, c.Authority))
|
||||
}
|
||||
}
|
||||
for name, where := range m.Needs {
|
||||
if !strings.HasPrefix(where, "/") {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
|
||||
Reference in New Issue
Block a user