The last of the four gaps ADR 0024 names. Everything the mesh handles today it generated itself, sealed to both ends, and discarded. An API key for a hosted service comes from a person, and carrying it needs a verb the mesh did not have. Accept seals it on the way in and keeps no plaintext — the same storage and the same property as a generated one, only a different origin. That is the whole difference from the arrangement being replaced, where an operator-supplied key sits in a column the control plane can read, which makes a copy of the database a copy of every account the mesh touches. The consequence is deliberate: the mesh cannot show it back. Somebody who loses the key gets a new one from wherever it came from. There is no reveal and there cannot be one, because a mesh that can reveal a secret is a mesh that holds it — asserted as a test, because it is a property somebody will eventually ask to break. An empty value is refused. A credential that exists, authenticates nowhere and looks exactly like a working one is the failure this whole mechanism is arranged to prevent.
129 lines
5.3 KiB
Go
129 lines
5.3 KiB
Go
package secrets
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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
|
|
}
|
|
|
|
// Accept seals a value somebody supplied, rather than one the mesh made.
|
|
//
|
|
// **The mesh generates most of what it hands out and discards the plaintext.** An API key for a
|
|
// hosted service does not work that way: it comes from a person, and the mesh's job is to carry it
|
|
// to the machines that need it without being able to read it afterwards
|
|
// (novox/hq ADR 0024).
|
|
//
|
|
// So the value is sealed on the way in and **the plaintext is not kept**. That is the whole of the
|
|
// difference from the arrangement this replaces, where an operator-supplied key sits in a column
|
|
// the control plane can read — which makes a copy of the database a copy of every account the mesh
|
|
// touches.
|
|
//
|
|
// The consequence is deliberate and worth stating: **the mesh cannot show it back.** Somebody who
|
|
// loses the key gets a new one from wherever it came from; there is no "reveal" and there cannot
|
|
// be one, because a mesh that can reveal a secret is a mesh that holds it.
|
|
func Accept(value string, consumerKey, providerKey string) (Sealed, error) {
|
|
if strings.TrimSpace(value) == "" {
|
|
return Sealed{}, fmt.Errorf("there is nothing to seal")
|
|
}
|
|
if consumerKey == "" || providerKey == "" {
|
|
return Sealed{}, fmt.Errorf("both ends need a sealing key before a secret can be kept")
|
|
}
|
|
forConsumer, err := Seal(consumerKey, []byte(value))
|
|
if err != nil {
|
|
return Sealed{}, err
|
|
}
|
|
forProvider, err := Seal(providerKey, []byte(value))
|
|
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
|
|
}
|