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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user