{"record":{"id":"a3f628d3853b3cd6","repo":"semaphoreui/semaphore","slug":"jwt-decrypt-signing-key-w","errorCode":null,"errorMessage":"jwt: decrypt signing key: %w","messagePattern":"jwt: decrypt signing key: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"util/jwt.go","lineNumber":167,"sourceCode":"\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}\n\treturn plaintext, nil\n}\n","sourceCodeStart":149,"sourceCodeEnd":171,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/util/jwt.go#L149-L171","documentation":"decryptJWTKey reverses encryptJWTKey when loading the stored JWT signing key: it decrypts via Config.DecryptOption, which tries the option keyring and then the access keyring as a migration fallback. This error means neither keyring could decrypt the stored key, so the JWT key cannot be loaded.","triggerScenarios":"Calling loadOrCreateJWTKey (server startup / JWT initialization) when the stored option ciphertext was encrypted with a key that is no longer in the option keyset or the access keyring, or the ciphertext is corrupted.","commonSituations":"Rotating SEMAPHORE_OPTION_KEY / access key without rekeying first; pointing a deployment at a database written by another instance with different keys; corrupted option row.","solutions":["Restore the keyring configuration (option/access keys) that originally encrypted the value.","Run `vault rekey --old-key <previous-key>` with the old key to migrate the stored key to the current keyset.","As a last resort delete the jwt signing key option so a new one is generated - all previously issued JWT tokens become invalid and users must re-login."],"exampleFix":"// before\n// rotate key env, restart -> decryptJWTKey fails\n// after\n// migrate before dropping the old key:\n//   semaphore vault rekey --old-key \"$OLD_OPTION_KEY\"\n// then rotate keys and restart","handlingStrategy":"fallback","validationCode":"// before startup, confirm the stored option is decryptable with current keys\nif stored, err := store.GetOption(util.JWTSigningKeyOption); err == nil && stored != \"\" {\n    if _, err := util.Config.DecryptOption(stored); err != nil {\n        // trigger recovery: rekey with old key or regenerate signing key\n    }\n}","typeGuard":null,"tryCatchPattern":"pem, err := loadOrCreateJWTKey()\nif err != nil && strings.Contains(err.Error(), \"jwt: decrypt signing key\") {\n    // recovery path: delete the option to force a fresh key (invalidates tokens)\n    _ = store.DeleteOption(util.JWTSigningKeyOption)\n    pem, err = loadOrCreateJWTKey()\n}","preventionTips":["Run `vault rekey --old-key <previous>` whenever rotating option/access keys.","Never point an instance at a database written with different keyring config without migrating keys first.","Back up the option store and key configuration together.","Accept that deleting the signing key invalidates all issued JWTs; warn users of forced re-login."],"tags":["jwt","decryption","keyring","startup"],"backgroundTag":"checksum-mismatch","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"}