A store connection string carries a password, and the control plane took it
from MESH_STORE_<CONTEXT> — an environment variable, which is readable in
`docker inspect`, in the process's own /proc entry, and in whatever composed
it. Every other module in the catalogue is given secret material as a file the
mesh sealed to the machine and the host wrote.
That difference is what stopped the control plane from being an ordinary module
(novox/hq ADR 0067). A manifest can put a sealed value into a file's `content`
with ${secret:…}; it has no substitution into a container's `env` at all. So a
control-plane manifest could be written with the password in it, or without the
setting — neither honest. The fix is not to change the manifest format but to
let the control plane read what everything else reads: a file.
MESH_STORE_<CONTEXT>_FILE names one. Exactly one of the two may be set; both is
refused rather than settled by precedence, because whichever won, the other
would still read as the setting in force and the process would be writing to a
store nobody expects. Trailing whitespace is trimmed — a file written by a
person or by a filled-in placeholder ends in a newline, and a newline inside a
URL is rejected several layers from anything that could explain it. Leading
whitespace is left, being a mangled value rather than a habit.
The no-leak property is kept and extended: a file that cannot be read names its
path, never its contents, and the parse failure now names whichever source was
used because a variable name and a path are not the secret.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
216 lines
6.6 KiB
Go
216 lines
6.6 KiB
Go
package store
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Where a context's connection settings come from, and what happens when that is unclear.
|
|
//
|
|
// No database is needed for any of this: pgxpool connects lazily, so `Open` succeeding proves that
|
|
// a string was found and parsed, which is exactly what is under test here. The live tests next door
|
|
// prove the rest.
|
|
|
|
// example is a context name these tests can own outright. Short, and matching `contextName`, which
|
|
// allows no dashes.
|
|
const example = "example"
|
|
|
|
// dsn is a connection string shaped like a real one, password and all — because the property most
|
|
// of these tests assert is that this never appears in an error.
|
|
const dsn = "postgres://postgres:s3cret-in-here@127.0.0.1:5432/example?sslmode=disable"
|
|
|
|
// alone clears both variables for a context, so a test is not passing or failing on what the
|
|
// machine running it happens to export.
|
|
func alone(t *testing.T) {
|
|
t.Helper()
|
|
t.Setenv(Variable(example), "")
|
|
t.Setenv(FileVariable(example), "")
|
|
}
|
|
|
|
func TestTheFileVariableIsTheVariablePlusFile(t *testing.T) {
|
|
if got := FileVariable("inventory"); got != "MESH_STORE_INVENTORY_FILE" {
|
|
t.Errorf("the file variable is %q", got)
|
|
}
|
|
}
|
|
|
|
func TestTheConnectionMayComeFromAFile(t *testing.T) {
|
|
alone(t)
|
|
path := filepath.Join(t.TempDir(), "inventory")
|
|
if err := os.WriteFile(path, []byte(dsn), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(FileVariable(example), path)
|
|
|
|
held, from, err := settingsFor(example)
|
|
if err != nil {
|
|
t.Fatalf("a file holding the settings was refused: %v", err)
|
|
}
|
|
if held != dsn {
|
|
t.Errorf("the settings came back as %q", held)
|
|
}
|
|
if !strings.Contains(from, FileVariable(example)) || !strings.Contains(from, path) {
|
|
t.Errorf("the source is reported as %q, which names neither the variable nor the file", from)
|
|
}
|
|
|
|
// And the whole way through, so this is not a test of a helper nobody calls.
|
|
opened, err := Open(t.Context(), example)
|
|
if err != nil {
|
|
t.Fatalf("the store would not open from a file: %v", err)
|
|
}
|
|
opened.Close()
|
|
}
|
|
|
|
func TestATrailingNewlineInTheFileIsTolerated(t *testing.T) {
|
|
alone(t)
|
|
path := filepath.Join(t.TempDir(), "inventory")
|
|
// What a person's editor writes, and what the host writes when it fills a ${secret:…}
|
|
// placeholder into a file whose content ended in one. Untrimmed it is a control character
|
|
// inside a URL, which is refused far from anything that could explain it.
|
|
if err := os.WriteFile(path, []byte(dsn+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(FileVariable(example), path)
|
|
|
|
held, _, err := settingsFor(example)
|
|
if err != nil {
|
|
t.Fatalf("a file ending in a newline was refused: %v", err)
|
|
}
|
|
if held != dsn {
|
|
t.Errorf("the newline survived: %q", held)
|
|
}
|
|
if _, err := pgxpool.ParseConfig(held); err != nil {
|
|
t.Errorf("what came out of the file does not parse: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBothTheVariableAndTheFileIsRefused(t *testing.T) {
|
|
alone(t)
|
|
path := filepath.Join(t.TempDir(), "inventory")
|
|
if err := os.WriteFile(path, []byte(dsn), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(Variable(example), dsn)
|
|
t.Setenv(FileVariable(example), path)
|
|
|
|
_, _, err := settingsFor(example)
|
|
if err == nil {
|
|
t.Fatal("both were set and one of them was silently chosen")
|
|
}
|
|
for _, want := range []string{Variable(example), FileVariable(example)} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not name %s: %v", want, err)
|
|
}
|
|
}
|
|
mustNotLeak(t, err)
|
|
|
|
// Open refuses for the same reason rather than reaching a database.
|
|
if _, err := Open(t.Context(), example); err == nil {
|
|
t.Fatal("Open accepted both settings at once")
|
|
}
|
|
}
|
|
|
|
func TestAFileThatCannotBeReadIsRefusedByPath(t *testing.T) {
|
|
alone(t)
|
|
path := filepath.Join(t.TempDir(), "never-written")
|
|
t.Setenv(FileVariable(example), path)
|
|
|
|
_, _, err := settingsFor(example)
|
|
if err == nil {
|
|
t.Fatal("a missing settings file was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), path) {
|
|
t.Errorf("the refusal does not say which file: %v", err)
|
|
}
|
|
mustNotLeak(t, err)
|
|
}
|
|
|
|
func TestAnEmptyFileIsRefusedByPath(t *testing.T) {
|
|
alone(t)
|
|
path := filepath.Join(t.TempDir(), "inventory")
|
|
// A placeholder that was never filled in looks exactly like this on the machine.
|
|
if err := os.WriteFile(path, []byte("\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(FileVariable(example), path)
|
|
|
|
_, _, err := settingsFor(example)
|
|
if err == nil {
|
|
t.Fatal("an empty settings file was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), path) {
|
|
t.Errorf("the refusal does not say which file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNeitherIsRefusedNamingBoth(t *testing.T) {
|
|
alone(t)
|
|
_, _, err := settingsFor(example)
|
|
if err == nil {
|
|
t.Fatal("a context with no settings at all was accepted")
|
|
}
|
|
for _, want := range []string{Variable(example), FileVariable(example)} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not name %s: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheEnvironmentVariableStillWorksUnchanged(t *testing.T) {
|
|
alone(t)
|
|
t.Setenv(Variable(example), dsn)
|
|
|
|
held, from, err := settingsFor(example)
|
|
if err != nil {
|
|
t.Fatalf("the variable that has always worked was refused: %v", err)
|
|
}
|
|
if held != dsn {
|
|
t.Errorf("the settings came back as %q", held)
|
|
}
|
|
if from != Variable(example) {
|
|
t.Errorf("the source is reported as %q", from)
|
|
}
|
|
|
|
opened, err := Open(t.Context(), example)
|
|
if err != nil {
|
|
t.Fatalf("the store would not open from the variable: %v", err)
|
|
}
|
|
opened.Close()
|
|
}
|
|
|
|
// The no-leak property, at the one place a file makes it easy to lose: a value that will not parse
|
|
// is usually a password with something wrong around it, and the error goes into a log.
|
|
func TestASettingsFileThatCannotBeParsedIsNotQuotedBack(t *testing.T) {
|
|
alone(t)
|
|
path := filepath.Join(t.TempDir(), "inventory")
|
|
const mangled = "postgres://postgres:s3cret-in-here@ 127.0.0.1:5432/example"
|
|
if err := os.WriteFile(path, []byte(mangled), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(FileVariable(example), path)
|
|
|
|
opened, err := Open(t.Context(), example)
|
|
if err == nil {
|
|
opened.Close()
|
|
t.Fatal("a mangled connection string was accepted")
|
|
}
|
|
if strings.Contains(err.Error(), "s3cret-in-here") {
|
|
t.Fatalf("the password is in the error: %v", err)
|
|
}
|
|
// It still says enough to be actionable: which variable, and which file.
|
|
if !strings.Contains(err.Error(), FileVariable(example)) ||
|
|
!strings.Contains(err.Error(), path) {
|
|
t.Errorf("the refusal says neither where to look nor which file: %v", err)
|
|
}
|
|
}
|
|
|
|
func mustNotLeak(t *testing.T, err error) {
|
|
t.Helper()
|
|
if strings.Contains(err.Error(), "s3cret-in-here") {
|
|
t.Fatalf("the password is in the error: %v", err)
|
|
}
|
|
}
|