Genesis makes the root secrets and the operator key, and installs the vault #14
@@ -397,6 +397,8 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
|
||||
if err := RefuseExistingServers(ctx, d.Run, creds); err != nil {
|
||||
return result, failed(StepBundle, err)
|
||||
}
|
||||
// From here on nothing this installer says contains the values it just made.
|
||||
say = Masking(say, creds)
|
||||
root, err := RewriteRoot(&rewritten, creds)
|
||||
if err != nil {
|
||||
return result, failed(StepBundle, err)
|
||||
|
||||
@@ -143,6 +143,20 @@ func freshSecret() (string, error) {
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// Masking makes a reporter that never says the credentials this run made.
|
||||
//
|
||||
// The applier reports each action with its command line, and two of them now carry a real
|
||||
// password — the context schemas' connection strings and the broker's change_password. Those
|
||||
// lines go to a terminal and to whatever keeps the transcript, which for the lab is a file. The
|
||||
// exact values are known here, so they are replaced wherever they appear, in every line said.
|
||||
func Masking(say func(string), c RootCredentials) func(string) {
|
||||
replacer := strings.NewReplacer(c.Store, "…", c.Broker, "…")
|
||||
if c.Store == "" || c.Broker == "" {
|
||||
return say
|
||||
}
|
||||
return func(line string) { say(replacer.Replace(line)) }
|
||||
}
|
||||
|
||||
// RefuseExistingServers stops a run that would put a made credential in front of a server raised
|
||||
// by an earlier installer with the template's.
|
||||
//
|
||||
@@ -219,14 +233,16 @@ func RewriteRoot(r *Rewritten, c RootCredentials) (RootRewrite, error) {
|
||||
// Verified by a marker on the broker's own data volume holding this password's fingerprint —
|
||||
// the image carries nothing that can try a password from inside, and a marker that merely
|
||||
// existed would let a regenerated password go unapplied for ever. What proves the password
|
||||
// works is the control plane answering over it, a few resources later.
|
||||
// works is the control plane answering over it, a few resources later. The marker ends in a
|
||||
// newline because the verify reads it with the shell's `read`, which fails at end of file
|
||||
// without one — an action that ran and a verify that said no, once, in the lab.
|
||||
fp := credentialFingerprint(c.Broker)
|
||||
action := templateBrokerReady + "\n" +
|
||||
" {\n" +
|
||||
" \"id\": \"broker-admin\",\n" +
|
||||
" \"type\": \"action\",\n" +
|
||||
" \"in\": \"mesh-broker\",\n" +
|
||||
" \"command\": [\"sh\", \"-c\", \"lavinmqctl change_password " + BrokerAdminUser + " '" + c.Broker + "' && printf %s " + fp + " > " + brokerAdminMarker + "\"],\n" +
|
||||
" \"command\": [\"sh\", \"-c\", \"lavinmqctl change_password " + BrokerAdminUser + " '" + c.Broker + "' && echo " + fp + " > " + brokerAdminMarker + "\"],\n" +
|
||||
" \"verify\": [\"sh\", \"-c\", \"read m < " + brokerAdminMarker + " && [ \\\"$m\\\" = " + fp + " ]\"]\n" +
|
||||
" },"
|
||||
if bundle, err = replaceOnce(bundle, templateBrokerReady, action, "the broker's readiness check"); err != nil {
|
||||
|
||||
@@ -121,3 +121,47 @@ func TestRootSecretsAreKeptAcrossRuns(t *testing.T) {
|
||||
t.Fatal("a dry run wrote a secret")
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing the installer says after making the credentials contains them.
|
||||
func TestTheTranscriptNeverSaysTheCredentials(t *testing.T) {
|
||||
var said []string
|
||||
say := Masking(func(l string) { said = append(said, l) }, RootCredentials{Store: "STORE-PW", Broker: "BROKER-PW"})
|
||||
say("created context-schemas (docker run -e MESH_STORE_INVENTORY=postgres://postgres:STORE-PW@127.0.0.1:5432/inventory)")
|
||||
say("failed broker-admin (sh -c lavinmqctl change_password guest 'BROKER-PW' && echo x)")
|
||||
for _, l := range said {
|
||||
if strings.Contains(l, "STORE-PW") || strings.Contains(l, "BROKER-PW") {
|
||||
t.Errorf("said a credential: %s", l)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(said[0], "postgres:…@") || !strings.Contains(said[1], "guest '…'") {
|
||||
t.Errorf("the lines were not the same lines with the values masked: %v", said)
|
||||
}
|
||||
}
|
||||
|
||||
// The broker-admin marker is written with a line ending, because the verify reads it with `read`.
|
||||
func TestTheBrokerAdminMarkerHasALineEnding(t *testing.T) {
|
||||
template, err := os.ReadFile("../../examples/foundation-first-node.lock")
|
||||
if err != nil {
|
||||
t.Skip("no example bundle beside this checkout")
|
||||
}
|
||||
r, err := Rewrite(template, "sha256:"+strings.Repeat("ab", 32))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := RewriteRoot(&r, RootCredentials{Store: "s", Broker: "b"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, res := range r.Declaration.Resources {
|
||||
a, ok := res.(*declaration.Action)
|
||||
if !ok || a.ID != "broker-admin" {
|
||||
continue
|
||||
}
|
||||
cmd := strings.Join(a.Command, " ")
|
||||
if !strings.Contains(cmd, "&& echo ") || strings.Contains(cmd, "printf %s") {
|
||||
t.Errorf("the marker is written without a line ending: %s", cmd)
|
||||
}
|
||||
if !strings.Contains(strings.Join(a.Verify, " "), "read m <") {
|
||||
t.Errorf("the verify does not read the marker: %v", a.Verify)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user