Skip to content

Encrypted Endpoint.secret_key has no documented APP_KEY-rotation runbook — a naive rotation permanently bricks every webhook signature #168

Description

@morcen

What

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.

Where

  • app/Models/Endpoint.php:33'secret_key' => 'encrypted'
  • config/app.php:102-104 — the (undocumented, from a user's perspective) previous_keys support
  • app/Jobs/SendWebhook.php:127catch (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.
  • Consider adding an artisan command for this re-encryption sweep, analogous to the existing secret-key encryption backfill migration referenced in issue Endpoint secret-key encryption backfill migration has no test coverage and can't be exercised by the existing test suite #142.
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationsecurity

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions