router-for-me/CLIProxyAPI · error

home jwt cluster_id is required

Error message

home jwt cluster_id is required

What it means

Claim-level validation in parseHomeJWTClaims: the JWT payload's cluster_id claim is empty or whitespace-only. The cluster_id identifies which home cluster the client enrolls into and is mandatory in the enrollment token contract.

Source

Thrown at internal/home/certificate.go:98

func parseHomeJWTClaims(rawJWT string) (homeJWTClaims, error) {
	var claims homeJWTClaims
	parts := strings.Split(strings.TrimSpace(rawJWT), ".")
	if len(parts) != 3 {
		return claims, fmt.Errorf("home jwt is invalid")
	}
	payload, errDecode := decodeJWTPart(parts[1])
	if errDecode != nil {
		return claims, errDecode
	}
	if errUnmarshal := json.Unmarshal(payload, &claims); errUnmarshal != nil {
		return claims, errUnmarshal
	}
	if strings.TrimSpace(claims.CertificateID) == "" {
		return claims, fmt.Errorf("home jwt certificate_id is required")
	}
	if strings.TrimSpace(claims.ClusterID) == "" {
		return claims, fmt.Errorf("home jwt cluster_id is required")
	}
	if normalizeFingerprint(claims.CAFingerprint) == "" {
		return claims, fmt.Errorf("home jwt ca_fingerprint is required")
	}
	if strings.TrimSpace(claims.EnrollmentSecret) == "" {
		return claims, fmt.Errorf("home jwt enrollment_secret is required")
	}
	if strings.TrimSpace(claims.IP) == "" || claims.Port <= 0 {
		return claims, fmt.Errorf("home jwt target address is invalid")
	}
	return claims, nil
}

func decodeJWTPart(part string) ([]byte, error) {
	if decoded, errDecode := base64.RawURLEncoding.DecodeString(part); errDecode == nil {
		return decoded, nil
	}
	return base64.URLEncoding.DecodeString(part)

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Obtain a new enrollment JWT from the coordinator that includes cluster_id
  2. If self-minting, add the claim: "cluster_id": "<your-cluster>"
  3. Inspect part 2 of the token (base64url-decode) to verify all required claims: certificate_id, cluster_id, ca_fingerprint, enrollment_secret, ip, port

Example fix

// before (JWT payload)
{"certificate_id":"client-123","ca_fingerprint":"AA:BB"}

// after (JWT payload)
{"certificate_id":"client-123","cluster_id":"home-1","ca_fingerprint":"AA:BB"}
Defensive patterns

Strategy: validation

Validate before calling

func requiredClaimsPresent(rawJWT string) error {
    parts := strings.Split(strings.TrimSpace(rawJWT), ".")
    if len(parts) != 3 {
        return fmt.Errorf("not a compact JWT")
    }
    payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
    for _, claim := range []string{"certificate_id", "cluster_id", "ca_fingerprint", "enrollment_secret"} {
        if !hasClaim(payload, claim) {
            return fmt.Errorf("missing claim %s", claim)
        }
    }
    return nil
}

Prevention

When it happens

Trigger: The enrollment JWT decodes fine but its payload has no cluster_id (or only whitespace). Fires during home enrollment startup before any network activity.

Common situations: Coordinator bug or version mismatch minting incomplete tokens; test tokens hand-built without the claim; payload template drift.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/e3310161167ed0f9. Report an issue: GitHub.