Summary
sam-node and sam-router poll enrollment status with POST, but HandleEnrollStatus only accepts GET with a peer_id query parameter. Every poll returns 405 Method Not Allowed, so a node that has been approved by an admin can never retrieve its Biscuit.
The effect is that bootstrap-token enrollment cannot complete. This is the headless path (CI, containers, non-interactive deployments), so it is not reachable via the interactive OIDC flow.
Confirmed present at 674af93e8942fee8f05f6f9aec7d210fed6e947b (current main at time of filing).
The mismatch
Server — internal/controlplane/server.go, HandleEnrollStatus:
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
peerID := r.URL.Query().Get("peer_id")
Client — internal/node/enroll.go:
statusURL := controlPlaneURL + "/enroll/status"
hReq, err := http.NewRequestWithContext(ctx, "POST", statusURL, bytes.NewReader(statusData))
hReq.Header.Set("Content-Type", "application/x-protobuf")
Client — internal/router/router.go:
statusResp, err := client.Post(r.config.ControlPlaneURL+"/enroll/status",
"application/x-protobuf", bytes.NewReader(statusData))
Reproduction
# 1. Control plane (manual approval is the default)
sam-control-plane \
--bind-address 127.0.0.1:8090 \
--issuer http://127.0.0.1:5570 \
--allowed-audiences my-audience \
--admin-token-path ./admin-token \
--db-dsn ./cp.db
# 2. Mint a node bootstrap token
curl -s -X POST http://127.0.0.1:8090/admin/bootstrap-tokens \
-H "Authorization: Bearer $(cat admin-token)" \
-H "Content-Type: application/json" \
-d '{"role":"sam:role:node","ttl_hours":2,"max_usages":5}'
# 3. Start a node with that token
sam-node run --control-plane http://127.0.0.1:8090 \
--bootstrap-token sam-bt-... --allow-loopback
# 4. Approve the pending enrollment
curl -s -X POST http://127.0.0.1:8090/admin/enrollments/<id>/approve \
-H "Authorization: Bearer $(cat admin-token)"
# -> 200 Enrollment approved
The node never picks the approval up:
WARN sam-node enroll.go:281 Status check returned status 405 Method Not Allowed: Method not allowed
WARN sam-node enroll.go:281 Status check returned status 405 Method Not Allowed: Method not allowed
...
sam-router behaves identically (router.go:408).
The endpoint itself works — only the verb is wrong:
curl -i "http://127.0.0.1:8090/enroll/status?peer_id=12D3KooW..."
# HTTP/1.1 200 OK
# Content-Type: application/x-protobuf (589 bytes: the approved Biscuit)
curl -i -X POST "http://127.0.0.1:8090/enroll/status"
# HTTP/1.1 405 Method Not Allowed
Suggested fix
Changing the two clients to GET is the smaller change, since the server already reads peer_id from the query string and the request body is otherwise unused:
// internal/node/enroll.go
statusURL := controlPlaneURL + "/enroll/status?peer_id=" + url.QueryEscape(n.Host.ID().String())
hReq, err := http.NewRequestWithContext(ctx, "GET", statusURL, nil)
// internal/router/router.go
statusResp, err := client.Get(r.config.ControlPlaneURL + "/enroll/status?peer_id=" + peerID.String())
statusData/statusReq then become unused in both call sites and can be removed.
Alternatively, have HandleEnrollStatus accept POST with the existing protobuf body — that keeps the wire format but is a larger server-side change.
I verified the client-side fix locally: after patching, the router logged Router bootstrap enrollment approved! Biscuit received. and three nodes enrolled and formed a working mesh.
Notes
- A test covering approve-then-collect over HTTP would catch this class of bug; the current tests appear to exercise the handlers directly rather than through the client polling path.
- Happy to open a PR if that is useful — I would need to sort out the CLA first.
Found while building an external integration against SAM. Environment: Windows, Go 1.25.14, binaries built from source.
Summary
sam-nodeandsam-routerpoll enrollment status with POST, butHandleEnrollStatusonly accepts GET with apeer_idquery parameter. Every poll returns405 Method Not Allowed, so a node that has been approved by an admin can never retrieve its Biscuit.The effect is that bootstrap-token enrollment cannot complete. This is the headless path (CI, containers, non-interactive deployments), so it is not reachable via the interactive OIDC flow.
Confirmed present at
674af93e8942fee8f05f6f9aec7d210fed6e947b(currentmainat time of filing).The mismatch
Server —
internal/controlplane/server.go,HandleEnrollStatus:Client —
internal/node/enroll.go:Client —
internal/router/router.go:Reproduction
The node never picks the approval up:
sam-routerbehaves identically (router.go:408).The endpoint itself works — only the verb is wrong:
Suggested fix
Changing the two clients to GET is the smaller change, since the server already reads
peer_idfrom the query string and the request body is otherwise unused:statusData/statusReqthen become unused in both call sites and can be removed.Alternatively, have
HandleEnrollStatusaccept POST with the existing protobuf body — that keeps the wire format but is a larger server-side change.I verified the client-side fix locally: after patching, the router logged
Router bootstrap enrollment approved! Biscuit received.and three nodes enrolled and formed a working mesh.Notes
Found while building an external integration against SAM. Environment: Windows, Go 1.25.14, binaries built from source.