🚨 WDYBTW: Check your JWKS handling now: Ghost rotates signing keys

What did you break this week, week of 2026-09-24

🚨 WDYBTW: Check your JWKS handling now: Ghost rotates signing keys
🆕
Welcome to "What did you break this week?", an AI-generated report of possibly-breaking changes in Ghost, created by an AI read of PRs, both merged and open, since you might want to know what's going to break your stuff next week, too.

Useful? Time will tell. Feedback welcome. Don't blame me or the AI if something breaks and we didn't tell you, or something didn't break and we told you. It's an experiment.

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

🚧 This report was generated at the same time as the inaugural report for 2026-09-17 . Normal weekly reporting will include a selection of open PRs that may be of concern. --Cathy

🚨 WDYBTW: Check comment widgets reading post.excerpt from Members API
What did you break this week, week of 2026-09-17

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