A secret reaches a process as a file (ADR 0086)

The broker settings take a _FILE twin like the store connections; the
catalogue engine refuses a secret placeholder in a container's env and a
secret-carrying env-file unless the container says why with
secrets-in-environment, which stays in the catalogue and never reaches the
machine.
This commit is contained in:
2026-09-21 10:10:33 +02:00
parent 6b695c82d7
commit 4531f2244f
8 changed files with 245 additions and 5 deletions
+39
View File
@@ -0,0 +1,39 @@
// Package envfile reads a setting that may be a secret from the environment or, preferably, from
// a file the environment names.
//
// **A secret reaches a process as a file** (novox/hq ADR 0086). An environment variable is
// readable in `docker inspect`, in the process's /proc entry and in whatever composed it; a file
// the mesh sealed to the machine and the host wrote at 0600 is readable where it is used and
// nowhere else. So every variable of the control plane's that carries a credential has a `_FILE`
// twin naming such a file, and the plain form remains only for a control plane a person starts by
// hand and for the bundle that raises the first one. The store's connections had this shape
// already (internal/store); this is the same rule for the rest.
package envfile
import (
"fmt"
"os"
"strings"
)
// Value is the setting named by `name`: the content of the file `name_FILE` points at when that
// is set, else the variable itself. Both set is refused — two sources that could disagree is how
// a setting silently stops meaning what it says. Neither set is "", nil.
func Value(name string) (string, error) {
plain, hasPlain := os.LookupEnv(name)
path, hasFile := os.LookupEnv(name + "_FILE")
switch {
case hasFile && hasPlain && strings.TrimSpace(plain) != "" && strings.TrimSpace(path) != "":
return "", fmt.Errorf("both %s and %s_FILE are set; one of them, not both", name, name)
case hasFile && strings.TrimSpace(path) != "":
raw, err := os.ReadFile(strings.TrimSpace(path))
if err != nil {
return "", fmt.Errorf("%s_FILE names %s, which cannot be read: %w", name, path, err)
}
// A file has a line ending and a value does not — trimmed, and only the ending, because a
// value may begin or end with a space and still be the value.
return strings.TrimRight(string(raw), "\r\n"), nil
default:
return strings.TrimSpace(plain), nil
}
}
+44
View File
@@ -0,0 +1,44 @@
package envfile
import (
"os"
"path/filepath"
"testing"
)
func TestAFileTwinIsPreferredAndOnlyItsLineEndingGoes(t *testing.T) {
path := filepath.Join(t.TempDir(), "value")
if err := os.WriteFile(path, []byte(" amqp://u:p@h/ \n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("MESH_X", "")
t.Setenv("MESH_X_FILE", path)
got, err := Value("MESH_X")
if err != nil || got != " amqp://u:p@h/ " {
t.Fatalf("got %q, %v", got, err)
}
}
func TestThePlainVariableStillWorks(t *testing.T) {
t.Setenv("MESH_Y", " plain ")
if got, err := Value("MESH_Y"); err != nil || got != "plain" {
t.Fatalf("got %q, %v", got, err)
}
}
func TestBothSetIsRefused(t *testing.T) {
path := filepath.Join(t.TempDir(), "value")
_ = os.WriteFile(path, []byte("a"), 0o600)
t.Setenv("MESH_Z", "b")
t.Setenv("MESH_Z_FILE", path)
if _, err := Value("MESH_Z"); err == nil {
t.Fatal("two sources were accepted")
}
}
func TestAMissingFileIsSaid(t *testing.T) {
t.Setenv("MESH_W_FILE", filepath.Join(t.TempDir(), "absent"))
if _, err := Value("MESH_W"); err == nil {
t.Fatal("a missing file produced a value")
}
}