{"record":{"id":"e55b2c294fb81aa1","repo":"semaphoreui/semaphore","slug":"encryption-key-id-q-not-found-in-keyset-the-key","errorCode":null,"errorMessage":"encryption key id %q not found in keyset (the key encrypting this value is missing)","messagePattern":"encryption key id %q not found in keyset \\(the key encrypting this value is missing\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"util/keyring.go","lineNumber":133,"sourceCode":"// fallback for values written before the option/access split.\nfunc (conf *ConfigType) DecryptOption(stored string) ([]byte, error) {\n\tks := conf.currentKeyset()\n\treturn ks.decrypt(stored, ks.legacyOptionCandidates())\n}\n\n// DecryptAccessSecretWithKey decrypts a stored secret with a single explicit key,\n// stripping any id prefix. Used by the rekey `--old-key` path.\nfunc (conf *ConfigType) DecryptAccessSecretWithKey(stored, key string) ([]byte, error) {\n\t_, ct, _ := parseEnvelope(stored)\n\treturn DecryptAESGCM(ct, key)\n}\n\nfunc (k *keyset) decrypt(stored string, legacy []string) ([]byte, error) {\n\tid, ct, hasID := parseEnvelope(stored)\n\tif hasID {\n\t\tmaterial, ok := k.byID[id]\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"encryption key id %q not found in keyset (the key encrypting this value is missing)\", id)\n\t\t}\n\t\treturn DecryptAESGCM(ct, material)\n\t}\n\treturn decryptWithKeys(ct, legacy)\n}\n\n// legacyAccessCandidates returns the keys to trial-decrypt an un-prefixed access\n// secret: the flat access key first, then every registry key. The empty\n// (passthrough) key is excluded unless there are no real keys at all, so a real\n// ciphertext is never \"successfully\" decrypted to garbage by the empty key.\nfunc (k *keyset) legacyAccessCandidates() []string {\n\treturn k.legacyCandidates(k.legacyAccess)\n}\n\n// legacyOptionCandidates is like legacyAccessCandidates but tries the flat option\n// key, then the flat access key, then the registry.\nfunc (k *keyset) legacyOptionCandidates() []string {\n\treturn k.legacyCandidates(k.legacyOption, k.legacyAccess)","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/util/keyring.go#L115-L151","documentation":"keyset.decrypt parses the stored envelope for a key id and looks the id up in its key map. When the envelope carries a key id that is absent from the keyset, decryption cannot proceed and this error is returned. It is the keyring's way of saying: this value was encrypted by a key that is no longer configured.","triggerScenarios":"Any keyring decrypt (options, JWT signing key, etc.) where the stored envelope's key id (\"v1:<id>:<ciphertext>\" form) is not present in k.byID - keys rotated/removed from config, or data copied between environments with different keysets.","commonSituations":"After key rotation without rekeying stored values; restoring a DB dump into an environment that lacks the original encryption keys; sharing a database between Semaphore instances configured with different key sets.","solutions":["Add the missing key (the id is named in the error) back to the keyset configuration so the value can be decrypted.","Run the rekey flow once the old key is restored to move values onto the current primary key.","Audit key env/config parity between environments that share a database."],"exampleFix":"// before\n// keyset: [key-2]; stored envelope: v1:key-1:...\n// -> \"encryption key id \\\"key-1\\\" not found\"\n// after\n// keyset: [key-1 (retained for decryption), key-2 (primary)]\n// then rekey values so envelopes reference key-2","handlingStrategy":"validation","validationCode":"id, _, hasID := util.ParseEnvelope(stored)\nif hasID && !keyset.HasID(id) {\n    return fmt.Errorf(\"key %q missing from keyset; restore it before decrypting\", id)\n}","typeGuard":"func hasKeyFor(stored string, ids map[string][]byte) bool {\n    id, _, hasID := util.ParseEnvelope(stored)\n    return !hasID || (ids != nil && ids[id] != nil)\n}","tryCatchPattern":"plaintext, err := keysetDecrypt(stored)\nif err != nil && strings.Contains(err.Error(), \"not found in keyset\") {\n    // extract id from message, restore that key to config, then retry\n    return fmt.Errorf(\"restore the named key to the keyset before proceeding: %w\", err)\n}","preventionTips":["Retain retired keys in the keyset until all stored envelopes have been rekeyed to the current primary.","Keep key configuration identical across environments sharing a database.","After restores/migrations, audit envelope key ids against the configured keyset.","Rekey stored values promptly after adding a new primary key."],"tags":["keyring","decryption","key-rotation","envelope"],"backgroundTag":"missing-config-key","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"}