Skip to content

Bootstrap enrollment cannot complete: clients POST to /enroll/status but the server only accepts GET (405) #297

Description

@yorkerhodes3

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions