nats-io/nats-server · error
default sentinel JWT not valid
Error message
default sentinel JWT not valid
What it means
validateTrustedOperators decodes the DefaultSentinel option with jwt.DecodeUserClaims; if the string is not a valid user claims JWT (malformed, wrong token type, bad signature structure), startup fails with 'default sentinel JWT not valid'. Note the error deliberately discards the underlying decode error detail.
Solutions
- Verify the token with `nats auth` tooling or decode it (base64url of header/payload) to confirm it is a user claims JWT.
- Regenerate the sentinel user JWT with `nsc` (nsc add user ... and export the bearer token).
- Check for whitespace/newline corruption when embedding the JWT in the config; use block scalars carefully.
- If the decode detail is needed, temporarily decode locally with github.com/nats-io/jwt/v2 DecodeUserClaims to see the real error.
Example fix
// before default_sentinel: "<account-jwt>" // wrong token type // after: use a user JWT (bearer) generated by nsc default_sentinel: "eyJhbGciOi...user-claims-jwt"
Defensive patterns
Strategy: validation
Validate before calling
// Go: verify the JWT decodes as user claims before configuring
_, err := jwt.DecodeUserClaims(sentinelJWT)
if err != nil {
return fmt.Errorf("default_sentinel is not a valid user claims JWT: %v", err)
} Type guard
func isUserClaimsJWT(tok string) bool {
_, err := jwt.DecodeUserClaims(tok)
return err == nil
} Prevention
- Generate the sentinel JWT with nsc rather than hand-editing.
- Validate the token decodes before pasting it into nats.conf.
- Use config tests (`nats-server -t`) on startup scripts.
- Store JWTs in files and reference them carefully to avoid truncation.
When it happens
Trigger: Setting default_sentinel in nats.conf (with operators configured) to a value that is not a decodable NATS user claims JWT — e.g. an account JWT, a truncated/pasted token, or a token produced by a different issuer/format.
Common situations: Copy/paste truncation of a long JWT in YAML/JSON config; mistaking an account or operator JWT for a user JWT; hand-editing a JWT that broke its base64 encoding.
Related errors
- default sentinel requires operators and accounts
- default sentinel must be a bearer token
- account not permitted as valid account option for auth…
- conflicting options for 'TrustedKeys' and 'TrustedOperators'
- error non operator mode account
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/893c597a3acedad9.
Report an issue: GitHub.
Appendix: source
Thrown at server/jwt.go:81
buf[i] = 'x'
}
}
// validateTrustedOperators will check that we do not have conflicts with
// assigned trusted keys and trusted operators. If operators are defined we
// will expand the trusted keys in options.
func validateTrustedOperators(o *Options) error {
if len(o.TrustedOperators) == 0 {
// if we have no operator, default sentinel shouldn't be set
if o.DefaultSentinel != _EMPTY_ {
return fmt.Errorf("default sentinel requires operators and accounts")
}
return nil
}
if o.DefaultSentinel != _EMPTY_ {
juc, err := jwt.DecodeUserClaims(o.DefaultSentinel)
if err != nil {
return fmt.Errorf("default sentinel JWT not valid")
}
if !juc.BearerToken && juc.IssuerAccount != "" && juc.HasEmptyPermissions() {
// we cannot resolve the account yet - but this looks like a scoped user
// it will be rejected at runtime if not valid
} else if !juc.BearerToken {
return fmt.Errorf("default sentinel must be a bearer token")
}
}
if o.AccountResolver == nil {
return fmt.Errorf("operators require an account resolver to be configured")
}
if len(o.Accounts) > 0 {
return fmt.Errorf("operators do not allow Accounts to be configured directly")
}
if len(o.Users) > 0 || len(o.Nkeys) > 0 {
return fmt.Errorf("operators do not allow users to be configured directly")
}View on GitHub (pinned to 3a66a489d2)