๐จ WDYBTW: Check your JWKS handling now: Ghost rotates signing keys
What did you break this week, week of 2026-09-24
The most consequential change this week is merged: Ghost now rotates member and staff RSA signing keys under 2048 bits. It publishes the new key in the JWKS, switches to signing with it under a new kid 48 hours later, and keeps the old key published for only 2 hours after the switch. Any service that verifies Ghost member, entitlement or staff identity tokens with a pinned PEM, an indefinitely cached JWKS, or keys[0] will start rejecting tokens, so select keys by kid and refetch on an unknown kid. Other merged changes are worth checking too: the oEmbed endpoint now returns Ghost-hosted thumbnails and a newer provider list, embed previews load from public.ghostembeds.com by default, and member webhooks gain custom field data. The labs-gated automations APIs also continue to expand.
Contents
Merged
Key validation & JWT
Added rotation of the site signing keys ยท Merged 2026-09-30 ยท acburdine
Impact: high ยท Confidence: high ยท ๐จ Breaking
Ghost now rotates any member or staff RSA signing key under 2048 bits. A new key appears in the JWKS, and 48h later Ghost starts signing with it under a new kid. The old key stays published for only 2h after the switch. Verifiers that pinned a key or cached the JWKS indefinitely will start rejecting tokens.
- What changed: New signing-keys service. members_* and ghost_* key rows rotate via *_next_private_key and *_previous_public_key. JWT kid changes at the switch for member identity/entitlement tokens (RS512) and staff identity tokens (RS256). JWKS may list 2-3 keys.
- Who is affected: Any external service that verifies Ghost member tokens (members session/identity tokens, entitlement tokens) or staff identity tokens. That includes ActivityPub-style services, billing token exchanges, and self-hosted integrations, especially ones that copied a public PEM or cache the JWKS with no expiry. Only sites whose existing key is under 2048 bits rotate automatically, but rotate(purpose) can trigger rotation for any key.
- What to check: Select the verification key by the token header kid from the fetched JWKS rather than keys[0] or a single cached key. Refetch the JWKS when a kid is unknown, and respect a cache of at most 24h. Remove any hard-coded members_public_key or ghost_public_key PEM. Test that a token signed with a new kid verifies, and that a retired kid is handled.
- Medium, Endpoint response shapes: Both JWKS endpoints now return the same shape: possibly several keys, ordered with the current signing key first. The members JWKS keys now include "use": "sig".
Medium Impact Changes
- Prevented changing automation slug via automated email API ยท Merged 2026-09-29 ยท Permissions & endpoint behaviour ยท The Admin API automated emails edit endpoint now rejects requests that try to change an automation's slug with a ValidationError (HTTP 422). Requests that omit slug or send the unchanged value still work.
- Required automations to be enabled to hit `GET /automations` endpoint ยท Merged 2026-09-28 ยท Permissions & endpoint behaviour ยท The Admin API browse endpoint for automations now returns 404 Not Found when the 'automations' labs flag is disabled. Before this change it returned data whether or not the flag was set.
- Changed member create to store the custom fields it was given ยท Merged 2026-09-30 ยท Permissions & endpoint behaviour, Webhook payloads ยท POST /ghost/api/admin/members/ now accepts metafields.custom on create and stores them in the same transaction, instead of rejecting with a 422. The feature is behind a private flag.
- Fixed integrations being refused the custom field definitions ยท Merged 2026-09-29 ยท Permissions & endpoint behaviour ยท Admin API integration keys can again call the member custom field definition routes under /ghost/api/admin/members/metafields/. Before this fix every such request was refused with a permission error. Staff session and staff token behaviour is unchanged.
- Added member custom fields to the member webhook payload ยท Merged 2026-09-30 ยท Endpoint response shapes, Webhook payloads ยท The members metafields key is now omitted whenever a member has no values. Previously it was an empty object whenever the site had defined any readable field. This applies to Admin API member reads and browses and to the Portal/Members API member response.
- ๐ Fixed crashes when a subscription's member or paid tier is missing ยท Merged 2026-09-28 ยท Permissions & endpoint behaviour, Web & email rendering ยท Subscription operations on a member that cannot be found now return a NotFoundError (404) instead of crashing with a TypeError (500). This applies when the member id or email is unknown.
- Added email verification for newsletters sent to removed members ยท Merged 2026-09-25 ยท Permissions & endpoint behaviour, Webhook payloads ยท On sites with removedRecipientsThreshold configured, publishing or sending a post as a newsletter can now fail with 403 EMAIL_VERIFICATION_NEEDED. It triggers when the site has emailed many addresses that no longer belong to members in the last 30 days.
- Added sorting and pagination to automation runs ยท Merged 2026-09-30 ยท Endpoint response shapes, Permissions & endpoint behaviour ยท The Admin API automation_runs browse endpoint now returns whatever automationsApi.browseRuns produces instead of wrapping it as { data }. Per the description, results are paginated at 50 runs per page with a cursor. Clients that expected the full run list in one response will only get the first page.
- ๐จ Improved display of YouTube video images in emails ยท Merged 2026-09-30 ยท Endpoint response shapes, Web & email rendering ยท The Admin API oEmbed endpoint now returns a Ghost-hosted thumbnail_url and keeps the provider's URL in a new thumbnail_url_original field. For YouTube, the thumbnail dimensions now reflect the 1280x720 maxresdefault image. The diff only shows thumbnail_url being passed through the oembed URL mapper; the storage and YouTube logic is from the description.
- Updated @extractus/oembed-extractor to v6 ยท Merged 2026-09-25 ยท Endpoint response shapes, Other, Web & email rendering ยท The Admin API oEmbed lookup now uses a newer provider list and validates provider responses. URLs from about 47 dropped providers come back as bookmark data instead of embeds, and malformed provider payloads now fail with an error.
- Fixed a member's own custom field changes never reaching webhooks ยท Merged 2026-09-30 ยท Webhook payloads ยท Custom field edits a member makes in Portal now fire member.edited, as staff edits already did. When a member edits a field and their name together, previous now includes the old custom field values. The feature is behind a private flag.
- Fixed member webhooks sending old custom field values after a mixed edit ยท Merged 2026-09-30 ยท Webhook payloads, Other ยท For Admin member edits that include custom fields, member.edited now fires once the member and custom field writes commit together. current.metafields.custom in the payload now carries the new values instead of the old ones. Custom fields are behind a private flag.
- Changed embed previews to use the public embed renderer by default ยท Merged 2026-09-25 ยท Other, Endpoint response shapes ยท By default, embed card previews in the Admin editor now load from the external origin public.ghostembeds.com instead of the Admin's own origin. The config change itself is not in the shown diff; this is inferred from the description and the snapshot.
Low Impact Changes
- Added settings for rotating the site signing keys ยท Merged 2026-09-29 ยท Key validation & JWT ยท This PR adds four empty core settings rows that a later PR in the stack will use to rotate the member and staff signing keys to 2048-bit RSA. Nothing changes yet: signing, JWKS output and token verification behave as before.
- Added automation run listing and status filtering ยท Merged 2026-09-30 ยท Permissions & endpoint behaviour, Endpoint response shapes ยท New Admin API endpoint GET /ghost/api/admin/automations/:id/runs lists an automation's runs, with status and entry-date filters. It is gated by the existing automations read permission.
- Disallowed creating more than 20 automations ยท Merged 2026-09-30 ยท Permissions & endpoint behaviour ยท The Admin API automations add endpoint now rejects creation once 20 automations exist, throwing a HostLimitError with code AUTOMATION_LIMIT_REACHED. The endpoint is still unimplemented (returns 501) and labs-gated, so practical impact is nil today.
- Added date-range filtering to automation performance statistics ยท Merged 2026-09-30 ยท Permissions & endpoint behaviour, Endpoint response shapes ยท The Admin API automation performance stats read endpoint now accepts optional date_from and date_to query options alongside timezone. Requests without them keep returning all-time stats.
- Gave admins "add automation" permissions ยท Merged 2026-09-29 ยท Permissions & endpoint behaviour ยท A new Admin API route, POST /ghost/api/admin/automations, is registered but is only a stub: it always returns 501 NOT_IMPLEMENTED. A migration adds an 'add' permission on 'automation' for the Administrator and Admin Integration roles.
- Added automation entry history and status statistics ยท Merged 2026-09-30 ยท Permissions & endpoint behaviour ยท Adds a new read-only Admin API endpoint returning all-time entry history and status counts for a single automation. It uses mw.authAdminApi and reuses the existing automations.read permission.
- Added React auth screens behind the authReact flag ยท Merged 2026-09-29 ยท Endpoint response shapes, Other ยท The public site payload now includes a boolean authReact field reflecting the private Labs flag. It appears in both the Admin API /site/ and Members API site responses.
- Added automation description to read/browse APIs ยท Merged 2026-09-29 ยท Endpoint response shapes ยท Admin API automations browse and read responses now include a string `description` field on each automation object. The change is additive; no fields were removed or renamed.
- Added the custom fields a deleted member had to member.deleted ยท Merged 2026-09-30 ยท Webhook payloads, Other ยท member.deleted webhook payloads can now include the deleted member's custom fields under member.previous.metafields, matching member.edited. The key is omitted when the member had no custom fields, and the feature is behind a private flag.
- Added the custom fields a member had before an edit to member.edited ยท Merged 2026-09-30 ยท Webhook payloads, Other ยท member.edited webhook payloads now include previous.metafields (the member's full set of custom fields before the edit) when an Admin API edit changed any custom field. This is additive and sits behind a private flag.
- Changed webhooks to build each event's payload once, and log when it fails ยท Merged 2026-09-30 ยท Webhook payloads ยท Webhook payload shape and signing are unchanged. Ghost now builds each event's payload once and reuses it for every subscriber, and logs an error and sends nothing to anyone if building it fails.
- Added user status to the SSO adapter repository ยท Merged 2026-09-30 ยท Other ยท The user repository that Ghost passes to SSO adapters now includes the staff user's status in both lookups. This is additive: existing adapters that read only id and email keep working.
- Updated miscellaneous dependencies ยท Merged 2026-09-26 ยท Key validation & JWT, Other ยท The Tinybird service's jwt.verify call now passes an explicit algorithms option, matching Ghost's other jwt.verify call sites. This appears to cover Ghost's own Tinybird analytics tokens, not Admin API keys or member JWTs.
- Added per-tier automation trigger migration ยท Merged 2026-09-29 ยท Endpoint response shapes ยท Schema-only addition for automations: a new nullable automations.trigger_tier_scope column and a new automation_trigger_tiers join table. The legacy automated_emails Admin API sets the new column internally, and nothing in the diff adds it to the response.
- ๐ Fixed importing posts with empty mobiledoc content ยท Merged 2026-09-28 ยท Other ยท Posts saved with an empty mobiledoc (sections: []) that the Post model converts to lexical now get Ghost's blank lexical document instead of failing with 'Invalid lexical structure'. This affects site content imports and possibly other paths that hit the same conversion branch.
- Bumped TryGhost/framework dependencies to latest ยท Merged 2026-09-30 ยท Other ยท Framework dependencies were bumped to their latest patch versions, plus a minor bump for @tryghost/errors to 3.4.0. No diff was available, so no integration-facing change could be confirmed. Any effect would most likely show up in error response bodies or security helpers.
Run stats
| Measure | Value |
|---|---|
| Window | 2026-09-24 to 2026-10-01 |
| PRs scanned | 530 |
| Candidates | 115 |
| Triaged | 115 |
| Deep reads | 31 |
| Previously flagged, still open and unchanged | 19 |
| Triage tokens | 115 calls, 164,951 in / 6,664 out, 0 cache read / 0 cache write |
| Deep-read tokens | 31 calls, 68,876 in / 22,998 out, 54,150 cache read / 1,805 cache write |
| Intro tokens | 1 calls, 16,901 in / 308 out, 0 cache read / 0 cache write |
| Estimated cost | $0.84 |