Fix AP inbox HTTP Signature verification failures by normalising host header
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m15s
All checks were successful
Deploy Indiekit Server / deploy (push) Successful in 1m15s
Root cause of "Failed to verify the request's HTTP Signatures" errors:
patch-ap-federation-bridge-base-url fixed Fedify URL routing (using the
canonical publicationUrl to build the Request URL) but left the "host"
header in the Headers object untouched.
Fedify's HTTP Signature verifier reads request.headers.get("host") when
reconstructing the signed-string for Cavage-style signatures. If nginx
forwards an internal Host value (e.g. "10.100.0.20") instead of the public
hostname, the reconstructed string differs from what the remote server signed
→ every inbox POST fails with a cryptographic verification error → remote
servers receive 401, exhaust retries, and stop delivering.
Fix (patch-ap-signature-host-header):
After the header-copy loop in fromExpressRequest(), override "host" with
new URL(publicationUrl).host ("blog.giersig.eu") when publicationUrl is
provided. This ensures the signed-string Fedify reconstructs matches what
Mastodon/Pleroma/etc. signed, regardless of what nginx forwards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/setup-gitea-url-rewrite.mjs",
|
||||
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-gitea-dispatch-conditional.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs",
|
||||
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||
"postinstall": "xattr -w com.apple.fileprovider.ignore#P 1 node_modules 2>/dev/null || true && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-micropub-gitea-dispatch-conditional.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs",
|
||||
"serve": "export NODE_ENV=${NODE_ENV:-production} INDIEKIT_DEBUG=${INDIEKIT_DEBUG:-0} && node scripts/preflight-production-security.mjs && node scripts/preflight-mongo-connection.mjs && node scripts/preflight-activitypub-rsa-key.mjs && node scripts/preflight-activitypub-profile-urls.mjs && node scripts/patch-lightningcss.mjs && node scripts/patch-endpoint-media-scope.mjs && node scripts/patch-endpoint-media-sharp-runtime.mjs && node scripts/patch-frontend-sharp-runtime.mjs && node scripts/patch-endpoint-files-upload-route.mjs && node scripts/patch-endpoint-files-upload-locales.mjs && node scripts/patch-endpoint-activitypub-locales.mjs && node scripts/patch-endpoint-homepage-locales.mjs && node scripts/patch-endpoint-homepage-identity-defaults.mjs && node scripts/patch-federation-unlisted-guards.mjs && node scripts/patch-endpoint-micropub-where-note-visibility.mjs && node scripts/patch-endpoint-podroll-opml-upload.mjs && node scripts/patch-frontend-serviceworker-file.mjs && node scripts/patch-endpoint-comments-locales.mjs && node scripts/patch-endpoint-posts-locales.mjs && node scripts/patch-endpoint-conversations-locales.mjs && node scripts/patch-conversations-collection-guards.mjs && node scripts/patch-indiekit-routes-rate-limits.mjs && node scripts/patch-indiekit-error-production-stack.mjs && node scripts/patch-indieauth-devmode-guard.mjs && node scripts/patch-listening-endpoint-runtime-guards.mjs && node scripts/patch-endpoint-github-changelog-categories.mjs && node scripts/patch-endpoint-github-contributions-log.mjs && node scripts/patch-store-github-error-message.mjs && node scripts/patch-store-github-update-fallback.mjs && node scripts/patch-store-github-gitea-methods.mjs && node scripts/patch-store-github-content-type.mjs && node scripts/patch-microsub-reader-ap-dispatch.mjs && node scripts/patch-microsub-compose-draft-guard.mjs && node scripts/patch-endpoint-blogroll-feeds-alias.mjs && node scripts/patch-endpoint-posts-uid-lookup.mjs && node scripts/patch-conversations-bluesky-self-filter.mjs && node scripts/patch-conversations-bluesky-cursor-fix.mjs && node scripts/patch-endpoint-micropub-source-filter.mjs && node scripts/patch-syndicate-force-checked-default.mjs && node scripts/patch-syndicate-normalize-syndication-array.mjs && node scripts/patch-endpoint-posts-fetch-diagnostic.mjs && node scripts/patch-micropub-fetch-internal-url.mjs && node scripts/patch-micropub-session-token.mjs && node scripts/patch-indiekit-endpoint-urls-protocol.mjs && node scripts/patch-webmention-sender-hentry-syntax.mjs && node scripts/patch-webmention-sender-retry.mjs && node scripts/patch-webmention-sender-livefetch.mjs && node scripts/patch-webmention-sender-empty-details.mjs && node scripts/patch-bluesky-syndicator-internal-url.mjs && node scripts/patch-bluesky-syndicator-media-type-guard.mjs && node scripts/patch-ap-skip-draft-syndication.mjs && node scripts/patch-ap-og-image.mjs && node scripts/patch-ap-webfinger-before-auth.mjs && node scripts/patch-ap-federation-bridge-base-url.mjs && node scripts/patch-ap-signature-host-header.mjs && node scripts/patch-ap-compose-default-checked.mjs && node scripts/patch-ap-mastodon-reply-threading.mjs && node scripts/patch-ap-mastodon-status-id.mjs && node scripts/patch-ap-interactions-send-guard.mjs && node scripts/patch-ap-syndicate-dedup.mjs && node scripts/patch-ap-mastodon-delete-fix.mjs && node scripts/patch-ap-status-reply-id.mjs && node scripts/patch-ap-inbox-publication-url.mjs && node scripts/patch-bluesky-syndicator-delete.mjs && node scripts/patch-micropub-delete-propagation.mjs && node scripts/patch-ap-inbox-delivery-debug.mjs && node --require ./metrics-shim.cjs node_modules/@indiekit/indiekit/bin/cli.js serve --config indiekit.config.mjs",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
103
scripts/patch-ap-signature-host-header.mjs
Normal file
103
scripts/patch-ap-signature-host-header.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Patch: fix HTTP Signature verification failures by normalising the `host`
|
||||
* header in fromExpressRequest().
|
||||
*
|
||||
* Root cause:
|
||||
* HTTP Signatures (Mastodon, Pleroma, …) include "host:" in the signed
|
||||
* components string. The signer uses the public hostname, e.g.
|
||||
* "host: blog.giersig.eu". Fedify reconstructs that string from
|
||||
* request.headers.get("host") when verifying.
|
||||
*
|
||||
* In our two-jail setup (nginx → node jail), nginx may proxy to the node
|
||||
* jail with a different Host header than the public hostname (e.g. the
|
||||
* internal IP "10.100.0.20" or "10.100.0.20:3000"). The
|
||||
* patch-ap-federation-bridge-base-url patch already fixed URL routing
|
||||
* (fromExpressRequest builds the correct canonical URL from publicationUrl),
|
||||
* but the host HEADER value in the Headers object was still copied verbatim
|
||||
* from req.headers — meaning Fedify's signature verifier reconstructed the
|
||||
* signed string with the wrong host value and the check always failed.
|
||||
*
|
||||
* Fix:
|
||||
* After copying headers from the Express request, override "host" with the
|
||||
* hostname extracted from publicationUrl when publicationUrl is provided.
|
||||
* This is safe even when nginx already forwards the correct Host header —
|
||||
* the value is identical and the set() is a no-op in that case.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const MARKER = "// [patch] ap-signature-host-header";
|
||||
|
||||
const candidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-bridge.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-bridge.js",
|
||||
];
|
||||
|
||||
// The headers-copy loop is followed immediately by the body reconstruction.
|
||||
// Insert the host-override right after the closing brace of the loop.
|
||||
const OLD_HEADERS_LOOP = ` for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) headers.append(key, v);
|
||||
} else if (typeof value === "string") {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let body;`;
|
||||
|
||||
const NEW_HEADERS_LOOP = ` for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) headers.append(key, v);
|
||||
} else if (typeof value === "string") {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise "host" to the public hostname so Fedify's HTTP Signature
|
||||
// verifier reconstructs the same signed-string the remote server created.
|
||||
// Without this, nginx may forward an internal Host (e.g. "10.100.0.20")
|
||||
// which doesn't match what the sender signed, causing every inbox POST
|
||||
// to fail with "Failed to verify the request's HTTP Signatures". ${MARKER}
|
||||
if (baseUrl) {
|
||||
try {
|
||||
const _canonicalHost = new URL(baseUrl).host; // e.g. "blog.giersig.eu"
|
||||
headers.set("host", _canonicalHost);
|
||||
} catch { /* invalid baseUrl — leave header as-is */ }
|
||||
}
|
||||
|
||||
let body;`;
|
||||
|
||||
async function exists(p) {
|
||||
try { await access(p); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
let checked = 0;
|
||||
let patched = 0;
|
||||
|
||||
for (const filePath of candidates) {
|
||||
if (!(await exists(filePath))) continue;
|
||||
checked++;
|
||||
|
||||
const source = await readFile(filePath, "utf8");
|
||||
if (source.includes(MARKER)) {
|
||||
console.log(`[postinstall] patch-ap-signature-host-header: already applied to ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!source.includes(OLD_HEADERS_LOOP)) {
|
||||
console.warn(`[postinstall] patch-ap-signature-host-header: target snippet not found in ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await writeFile(filePath, source.replace(OLD_HEADERS_LOOP, NEW_HEADERS_LOOP), "utf8");
|
||||
patched++;
|
||||
console.log(`[postinstall] Applied patch-ap-signature-host-header to ${filePath}`);
|
||||
}
|
||||
|
||||
if (checked === 0) {
|
||||
console.log("[postinstall] patch-ap-signature-host-header: no target files found");
|
||||
} else if (patched === 0) {
|
||||
console.log("[postinstall] patch-ap-signature-host-header: already up to date");
|
||||
} else {
|
||||
console.log(`[postinstall] patch-ap-signature-host-header: patched ${patched} file(s)`);
|
||||
}
|
||||
Reference in New Issue
Block a user