{"record":{"id":"18c0e1a8f1876d3f","repo":"semaphoreui/semaphore","slug":"jwt-decrypt-signing-key-for-rekey-w","errorCode":null,"errorMessage":"jwt: decrypt signing key for rekey: %w","messagePattern":"jwt: decrypt signing key for rekey: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"util/jwt.go","lineNumber":137,"sourceCode":"// option key (stamping its id). It decrypts via the option keyset, the access\n// fallback, and — when supplied — oldKey (the legacy `vault rekey --old-key`\n// flow). No-op when no key is stored or the ciphertext is unchanged.\nfunc RekeyJWTSigningKey(store OptionStore, oldKey string) error {\n\tstored, err := store.GetOption(jwtSigningKeyOption)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read jwt signing key option: %w\", err)\n\t}\n\tif stored == \"\" {\n\t\treturn nil\n\t}\n\n\tpemBytes, err := Config.DecryptOption(stored)\n\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","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/util/jwt.go#L119-L155","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nerr := util.RekeyJWTSigningKey(store, \"\") // legacy-encrypted value, no old key\n// after\nerr := util.RekeyJWTSigningKey(store, oldKey) // supply legacy key from `vault rekey --old-key`","handlingStrategy":"fallback","validationCode":"// verify at least one key can decrypt before rekeying\nif _, err := util.Config.DecryptOption(stored); err != nil && oldKey == \"\" {\n    return errors.New(\"cannot decrypt stored key: supply --old-key for legacy values\")\n}","typeGuard":null,"tryCatchPattern":"if err := util.RekeyJWTSigningKey(store, oldKey); err != nil {\n    if strings.Contains(err.Error(), \"decrypt signing key for rekey\") {\n        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)\n    }\n    return err\n}","preventionTips":["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."],"tags":["jwt","decryption","aes-gcm","keyring"],"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"}