semaphoreui/semaphore · error

jwt: failed to initialise signer

Error message

jwt: failed to initialise signer: %w

What it means

Returned by InitJWTSignerFromStore in util/jwt.go when a signing-key PEM was obtained (loaded or newly generated) but jwt.NewECDSASignerFromPEM rejects it — for example the PEM block is missing, the key is not ECDSA, or the curve is not P-256 (the signer's own parsePrivateKey errors). The %w wraps the signer construction error, distinguishing a bad key from the key-loading failure handled one branch above.

Solutions

  1. Inspect the wrapped signer error — 'no PEM block', 'key is not ECDSA', or 'unsupported curve' each point to a different fix
  2. Regenerate the stored key as an ECDSA P-256 PEM and update the jwt_signing_key option (or use the rekey flow)
  3. If the stored value was manually edited, restore the exact PEM produced by the key-generation path
  4. Confirm JWT config options (issuer/TTLs) parse correctly so the signer options themselves are not the cause
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at util/jwt.go:39 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/70d9ec971ceb0fbd. Report an issue: GitHub.

Appendix: source

Thrown at util/jwt.go:39

const jwtSigningKeyOption = "jwt_signing_key"

// InitJWTSignerFromStore initialises the global JWT signer.
// It must be called once after the db.Store has been opened and after ConfigInit has run.
func InitJWTSignerFromStore(store OptionStore) (singer jwt.Signer, err error) {
	if !Config.JWT.Enabled {
		return
	}

	opts := jwtSignerOptions()

	pemBytes, err := loadOrCreateJWTKey(store)
	if err != nil {
		return nil, fmt.Errorf("jwt: could not load or create signing key: %w", err)
	}

	signer, err := jwt.NewECDSASignerFromPEM(pemBytes, opts)
	if err != nil {
		return nil, fmt.Errorf("jwt: failed to initialise signer: %w", err)
	}

	return signer, nil
}

// jwtSignerOptions builds SignerOptions from the current Config.
func jwtSignerOptions() jwt.SignerOptions {
	ttl := time.Hour
	if Config.JWT.DefaultTTL != "" {
		if parsed, err := time.ParseDuration(Config.JWT.DefaultTTL); err == nil {
			ttl = parsed
		} else {
			fmt.Fprintf(os.Stderr, "jwt: invalid jwt_default_ttl %q, falling back to 1h: %v\n", Config.JWT.DefaultTTL, err)
		}
	}

	maxTTL := 24 * time.Hour
	if Config.JWT.MaxTTL != "" {

View on GitHub (pinned to 1774ccb71a)