activator: the token file is tightened and shape-checked on the early return
EnsureAdminToken returned early the moment it found a CARLOS_ADMIN_TOKEN line, and the chmod lived on the minting path only — so an env file that was already 0644 when the platform first looked at it kept holding the instance's secret at world-readable mode for the life of the instance. The mode only ever converged for files this function itself created. Tightening now runs on both paths; a chmod that fails does NOT fail the ensure (the token still works, and refusing a wake over a mode trades hardening for an outage) — it logs, once per file. The same early return also took an existing value entirely on trust. A torn write (an append interrupted, a file truncated by a full disk) leaves a short or garbled token that will 401 every tick, and the operator meets it as "scheduled work stopped working" rather than as a diagnosis. It is now shape-checked against the exact format this function mints: 64 lowercase hex characters. RULING (2026-08-24): a malformed existing token is WARNED about and LEFT IN PLACE — never rotated. The running instance was handed this same value at spawn, so file and process agree and its ticks authenticate; re-minting behind its back would 401 a live instance in order to tidy a string. Consistency beats hygiene, and the warning carries the one safe repair (stop the instance, delete the line, restart). It is also not proof of corruption: before the platform minted these, Eleven's tooling wrote them by hand, and a hand-written value of another shape works fine. Both complaints are deduplicated per (condition, file) per process. They are sticky conditions and every caller repeats — the runner ensures the token on every tick for the host — so an undeduplicated line would be thousands a day saying the same thing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+224
−4
internal/activator/admintoken.go+85 −4internal/activator/admintoken_test.go+139 −0
diff --git a/internal/activator/admintoken.go b/internal/activator/admintoken.go| index 225de9f2b..9ee178208 100644 |
| --- a/internal/activator/admintoken.go |
| +++ b/internal/activator/admintoken.go |
| @@ -24,6 +24,7 @@ import ( |
| "crypto/rand" |
| "encoding/hex" |
| "fmt" |
| + "log" |
| "os" |
| "strings" |
| "sync" |
| @@ -36,13 +37,66 @@ import ( |
| // first spawn's child holds the first. |
| var adminTokenMu sync.Mutex |
| +// adminTokenLogged remembers which (condition, file) pairs have already had |
| +// their complaint logged, so the two conditions below are diagnoses rather |
| +// than a log flood. Both of them are STICKY — a 0644 file the agent cannot |
| +// chmod, a token the wrong shape — and every one of the callers repeats: the |
| +// runner ensures the token on every tick for the host (every 15s while a |
| +// timer is due), so an un-deduplicated line would be thousands a day saying |
| +// the same thing. Read and written only under adminTokenMu. |
| +// |
| +// Per-process, deliberately: an agent restart says it again, which is the |
| +// one moment an operator is reading these logs anyway. |
| +var adminTokenLogged = map[string]bool{} |
| + |
| +// logOncePerFile emits msg the first time it is asked for this (path, kind) |
| +// pair and stays quiet afterwards. Keyed by kind as well as path so a file |
| +// that is both un-tightenable AND holding a malformed token still reports |
| +// both — one condition must not swallow the other's only line. Caller holds |
| +// adminTokenMu. |
| +func logOncePerFile(kind, path, msg string) { |
| + key := kind + "\x00" + path |
| + if adminTokenLogged[key] { |
| + return |
| + } |
| + adminTokenLogged[key] = true |
| + log.Print(msg) |
| +} |
| + |
| +// wellFormedAdminToken reports whether tok has the exact shape this file |
| +// mints: 64 lowercase hex characters (32 random bytes, hex-encoded). A |
| +// torn write — an append interrupted mid-line, a file truncated by a full |
| +// disk — can leave a short or garbled value that authenticates nothing, |
| +// and the failure it produces afterwards ("401 on every tick") points at |
| +// the wrong thing entirely. |
| +// |
| +// A token that fails this is not necessarily broken: before the platform |
| +// minted these, Eleven's tooling wrote them by hand, and a hand-written |
| +// value of another shape works perfectly well as long as the instance and |
| +// the file agree. That is exactly why a failure here is a WARNING and never |
| +// a rotation — see EnsureAdminToken. |
| +func wellFormedAdminToken(tok string) bool { |
| + if len(tok) != 64 { |
| + return false |
| + } |
| + for i := 0; i < len(tok); i++ { |
| + c := tok[i] |
| + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| + |
| // EnsureAdminToken makes sure path (an instance's own <host>.env) carries a |
| // CARLOS_ADMIN_TOKEN, minting one if it does not. The file is the instance's, |
| // not the agent's: every other line in it is preserved untouched and an |
| // existing token is never rewritten. The file ends up 0600 whether it was |
| -// created here or already existed in a looser mode — the same mode the env |
| -// file carries everywhere else, and the only thing standing between another |
| -// UNIX user on the box and the instance's secret. |
| +// created here, already existed in a looser mode, or already held a token — |
| +// that is the same mode the env file carries everywhere else, and the only |
| +// thing standing between another UNIX user on the box and the instance's |
| +// secret. An existing token is also shape-checked, and a value that isn't |
| +// the shape this mints is reported and left exactly as it is. |
| func EnsureAdminToken(path string) error { |
| adminTokenMu.Lock() |
| defer adminTokenMu.Unlock() |
| @@ -52,7 +106,34 @@ func EnsureAdminToken(path string) error { |
| if err != nil && !os.IsNotExist(err) { |
| return fmt.Errorf("read %s: %w", path, err) |
| } |
| - if parseEnvBytes(raw)["CARLOS_ADMIN_TOKEN"] != "" { |
| + if tok := parseEnvBytes(raw)["CARLOS_ADMIN_TOKEN"]; tok != "" { |
| + // The early return is NOT a no-op. Two things still have to be true |
| + // of a file that already holds the secret. |
| + // |
| + // First, the mode. The chmod below used to run only on the minting |
| + // path, so an env file that was already 0644 when the platform found |
| + // a token in it stayed 0644 — holding the very secret the mode is |
| + // there to protect. Tightening is idempotent and costs one syscall |
| + // per ensure; failing it does NOT fail the ensure, because the token |
| + // still works and refusing the wake over a mode would trade a |
| + // hardening for an outage. |
| + if err := os.Chmod(path, 0o600); err != nil { |
| + logOncePerFile("chmod", path, fmt.Sprintf("activator: %s holds CARLOS_ADMIN_TOKEN but could not be tightened to 0600: %v — another UNIX user on this box may be able to read the instance's secret", path, err)) |
| + } |
| + // Second, the shape. A torn write leaves a value that will 401 every |
| + // tick, and the platform must say so rather than let the operator |
| + // discover it as "scheduled work stopped working". |
| + // |
| + // It is LEFT IN PLACE, never rotated (ruling, 2026-08-24). The |
| + // running instance was handed this same value at spawn, so file and |
| + // process agree and ticks authenticate; re-minting behind the |
| + // instance's back would 401 a live instance instead of repairing |
| + // anything. Consistency beats hygiene here, and the operator has the |
| + // one safe repair — stop the instance, delete the line, let the next |
| + // spawn mint it. |
| + if !wellFormedAdminToken(tok) { |
| + logOncePerFile("shape", path, fmt.Sprintf("activator: CARLOS_ADMIN_TOKEN in %s is not the shape the platform mints (64 hex characters) — left in place, because the running instance holds this same value and rotating it would lock the platform out of a live instance; if it is corrupt, stop the instance, delete the line and restart it to have one minted", path)) |
| + } |
| return nil |
| } |
| secret := make([]byte, 32) |
diff --git a/internal/activator/admintoken_test.go b/internal/activator/admintoken_test.go| index a2c095408..a4dec130b 100644 |
| --- a/internal/activator/admintoken_test.go |
| +++ b/internal/activator/admintoken_test.go |
| @@ -1,6 +1,7 @@ |
| package activator |
| import ( |
| + "log" |
| "os" |
| "path/filepath" |
| "strings" |
| @@ -60,3 +61,141 @@ func TestEnsureAdminTokenTightensAnExistingFile(t *testing.T) { |
| t.Fatalf("mode after mint = %o, want 600", st.Mode().Perm()) |
| } |
| } |
| + |
| +// The tightening is not the minting path's alone. A file that ALREADY held a |
| +// token took an early return that skipped the chmod entirely, so a 0644 env |
| +// file written by hand (or by an older tool) kept holding the instance's |
| +// secret at world-readable mode for as long as the instance lived — the mode |
| +// only ever converged for files this function itself created. |
| +func TestEnsureAdminTokenTightensAFileThatAlreadyHoldsAToken(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "h.env") |
| + tok := strings.Repeat("ab", 32) |
| + if err := os.WriteFile(p, []byte("CARLOS_ADMIN_TOKEN="+tok+"\n"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := EnsureAdminToken(p); err != nil { |
| + t.Fatal(err) |
| + } |
| + st, err := os.Stat(p) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if st.Mode().Perm() != 0o600 { |
| + t.Fatalf("mode of a pre-existing token file after ensure = %o, want 600", st.Mode().Perm()) |
| + } |
| + env, _ := readEnvFile(p) |
| + if env["CARLOS_ADMIN_TOKEN"] != tok { |
| + t.Fatalf("token = %q, want the pre-existing %q — tightening must not rewrite the secret", env["CARLOS_ADMIN_TOKEN"], tok) |
| + } |
| +} |
| + |
| +// captureLog redirects the standard logger for the duration of one test and |
| +// returns what was written to it. Assertions below count a marker substring |
| +// rather than compare whole buffers: a goroutine left running by an earlier |
| +// test in this package (the activator's sweeper logs) can drop a line in |
| +// here at any moment, and a test that fails on someone else's log line is a |
| +// flake, not a finding. |
| +func captureLog(t *testing.T) *strings.Builder { |
| + t.Helper() |
| + var buf strings.Builder |
| + flags := log.Flags() |
| + log.SetOutput(&buf) |
| + log.SetFlags(0) |
| + t.Cleanup(func() { |
| + log.SetOutput(os.Stderr) |
| + log.SetFlags(flags) |
| + }) |
| + return &buf |
| +} |
| + |
| +// The ruling (2026-08-24): a malformed existing token is WARNED about and |
| +// LEFT IN PLACE — never rotated. The running instance was handed this same |
| +// value at spawn, so file and process agree and its ticks authenticate; |
| +// re-minting behind its back would 401 a live instance to repair a string. |
| +// The warning carries the operator's own repair instead. |
| +func TestEnsureAdminTokenWarnsAboutAMalformedTokenAndLeavesIt(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "h.env") |
| + const body = "DATABASE_URL=sqlite\nCARLOS_ADMIN_TOKEN=deadbe\n" |
| + if err := os.WriteFile(p, []byte(body), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + buf := captureLog(t) |
| + if err := EnsureAdminToken(p); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, err := os.ReadFile(p) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if string(raw) != body { |
| + t.Fatalf("file after ensure = %q, want it byte-identical to %q — a malformed token is never rotated", raw, body) |
| + } |
| + got := buf.String() |
| + if !strings.Contains(got, "CARLOS_ADMIN_TOKEN") || !strings.Contains(got, p) { |
| + t.Fatalf("log = %q, want a warning naming the variable and the file", got) |
| + } |
| + if !strings.Contains(got, "left in place") { |
| + t.Fatalf("log = %q, want it to say the value was left in place", got) |
| + } |
| + if n := strings.Count(got, "not the shape"); n != 1 { |
| + t.Fatalf("warning appeared %d times, want 1: %q", n, got) |
| + } |
| + // Sticky condition, repeated caller: the runner ensures the token on every |
| + // tick for the host. One line per file per process, not one per tick. |
| + if err := EnsureAdminToken(p); err != nil { |
| + t.Fatal(err) |
| + } |
| + if n := strings.Count(buf.String(), "not the shape"); n != 1 { |
| + t.Fatalf("warning appeared %d times across two ensures, want 1: %q", n, buf.String()) |
| + } |
| +} |
| + |
| +// The other half of the same contract: a well-formed token is silent. A |
| +// warning that fired on healthy files would be noise on every box, and the |
| +// shape check exists to be believed. |
| +func TestEnsureAdminTokenIsSilentForAWellFormedToken(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "h.env") |
| + if err := EnsureAdminToken(p); err != nil { // mint one, so the shape is by construction ours |
| + t.Fatal(err) |
| + } |
| + before, err := os.ReadFile(p) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + buf := captureLog(t) |
| + if err := EnsureAdminToken(p); err != nil { |
| + t.Fatal(err) |
| + } |
| + after, err := os.ReadFile(p) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if string(after) != string(before) { |
| + t.Fatalf("file changed under a well-formed token: %q → %q", before, after) |
| + } |
| + if strings.Contains(buf.String(), "CARLOS_ADMIN_TOKEN") || strings.Contains(buf.String(), "not the shape") { |
| + t.Fatalf("log = %q, want nothing said about a healthy token", buf.String()) |
| + } |
| +} |
| + |
| +// The shape is the minting format, exactly: 64 lowercase hex characters. |
| +func TestWellFormedAdminToken(t *testing.T) { |
| + good := strings.Repeat("0f", 32) |
| + for _, c := range []struct { |
| + name string |
| + tok string |
| + want bool |
| + }{ |
| + {"minted shape", good, true}, |
| + {"empty", "", false}, |
| + {"short", good[:63], false}, |
| + {"long", good + "0", false}, |
| + {"uppercase hex", strings.ToUpper(good), false}, |
| + {"non-hex", strings.Repeat("z", 64), false}, |
| + {"quoted", `"` + good[:62] + `"`, false}, |
| + } { |
| + if got := wellFormedAdminToken(c.tok); got != c.want { |
| + t.Errorf("%s: wellFormedAdminToken(%q) = %v, want %v", c.name, c.tok, got, c.want) |
| + } |
| + } |
| +} |