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.

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)

Solutions

  1. Verify the token with `nats auth` tooling or decode it (base64url of header/payload) to confirm it is a user claims JWT.
  2. Regenerate the sentinel user JWT with `nsc` (nsc add user ... and export the bearer token).
  3. Check for whitespace/newline corruption when embedding the JWT in the config; use block scalars carefully.
  4. 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

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


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/893c597a3acedad9. Report an issue: GitHub.