mirror of
https://github.com/luxfi/kms.git
synced 2026-10-30 16:23:36 +00:00
A missing or malformed KMS_ENCRYPTION_KEY_B64 used to degrade to an unencrypted ZapDB behind one log line at boot. The deployment it shipped in ran that way: every credential the fleet syncs sat in cleartext in the SSTs and the value log, and the S3 replica carried those same cleartext files off-cluster, so one PVC snapshot or one bucket read was the whole store. Correct JWT gating in front of a plaintext store does not protect the bytes. pkg/atrest is now the one place the key is spelled, validated and used to open the store, so a server open and a migration open cannot drift into different databases. An absent key is a refusal to boot, not a warning. It also names ROOT_ENCRYPTION_KEY in that refusal: that Infisical-era name is what a Hanzo deployment actually provisions today, and an operator reading "no key configured" while looking straight at a populated Secret needs to be told which name counts. The refusal is only shippable with a way through it. cmd/kms-rekey copies a store onto a key: the source is opened read-only and never written, the destination must be empty, and the record count is printed so "it exited 0" is not the only evidence the migration is complete. Rotation of an already-encrypted store is the same operation with KMS_REKEY_FROM_B64 set — there is no separate rotate path to keep in sync. It is a separate binary because the server starts serving the moment it runs: a migration hidden behind an argv word in that binary is one typo away from starting a second server on the directory it is copying. Co-authored-by: Hanzo Dev <[email protected]>
66 lines
2.1 KiB
Go
66 lines
2.1 KiB
Go
// Command kms-rekey moves a KMS store onto an at-rest encryption key.
|
|
//
|
|
// It is a separate binary on purpose. The server starts serving the moment it
|
|
// runs — it takes no subcommand — so a migration hidden behind an argv word in
|
|
// that binary is a migration one typo away from starting a second server on the
|
|
// data directory it is supposed to be copying. One binary, one job.
|
|
//
|
|
// Run it with the server STOPPED: ZapDB takes a directory lock, and a store
|
|
// being written while it is read is a store whose copy is missing records.
|
|
//
|
|
// # plaintext store -> encrypted store
|
|
// KMS_ENCRYPTION_KEY_B64=$(head -c 32 /dev/urandom | base64) \
|
|
// kms-rekey -from /data/kms -to /data/kms-encrypted
|
|
//
|
|
// # rotate an already-encrypted store onto a new key
|
|
// KMS_REKEY_FROM_B64=<old> KMS_ENCRYPTION_KEY_B64=<new> \
|
|
// kms-rekey -from /data/kms -to /data/kms-next
|
|
//
|
|
// The source is opened read-only and is never written. On success it prints the
|
|
// number of records in the destination; compare it against the source before
|
|
// pointing KMS_DATA_DIR at the result and deleting nothing until you have.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/luxfi/kms/pkg/atrest"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(0)
|
|
log.SetPrefix("kms-rekey: ")
|
|
|
|
from := flag.String("from", os.Getenv("KMS_DATA_DIR"), "store to read (defaults to $KMS_DATA_DIR)")
|
|
to := flag.String("to", "", "directory to write the re-encrypted store into (must be empty or absent)")
|
|
flag.Parse()
|
|
|
|
if *from == "" || *to == "" {
|
|
flag.Usage()
|
|
log.Fatal("-from and -to are both required")
|
|
}
|
|
|
|
dstKey, err := atrest.KeyFromEnv()
|
|
if err != nil {
|
|
log.Fatalf("%v", err)
|
|
}
|
|
srcKey, err := atrest.SourceKeyFromEnv()
|
|
if err != nil {
|
|
log.Fatalf("%v", err)
|
|
}
|
|
if len(srcKey) == 0 {
|
|
log.Printf("source %s is being read as PLAINTEXT (%s is unset)", *from, atrest.SourceKeyEnv)
|
|
}
|
|
|
|
records, err := atrest.Rekey(*from, *to, srcKey, dstKey)
|
|
if err != nil {
|
|
log.Fatalf("%v", err)
|
|
}
|
|
|
|
fmt.Printf("%d records copied to %s, encrypted under %s\n", records, *to, atrest.KeyEnv)
|
|
fmt.Printf("the source at %s is unchanged; verify the count before you point KMS_DATA_DIR at %s\n", *from, *to)
|
|
}
|