fix(0006f): cluster secret out of argv, migrate-to-kv TLS guard, R1/CA docs (audit 0008 lows)
Low-severity cluster hardening from audit 0008: - Route secret out of argv (N1-low): --cluster-pass and a nats://user:pass@host in --routes are visible in ps/journald. New --cluster-pass-file and the UNIBUS_CLUSTER_PASS env var (precedence file > env > flag); the resolved secret guards the route layer and is injected into bare --routes entries (injectRouteCreds), so peers can be listed as nats://host:6250 with no secret in argv. The legacy --cluster-pass stays for dev/compat. - migrate-to-kv confidentiality (N6): refuse a remote --nats-url without --ca (the allowlist would travel cleartext); loopback targets are exempt (isLoopbackURL). - Docs (N1 route CA, N3 DoS): deploy/README gains a Clustering section — use a SEPARATE cluster CA for routes (not the client CA), keep the secret out of argv, run migrate-to-kv loopback/TLS only, and R1 is a SPOF of auth (not HA); R3 quorum is real HA. The generated cert material lives in deploy/cluster/ (0006g). Tests: - TestResolveClusterPass (file > env > flag precedence; missing file errors), - TestInjectRouteCreds (injects only into userinfo-less routes; preserves overrides), - TestIsLoopbackURL (loopback vs remote vs malformed). CGO_ENABLED=0 go build/vet/test green; govulncheck 0 reachable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestResolveClusterPass verifies the secret resolution precedence
|
||||
// (file > env > flag) that keeps the cluster password out of argv (issue 0006f).
|
||||
func TestResolveClusterPass(t *testing.T) {
|
||||
// file wins over env and flag, and is trimmed.
|
||||
f := filepath.Join(t.TempDir(), "pass")
|
||||
if err := os.WriteFile(f, []byte("filesecret\n"), 0o600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
if got, src, err := resolveClusterPass("flagsecret", f, "envsecret"); err != nil || got != "filesecret" || src != "file" {
|
||||
t.Fatalf("file precedence: got %q src %q err %v", got, src, err)
|
||||
}
|
||||
// env wins over flag when no file.
|
||||
if got, src, err := resolveClusterPass("flagsecret", "", "envsecret"); err != nil || got != "envsecret" || src != "env" {
|
||||
t.Fatalf("env precedence: got %q src %q err %v", got, src, err)
|
||||
}
|
||||
// flag is the last resort.
|
||||
if got, src, err := resolveClusterPass("flagsecret", "", ""); err != nil || got != "flagsecret" || src != "flag" {
|
||||
t.Fatalf("flag fallback: got %q src %q err %v", got, src, err)
|
||||
}
|
||||
// none set.
|
||||
if got, src, err := resolveClusterPass("", "", ""); err != nil || got != "" || src != "none" {
|
||||
t.Fatalf("none: got %q src %q err %v", got, src, err)
|
||||
}
|
||||
// missing file is an error.
|
||||
if _, _, err := resolveClusterPass("", filepath.Join(t.TempDir(), "nope"), ""); err == nil {
|
||||
t.Fatalf("missing file must error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectRouteCreds verifies the secret is injected only into routes that omit
|
||||
// userinfo, so --routes argv need not carry the password (issue 0006f).
|
||||
func TestInjectRouteCreds(t *testing.T) {
|
||||
in := []string{"nats://10.0.0.2:6250", "nats://override:pw@10.0.0.3:6250"}
|
||||
out, err := injectRouteCreds(in, "user", "secret")
|
||||
if err != nil {
|
||||
t.Fatalf("inject: %v", err)
|
||||
}
|
||||
if !strings.Contains(out[0], "user:secret@10.0.0.2:6250") {
|
||||
t.Fatalf("creds not injected into bare route: %q", out[0])
|
||||
}
|
||||
if !strings.Contains(out[1], "override:pw@10.0.0.3:6250") {
|
||||
t.Fatalf("existing userinfo must be preserved: %q", out[1])
|
||||
}
|
||||
// empty user is a no-op.
|
||||
noop, err := injectRouteCreds(in, "", "")
|
||||
if err != nil || noop[0] != in[0] {
|
||||
t.Fatalf("empty user must be a no-op: %v %q", err, noop[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsLoopbackURL guards migrate-to-kv against pushing the allowlist cleartext
|
||||
// to a remote NATS (issue 0006f, audit 0008 N6).
|
||||
func TestIsLoopbackURL(t *testing.T) {
|
||||
loop := []string{"nats://127.0.0.1:4250", "nats://localhost:4250", "nats://[::1]:4250"}
|
||||
for _, u := range loop {
|
||||
if !isLoopbackURL(u) {
|
||||
t.Fatalf("%q should be loopback", u)
|
||||
}
|
||||
}
|
||||
remote := []string{"nats://10.0.0.2:4250", "nats://bus.example.com:4250", "::not-a-url"}
|
||||
for _, u := range remote {
|
||||
if isLoopbackURL(u) {
|
||||
t.Fatalf("%q should NOT be loopback", u)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user