{"record":{"id":"cb1fade112e5eaef","repo":"semaphoreui/semaphore","slug":"jwt-persist-re-encrypted-signing-key-w","errorCode":null,"errorMessage":"jwt: persist re-encrypted signing key: %w","messagePattern":"jwt: persist re-encrypted signing key: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"util/jwt.go","lineNumber":150,"sourceCode":"\tif err != nil && oldKey != \"\" {\n\t\t_, ct, _ := parseEnvelope(stored)\n\t\tpemBytes, err = DecryptAESGCM(ct, oldKey)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"jwt: decrypt signing key for rekey: %w\", err)\n\t}\n\n\treEncrypted, err := Config.EncryptOption(pemBytes)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"jwt: re-encrypt signing key: %w\", err)\n\t}\n\n\tif reEncrypted == stored {\n\t\treturn nil\n\t}\n\n\tif err := store.SetOption(jwtSigningKeyOption, reEncrypted); err != nil {\n\t\treturn fmt.Errorf(\"jwt: persist re-encrypted signing key: %w\", err)\n\t}\n\treturn nil\n}\n\n// encryptJWTKey encrypts pemBytes using the option keyring primary key (which\n// falls back to the access key when no separate option key is configured).\nfunc encryptJWTKey(pemBytes []byte) (string, error) {\n\treturn Config.EncryptOption(pemBytes)\n}\n\n// decryptJWTKey reverses encryptJWTKey. It tries the option keyring and then\n// the access keyring as a migration fallback, so a key written before the\n// option/access split (encrypted with the access key) still loads.\nfunc decryptJWTKey(stored string) ([]byte, error) {\n\tplaintext, err := Config.DecryptOption(stored)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"jwt: decrypt signing key: %w\", err)\n\t}","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/util/jwt.go#L132-L168","documentation":"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.","triggerScenarios":"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.","commonSituations":"DB disk full or in read-only mode during `vault rekey`; permissions revoked on the options table; transient network failure between app and database.","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."],"exampleFix":"// before\nif err := store.SetOption(util.JWTSigningKeyOption, reEncrypted); err != nil {\n    return fmt.Errorf(\"jwt: persist re-encrypted signing key: %w\", err)\n}\n// after (caller side)\nif err := util.RekeyJWTSigningKey(store, oldKey); err != nil {\n    if errors.Is(err, context.DeadlineExceeded) {\n        // retry: operation is idempotent\n        err = util.RekeyJWTSigningKey(store, oldKey)\n    }\n    return err\n}","handlingStrategy":"retry","validationCode":"if err := store.GetOption(util.JWTSigningKeyOption); err != nil {\n    return fmt.Errorf(\"store not writable/available: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"err := util.RekeyJWTSigningKey(store, oldKey)\nfor i := 0; err != nil && strings.Contains(err.Error(), \"persist re-encrypted signing key\") && i < 3; i++ {\n    time.Sleep(time.Duration(1<<i) * time.Second) // idempotent, safe to retry\n    err = util.RekeyJWTSigningKey(store, oldKey)\n}","preventionTips":["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."],"tags":["jwt","option-store","write-failure","rekey"],"backgroundTag":"database-write-failed","analyzedSha":"1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa","analyzedAt":"2026-09-07T11:00:33.293Z","contentChangedAt":"2026-09-07T11:00:33.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}