semaphoreui/semaphore · error
jwt: decrypt signing key for rekey
Error message
jwt: decrypt signing key for rekey: %w
What it means
During a JWT signing key rekey, the stored ciphertext must be decrypted before re-encryption. RekeyJWTSigningKey first tries Config.DecryptOption (option keyset, then access-key fallback); if that fails and oldKey was supplied (legacy `vault rekey --old-key`), it also tries AES-GCM with oldKey. This error means every available key failed to decrypt the stored value.
Solutions
- Re-run with the legacy key: pass the previous value to oldKey (`vault rekey --old-key <previous-key>`) so the legacy ciphertext can be decrypted.
- Restore the keyset entry (key id in the envelope) that originally encrypted the option - check keyring configuration/env vars.
- If the plaintext is unrecoverable, delete the stored option so a fresh JWT signing key is generated on next load, accepting invalidation of issued tokens.
Example fix
// before err := util.RekeyJWTSigningKey(store, "") // legacy-encrypted value, no old key // after err := util.RekeyJWTSigningKey(store, oldKey) // supply legacy key from `vault rekey --old-key`
Defensive patterns
Strategy: fallback
Validate before calling
// verify at least one key can decrypt before rekeying
if _, err := util.Config.DecryptOption(stored); err != nil && oldKey == "" {
return errors.New("cannot decrypt stored key: supply --old-key for legacy values")
} Try / catch
if err := util.RekeyJWTSigningKey(store, oldKey); err != nil {
if strings.Contains(err.Error(), "decrypt signing key for rekey") {
return fmt.Errorf("no available key decrypts the stored signing key; re-run with the legacy --old-key or restore the original keyset: %w", err)
}
return err
} Prevention
- Always run `vault rekey --old-key <previous>` when rotating keys on legacy installations.
- Keep superseded keys in the keyset until every stored value has been rekeyed.
- Test decryption of the stored option (dry run) before removing old keys from config.
- Avoid manual edits to the options storage that can corrupt ciphertext.
When it happens
Trigger: Calling RekeyJWTSigningKey when: the option keyset and access keyring were rotated and no longer contain the encrypting key, the stored ciphertext is corrupted/truncated, or decryption fails and no oldKey was passed for a value still encrypted with the legacy key.
Common situations: Operator rekeys after rotating SEMAPHORE_OPTION_KEY / access key without the old key; legacy installation encrypted with the access key but rekey run without --old-key; ciphertext damaged by manual DB edits or failed writes.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- jwt: decrypt signing key
- jwt: re-encrypt signing key
- encryption key id not found in keyset (the key encrypting…
- internal error
- invalid JWT TTL
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/18c0e1a8f1876d3f.
Report an issue: GitHub.
Appendix: source
Thrown at util/jwt.go:137
// option key (stamping its id). It decrypts via the option keyset, the access
// fallback, and — when supplied — oldKey (the legacy `vault rekey --old-key`
// flow). No-op when no key is stored or the ciphertext is unchanged.
func RekeyJWTSigningKey(store OptionStore, oldKey string) error {
stored, err := store.GetOption(jwtSigningKeyOption)
if err != nil {
return fmt.Errorf("read jwt signing key option: %w", err)
}
if stored == "" {
return nil
}
pemBytes, err := Config.DecryptOption(stored)
if err != nil && oldKey != "" {
_, ct, _ := parseEnvelope(stored)
pemBytes, err = DecryptAESGCM(ct, oldKey)
}
if err != nil {
return fmt.Errorf("jwt: decrypt signing key for rekey: %w", err)
}
reEncrypted, err := Config.EncryptOption(pemBytes)
if err != nil {
return fmt.Errorf("jwt: re-encrypt signing key: %w", err)
}
if reEncrypted == stored {
return nil
}
if err := store.SetOption(jwtSigningKeyOption, reEncrypted); err != nil {
return fmt.Errorf("jwt: persist re-encrypted signing key: %w", err)
}
return nil
}
// encryptJWTKey encrypts pemBytes using the option keyring primary key (whichView on GitHub (pinned to 1774ccb71a)