semaphoreui/semaphore · error

jwt: could not load or create signing key

Error message

jwt: could not load or create signing key: %w

What it means

Returned by InitJWTSignerFromStore in util/jwt.go when loadOrCreateJWTKey fails at any step — reading the jwt_signing_key option from the database, decrypting the stored PEM, generating a fresh key, or persisting a newly generated one. It is a wrapping guard around the whole key lifecycle: JWT signing is enabled in config, so the service cannot start without a usable signing key, and the %w carries the concrete underlying failure.

Solutions

  1. Read the wrapped error to see which stage failed: option read (DB), decrypt (keyring), generate (crypto/rand), or persist (DB write)
  2. Verify the configured encryption key is correct — a rotated or wrong key makes the stored jwt_signing_key undecryptable
  3. If the stored key is unrecoverable, use the rekey/vault flow (RekeyJWTSigningKey) to install a fresh signing key
  4. Check database connectivity/permissions if the option read or write itself failed
Defensive patterns

Strategy: fallback

When it happens

Trigger: Thrown at util/jwt.go:34 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/216fab218d210e5e. Report an issue: GitHub.

Appendix: source

Thrown at util/jwt.go:34

	SetOption(key string, value string) error
}

// jwtSigningKeyOption is the database option key under which the AES-GCM
// encrypted ECDSA P-256 private key PEM is stored.
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)

View on GitHub (pinned to 1774ccb71a)