Issue Summary
Self-hosted Ghost instances running in production cannot deliver webhooks to explicitly trusted services on the same private Docker network.
Ghost rejects webhook targets whose hostnames resolve to private IP addresses:
URL_PRIVATE_INVALID: URL resolves to a non-permitted private IP block
Please consider supporting an configuration controlled allowlist for exact private webhook hostnames and ports.
Steps to Reproduce
- Create a private Docker network:
docker network create ghost-network
- Start a MySQL 8.4 container:
docker run -d \
--name ghost-mysql \
--network ghost-network \
-e MYSQL_ROOT_PASSWORD=ghost-root-password \
-e MYSQL_DATABASE=ghost \
-e MYSQL_USER=ghost \
-e MYSQL_PASSWORD=ghost-password \
mysql:8.4
- Start a webhook receiver on the same network:
docker run -d \
--name webhook-receiver \
--network ghost-network \
example/webhook-receiver:latest
The receiver listens on port 3000 and exposes: POST /api/webhooks/posts
- Start Ghost in production mode on the same network:
docker run -d \
--name ghost \
--network ghost-network \
-p 2368:2368 \
-e NODE_ENV=production \
-e url=http://localhost:2368 \
-e database__client=mysql \
-e database__connection__host=ghost-mysql \
-e database__connection__port=3306 \
-e database__connection__user=ghost \
-e database__connection__password=ghost-password \
-e database__connection__database=ghost \
ghost:6.63.0
- Verify that Docker DNS resolves the receiver from the Ghost container:
docker exec ghost node -e \
'require("dns").lookup("webhook-receiver", console.log)'
The hostname resolves to a private Docker network address such as:
null 172.18.0.3 4
- Verify that the receiver is reachable from the Ghost container:
docker exec ghost node -e '
const http = require("http");
const request = http.request(
"http://webhook-receiver:3000/api/webhooks/posts",
{
method: "POST",
headers: {
"Content-Type": "application/json"
}
},
response => {
console.log("Status:", response.statusCode);
response.resume();
}
);
request.on("error", console.error);
request.end("{}");
'
-
In Ghost Admin, create a custom integration and configure a webhook with this target URL:
http://webhook-receiver:3000/api/webhooks/posts
-
Trigger the webhook by performing the configured event, such as editing or publishing a post.
-
Observe that Ghost rejects the destination because it resolves to a private Docker network address.
The validation occurs in errorIfHostnameResolvesToPrivateIp():
https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/lib/request-external.js
Ghost Version
6.63.0
Node.js Version
22.23.2
How did you install Ghost?
Official ghost:6.63.0 Docker image running on a self-hosted Docker host.
Database type
MySQL 8
Browser & OS version
Not applicable.
Relevant log / error output
Triggering webhook for "post.edited" with url "http://webhook-receiver:3000/api/webhooks/posts"
ERROR [WEBHOOK_DELIVERY_FAILURE]
url=http://webhook-receiver:3000/api/webhooks/posts
status=none
error_code=URL_PRIVATE_INVALID
message=URL resolves to a non-permitted private IP block
RequestError: URL resolves to a non-permitted private IP block
at Request._beforeError
at Request.flush
at errorIfHostnameResolvesToPrivateIp
at async Request._makeRequest
Code of Conduct
Issue Summary
Self-hosted Ghost instances running in production cannot deliver webhooks to explicitly trusted services on the same private Docker network.
Ghost rejects webhook targets whose hostnames resolve to private IP addresses:
URL_PRIVATE_INVALID: URL resolves to a non-permitted private IP blockPlease consider supporting an configuration controlled allowlist for exact private webhook hostnames and ports.
Steps to Reproduce
The receiver listens on port
3000and exposes:POST /api/webhooks/postsIn Ghost Admin, create a custom integration and configure a webhook with this target URL:
http://webhook-receiver:3000/api/webhooks/postsTrigger the webhook by performing the configured event, such as editing or publishing a post.
Observe that Ghost rejects the destination because it resolves to a private Docker network address.
The validation occurs in
errorIfHostnameResolvesToPrivateIp():https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/lib/request-external.js
Ghost Version
6.63.0
Node.js Version
22.23.2
How did you install Ghost?
Official
ghost:6.63.0Docker image running on a self-hosted Docker host.Database type
MySQL 8
Browser & OS version
Not applicable.
Relevant log / error output
Code of Conduct