package main import ( "context" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "math/big" "net" "os" "path/filepath" "strings" "time" "github.com/novox/mesh-controller/internal/broker" ) // The bus's own certificate, made by the mesh rather than borrowed from an image. // // **The foundation asked a third-party image for a tool it never said must be there** (novox/hq // 04-ISSUES/146). The bootstrap made this certificate by running `openssl` inside the broker's // image, which worked while the broker was one that happened to carry it and stopped the day the // bus changed: the new one has a shell and no openssl, so the step exited 127 and no mesh could be // raised. Substituting another image the bundle names does not help — none of them carry it // either. // // So the program that needs a certificate makes one. It is the mesh's own binary, already on the // machine at this point in the bootstrap (the schema step ran it), and it needs nothing from the // image it writes into but a mounted directory. // // **Self-signed, and that is the design** — a host pins this server's exact certificate and // authenticates with a password (novox/hq ADR 0004). There is no authority above it to ask, and at // this moment in a bootstrap there is no mesh to ask one of. // // Idempotent, because the step is applied again on every reconcile and a second certificate would // be one the hosts that pinned the first no longer believe. // busCertificateNames is what the bus is reached by: the container name on a mesh network, and the // loopback address the machine's own foundation dials. var busCertificateNames = []string{"mesh-broker"} const busCertificateLife = 10 * 365 * 24 * time.Hour // busCertificate makes the bus's certificate in a directory, or says whether one is there. // // broker certificate --into /tls make it if it is not there // broker certificate --check --into /tls exit non-zero unless a usable pair is func busCertificate(args []string) error { into, check := "", false for i := 0; i < len(args); i++ { switch args[i] { case "--check": check = true case "--into": if i+1 >= len(args) { return errors.New("--into needs a directory") } into = args[i+1] i++ default: return fmt.Errorf("broker certificate [--check] --into : %q", args[i]) } } if into == "" { return errors.New("broker certificate [--check] --into ") } crt, key := filepath.Join(into, "tls.crt"), filepath.Join(into, "tls.key") if usable, err := busCertificateUsable(crt, key); err != nil { return err } else if usable { fmt.Printf("the bus already has a certificate at %s, and it was left alone\n", crt) return nil } if check { // Said as a failure, because that is what the caller asked: a bootstrap's verify runs // this and a false answer is what makes the step run. return fmt.Errorf("no usable certificate and key at %s", into) } return writeBusCertificate(crt, key) } // busCertificateUsable says whether a certificate and its key are both there and parse. // // Both, and parsed rather than stat'ed: a half-written pair is the state a bootstrap interrupted // between the two files leaves behind, and a step that treated it as done would hand the server a // certificate with no key and report success. func busCertificateUsable(crt, key string) (bool, error) { certPEM, err := os.ReadFile(crt) if errors.Is(err, os.ErrNotExist) { return false, nil } if err != nil { return false, err } keyPEM, err := os.ReadFile(key) if errors.Is(err, os.ErrNotExist) { return false, nil } if err != nil { return false, err } if _, err := tlsPairParses(certPEM, keyPEM); err != nil { return false, nil } return true, nil } func tlsPairParses(certPEM, keyPEM []byte) (*x509.Certificate, error) { block, _ := pem.Decode(certPEM) if block == nil || block.Type != "CERTIFICATE" { return nil, errors.New("not a certificate") } certificate, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } keyBlock, _ := pem.Decode(keyPEM) if keyBlock == nil { return nil, errors.New("not a key") } if _, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes); err != nil { if _, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes); err != nil { return nil, err } } return certificate, nil } func writeBusCertificate(crt, key string) error { private, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err } serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return err } template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{CommonName: busCertificateNames[0]}, DNSNames: busCertificateNames, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(busCertificateLife), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } der, err := x509.CreateCertificate(rand.Reader, template, template, &private.PublicKey, private) if err != nil { return err } pkcs8, err := x509.MarshalPKCS8PrivateKey(private) if err != nil { return err } // **The key first, and only then the certificate**, so the pair a reader finds is never a // certificate whose key has not been written yet — the one order in which an interruption // leaves something that looks finished (novox/hq 04-ISSUES/014, a key present and unusable). if err := os.WriteFile(key, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}), 0o600); err != nil { return err } if err := os.WriteFile(crt, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644); err != nil { return err } fmt.Printf("made the bus a certificate for %v, valid until %s\n %s\n %s\n", busCertificateNames, template.NotAfter.Format(time.RFC3339), crt, key) return nil } // busAccounts writes the mesh's composed user list to a file. // // **For genesis, where no declaration can deliver it** (novox/hq 04-ISSUES/146). Everywhere else // the list reaches the machine running the bus as a resource of the module that holds it — which // requires that machine to be an enrolled node, and at genesis it is not: the first node cannot // enrol because the account it would enrol with cannot be composed onto a bus it has no declaration // for. The installer breaks that circle by placing the file itself, once, and the module takes the // file over from its first push. // // The same composition, not a second one: this asks the store for the same records and renders them // with the same composer the declaration uses. A genesis that hand-wrote an account would be a // second statement of who may say what, able to disagree with the first. // // **It writes to standard output unless told a file**, and that is the point: the control plane // composes and says what it composed, and whoever is raising the machine puts it where that // machine's bus reads it. A control plane that wrote into the bus's own directory would have to // know where that is and how to make the server re-read it — which is the module's knowledge, and // the module is what takes this over on the first push. // // broker accounts > /var/lib/mesh-bus-conf/accounts.conf func busAccounts(ctx context.Context, args []string) error { into := "" for i := 0; i < len(args); i++ { switch args[i] { case "--into": if i+1 >= len(args) { return errors.New("--into needs a file") } into = args[i+1] i++ default: return fmt.Errorf("broker accounts --into : %q", args[i]) } } open, err := openStores(ctx) if err != nil { return err } defer open.Close() records, err := open.inventory.BusRecords(ctx) if err != nil { return err } users, err := broker.Users(records) if err != nil { return err } kept, err := open.inventory.BusUsers(ctx) if err != nil { return err } hashes := make(map[string]string, len(kept)) for name, u := range kept { hashes[name] = u.PasswordHash } filled, missing := broker.WithPasswords(users, hashes) if len(missing) > 0 { // To standard error, always: the composed file may be going to standard output, and a // remark in the middle of it is a configuration the server refuses to parse. fmt.Fprintf(os.Stderr, "leaving out %d user(s) the mesh has minted no credential for: %s\n", len(missing), strings.Join(missing, ", ")) } if len(filled) == 0 { return errors.New("not one user has a credential, so this list would refuse every " + "connection in the mesh") } accounts, err := broker.ComposeAccounts(filled) if err != nil { return err } if into == "" { fmt.Print(accounts) return nil } if err := os.WriteFile(into, []byte(accounts), 0o600); err != nil { return err } fmt.Printf("wrote %d user(s) to %s\n", len(filled), into) return nil }