package secrets import ( "crypto/rand" "encoding/base64" "fmt" "golang.org/x/crypto/nacl/box" ) // Secrets the mesh delivers and cannot read. // // **What this is not.** The obvious arrangement is a credentials column, encrypted at rest. It // has been built, in another mesh, and that mesh's own tooling records what it bought: a query // against the encrypted column returns zero rows and proves nothing, so auditing moved to the // decrypted copies on the nodes; and the tool for finding a secret has to search **by value** // rather than by name, because the same password sits in the provisions table, in the environment // table, in each node's environment file in plain text, and inside every connection string // composed from it — copies its own documentation calls "often the only copies actually in use". // // Two faults there, and encryption at rest addresses neither. **The control plane can read what // it stores**, so a copy of its database is a copy of every credential in the mesh. And **one // secret has many homes with nothing tracking them.** // // So here the value is sealed to the node that will use it before it is stored, with a key that // node generated and whose private half the mesh has never seen. What gets written is unusable by // whoever holds it, the mesh included. And nothing is composed centrally — a connection string is // assembled on the machine that needs one, so the mesh never mints a second copy in a shape // nothing tracks. // Sealed is one value, closed to both ends of a provision. // // Two blobs of the same secret rather than one shared key: a key both ends hold is a key the mesh // would have to distribute, which is this problem again one level down. type Sealed struct { ForConsumer string ForProvider string // Which key each was sealed to, kept so a node that regenerated its key can be told what it // can no longer open rather than discovering it as a service that will not start. ConsumerKey string ProviderKey string } // Make generates a secret and seals it to both ends, keeping no readable copy. // // The plaintext exists for the length of this call. Rotation is therefore generating a new one // rather than reading the old one back — the only version of rotation that is honest about what // the mesh knows. func Make(consumerKey, providerKey string) (Sealed, error) { if consumerKey == "" || providerKey == "" { // Sealing to an empty key would produce a blob nobody can open, stored as though it were // a working credential. The caller knows which node is which and says so. return Sealed{}, fmt.Errorf("both ends need a sealing key before a secret can be made") } value := make([]byte, 32) if _, err := rand.Read(value); err != nil { return Sealed{}, err } // Base64 without padding, because it lands in a configuration file something else parses and // a password containing a newline or a quote is a support call. password := base64.RawURLEncoding.EncodeToString(value) forConsumer, err := Seal(consumerKey, []byte(password)) if err != nil { return Sealed{}, err } forProvider, err := Seal(providerKey, []byte(password)) if err != nil { return Sealed{}, err } return Sealed{ ForConsumer: forConsumer, ForProvider: forProvider, ConsumerKey: consumerKey, ProviderKey: providerKey, }, nil } // Seal closes a value to a node's public sealing key. func Seal(publicKey string, value []byte) (string, error) { public, err := base64.StdEncoding.DecodeString(publicKey) if err != nil || len(public) != 32 { return "", fmt.Errorf("%q is not a sealing key", publicKey) } var pub [32]byte copy(pub[:], public) sealed, err := box.SealAnonymous(nil, value, &pub, rand.Reader) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(sealed), nil }