Verify the validator-side half of the tokenless-sim REJECT root cause against current source (first link of the bf-34lwt split): - mothership/cmd/mothership/main.go:4494 — SetTokenValidator(...) is a bare statement with no env/build guard; provSrv is created unconditionally at :4492. 'validator configured?' is always YES. - mothership/internal/ingestion/server.go:513 — tokenOK := hello.Token != "" && validator(hello.MAC, hello.Token) depends solely on the deserialized hello JSON body field (message.go:22), not any HTTP header. No X-Spaxel-Token read exists on this path. Conclusion: validator always configured; accept/reject hinges entirely on hello.Token. Co-Authored-By: Claude <noreply@anthropic.com>
4.7 KiB
Confirmation: validator wired unconditionally + reads hello.Token only
Tracking: bead bf-5ig3e (first link of the bf-34lwt split).
Date: 2026-07-07.
Scope: isolate and re-verify the validator-side half of the
tokenless-sim REJECT root cause against current source. This confirms the two
statements "validator configured? always YES" and "acceptance/rejection hinges
entirely on hello.Token". The header-supply half of the root cause is covered
by the parent note token-reject-root-cause.md
(bead bf-34lwt); this note re-checks the two facts that note depends on, so a
drift in either is detectable on its own.
Path convention. Citations are full repo-relative paths. The
bf-5ig3etask body drops themothership/module prefix, so itscmd/mothership/main.go:4494ismothership/cmd/mothership/main.go:4494here, and itsinternal/ingestion/server.go:513ismothership/internal/ingestion/server.go:513.
Fact 1 — validator is wired UNCONDITIONALLY
mothership/cmd/mothership/main.go:4492-4499:
provSrv := provisioning.NewServer(cfg.DataDir, cfg.MDNSName, msPort, cfg.NTPServer, cfg.InstallSecret)
r.Post("/api/provision", provSrv.HandleProvision)
ingestSrv.SetTokenValidator(provSrv.ValidateToken) // :4494 — bare statement, no guard
if cfg.MigrationWindowHours > 0 { // :4495 — gates the DEADLINE, not the validator
deadline := time.Now().Add(time.Duration(cfg.MigrationWindowHours) * time.Hour)
ingestSrv.SetMigrationDeadline(deadline)
log.Printf("[INFO] Migration window open until %s (%d h)", deadline.Format(time.RFC3339), cfg.MigrationWindowHours)
}
The SetTokenValidator call at :4494 is a top-level statement in the wiring
sequence. There is no if around it — not behind cfg.MigrationWindowHours,
not behind any env var, not behind a build tag. provSrv is itself created
unconditionally at :4492. The immediately following if cfg.MigrationWindowHours > 0 (:4495) gates SetMigrationDeadline, not
SetTokenValidator. Therefore s.tokenValidator is always non-nil at runtime:
"validator configured?" is always YES.
Fact 2 — the validator reads hello.Token only
mothership/internal/ingestion/server.go:506-513:
// Token validation: if a validator is configured, reject unauthenticated nodes
// unless the migration window is still open, in which case allow but mark as Unpaired.
s.mu.RLock()
validator := s.tokenValidator
deadline := s.migrationDeadline
s.mu.RUnlock()
if validator != nil {
tokenOK := hello.Token != "" && validator(hello.MAC, hello.Token) // :513
tokenOK is a function of exactly two values, both drawn from the deserialized
hello WebSocket JSON frame:
hello.Token— thetoken,omitemptyJSON field ofHelloMessage(mothership/internal/ingestion/message.go:22), populated only byParseJSONMessage(msg)from the node's first WS frame (server.go:489,hello, ok := parsed.(*HelloMessage)at:496). It is not an HTTP header value.hello.MAC— passed through to the validator; irrelevant to where the token comes from.
The validator itself is provSrv.ValidateToken,
mothership/internal/provisioning/server.go:135:
func (s *Server) ValidateToken(mac, token string) bool {
...
expected := s.deriveToken(mac)
return subtle.ConstantTimeCompare([]byte(expected), []byte(token)) == 1
}
Its signature takes only (mac, token) — there is no header, no *http.Request,
no r *http.Request in scope anywhere on this path. A grep for the header name
over the ingestion tree returns no reads:
$ grep -rn 'X-Spaxel-Token' mothership/internal/ingestion/ # (no hits)
The only Header() reference in server.go (:463) is w.Header().Set(...)
on an outbound response. The WS upgrade at :469
(conn, err := s.upgrader.Upgrade(w, r, nil)) inspects nothing on r.
So there is no path through which an HTTP header (e.g. the X-Spaxel-Token
header the sim does send) can flip tokenOK to true. Acceptance/rejection
hinges entirely on hello.Token.
Conclusion
The validator is always configured (
main.go:4494, unconditional); acceptance vs. rejection hinges entirely onhello.Token(server.go:513), the JSON body field — no HTTP header is read on this path.
This isolates the validator-side half of bf-34lwt. Combined with the
header-side facts in the parent note (the sim supplies its token only as the
unread X-Spaxel-Token header, so hello.Token is always ""), it follows
that tokenOK is always false for a tokenless sim node — and the node is
accepted only while the migration window is open (the default 24h mask), then
sendReject-ed with invalid_token once it closes.