You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
app/Models/Endpoint.php:33 casts secret_key as 'encrypted' (added in #114 / commit 1348d35), so every endpoint's HMAC signing secret is encrypted at rest using APP_KEY. Laravel supports graceful key rotation via APP_PREVIOUS_KEYS (config/app.php:102-104, reads env('APP_PREVIOUS_KEYS', '')) — the encrypter tries the current key first, then falls back through the previous keys list to decrypt values encrypted under an older key.
However, nothing in the docs tells an operator this mechanism exists or must be used. DEPLOYMENT.md, DOCKER.md, LARAVEL-CLOUD.md, and CLAUDE.md all describe APP_KEY generation (php artisan key:generate) but none mention APP_PREVIOUS_KEYS or warn against rotating APP_KEY in place.
config/app.php:102-104 — the (undocumented, from a user's perspective) previous_keys support
app/Jobs/SendWebhook.php:127 — catch (Exception $e) is generic; a DecryptException thrown while reading $endpoint->secret_key for the HMAC signature (SendWebhook.php:95) is caught by the same broad handler as any other delivery failure, with no distinguishing log message or status.
Why it matters
If an operator rotates APP_KEY the "obvious" way (generate a new key, replace the old value, restart) without first moving the old key into APP_PREVIOUS_KEYS, every Endpoint::secret_key becomes permanently undecryptable — there is no other copy of the plaintext secret anywhere. The failure mode is silent and total: every single delivery for every endpoint starts throwing DecryptException inside SendWebhook::handle(), gets caught by the generic catch (Exception $e), and is recorded as an ordinary delivery failure — indistinguishable from a downed customer endpoint — with retries burning through backoff for a condition retries can never fix. Recovery requires manually resetting every endpoint's secret and asking every customer to re-configure their receiver, and there's no test covering the APP_PREVIOUS_KEYS recovery path to catch a regression here.
Suggested fix
Document an explicit key-rotation runbook (e.g. in DEPLOYMENT.md): move the current APP_KEY into APP_PREVIOUS_KEYS, set the new APP_KEY, then run a one-off command that re-saves every Endpoint row (touching the encrypted cast re-encrypts it under the current key) before removing the old key from APP_PREVIOUS_KEYS.
In SendWebhook, catch DecryptException specifically and log/flag it distinctly from a normal HTTP failure so an APP_KEY misconfiguration is immediately visible instead of looking like mass endpoint downtime.
Add a test that rotates APP_KEY/APP_PREVIOUS_KEYS and asserts an Endpoint created under the old key still decrypts correctly.
What
app/Models/Endpoint.php:33castssecret_keyas'encrypted'(added in #114 / commit1348d35), so every endpoint's HMAC signing secret is encrypted at rest usingAPP_KEY. Laravel supports graceful key rotation viaAPP_PREVIOUS_KEYS(config/app.php:102-104, readsenv('APP_PREVIOUS_KEYS', '')) — the encrypter tries the current key first, then falls back through the previous keys list to decrypt values encrypted under an older key.However, nothing in the docs tells an operator this mechanism exists or must be used.
DEPLOYMENT.md,DOCKER.md,LARAVEL-CLOUD.md, andCLAUDE.mdall describeAPP_KEYgeneration (php artisan key:generate) but none mentionAPP_PREVIOUS_KEYSor warn against rotatingAPP_KEYin place.Where
app/Models/Endpoint.php:33—'secret_key' => 'encrypted'config/app.php:102-104— the (undocumented, from a user's perspective)previous_keyssupportapp/Jobs/SendWebhook.php:127—catch (Exception $e)is generic; aDecryptExceptionthrown while reading$endpoint->secret_keyfor the HMAC signature (SendWebhook.php:95) is caught by the same broad handler as any other delivery failure, with no distinguishing log message or status.Why it matters
If an operator rotates
APP_KEYthe "obvious" way (generate a new key, replace the old value, restart) without first moving the old key intoAPP_PREVIOUS_KEYS, everyEndpoint::secret_keybecomes permanently undecryptable — there is no other copy of the plaintext secret anywhere. The failure mode is silent and total: every single delivery for every endpoint starts throwingDecryptExceptioninsideSendWebhook::handle(), gets caught by the genericcatch (Exception $e), and is recorded as an ordinary delivery failure — indistinguishable from a downed customer endpoint — with retries burning through backoff for a condition retries can never fix. Recovery requires manually resetting every endpoint's secret and asking every customer to re-configure their receiver, and there's no test covering theAPP_PREVIOUS_KEYSrecovery path to catch a regression here.Suggested fix
DEPLOYMENT.md): move the currentAPP_KEYintoAPP_PREVIOUS_KEYS, set the newAPP_KEY, then run a one-off command that re-saves everyEndpointrow (touching theencryptedcast re-encrypts it under the current key) before removing the old key fromAPP_PREVIOUS_KEYS.SendWebhook, catchDecryptExceptionspecifically and log/flag it distinctly from a normal HTTP failure so anAPP_KEYmisconfiguration is immediately visible instead of looking like mass endpoint downtime.APP_KEY/APP_PREVIOUS_KEYSand asserts anEndpointcreated under the old key still decrypts correctly.