semaphoreui/semaphore · error
jwt: persist re-encrypted signing key
Error message
jwt: persist re-encrypted signing key: %w
What it means
The final step of RekeyJWTSigningKey writes the re-encrypted ciphertext back with store.SetOption(jwtSigningKeyOption, reEncrypted). This error wraps any failure of that write; the key was already decrypted and re-encrypted, but the new ciphertext never reached the store.
Solutions
- Inspect the wrapped cause (%w) and fix the store write failure (connectivity, read-only mode, permissions).
- Re-run RekeyJWTSigningKey once the store is writable - it is idempotent and returns nil if the ciphertext is unchanged.
- Verify the stored value decrypts with the new keyset after a successful rekey.
Example fix
// before
if err := store.SetOption(util.JWTSigningKeyOption, reEncrypted); err != nil {
return fmt.Errorf("jwt: persist re-encrypted signing key: %w", err)
}
// after (caller side)
if err := util.RekeyJWTSigningKey(store, oldKey); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
// retry: operation is idempotent
err = util.RekeyJWTSigningKey(store, oldKey)
}
return err
} Defensive patterns
Strategy: retry
Validate before calling
if err := store.GetOption(util.JWTSigningKeyOption); err != nil {
return fmt.Errorf("store not writable/available: %w", err)
} Try / catch
err := util.RekeyJWTSigningKey(store, oldKey)
for i := 0; err != nil && strings.Contains(err.Error(), "persist re-encrypted signing key") && i < 3; i++ {
time.Sleep(time.Duration(1<<i) * time.Second) // idempotent, safe to retry
err = util.RekeyJWTSigningKey(store, oldKey)
} Prevention
- Ensure the database is writable (not read-only, disk not full) before running rekey.
- Retry the rekey on transient write failures - the operation is idempotent.
- Monitor DB health/alerts so write outages are fixed before maintenance windows.
When it happens
Trigger: Calling RekeyJWTSigningKey when the OptionStore rejects SetOption - database down or read-only, connection dropped mid-write, constraint violations, or a store implementation with write disabled.
Common situations: DB disk full or in read-only mode during `vault rekey`; permissions revoked on the options table; transient network failure between app and database.
Related errors
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/cb1fade112e5eaef.
Report an issue: GitHub.
Appendix: source
Thrown at util/jwt.go:150
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 (which
// falls back to the access key when no separate option key is configured).
func encryptJWTKey(pemBytes []byte) (string, error) {
return Config.EncryptOption(pemBytes)
}
// decryptJWTKey reverses encryptJWTKey. It tries the option keyring and then
// the access keyring as a migration fallback, so a key written before the
// option/access split (encrypted with the access key) still loads.
func decryptJWTKey(stored string) ([]byte, error) {
plaintext, err := Config.DecryptOption(stored)
if err != nil {
return nil, fmt.Errorf("jwt: decrypt signing key: %w", err)
}View on GitHub (pinned to 1774ccb71a)