I wanted Telegram and Discord to trigger workflows in my homelab without putting the entire n8n editor behind a public tunnel. The solution is a small nginx proxy that exposes only the webhook paths I choose.
The Problem
Running n8n at home is straightforward until an external service needs to call it. Telegram, Discord, and similar platforms require a public HTTPS endpoint, while the n8n editor should remain private.
Pointing ngrok directly at port 5678 makes every route on that n8n instance reachable through the tunnel. Authentication may still protect the editor, but the public attack surface is much larger than necessary.
The goal is narrower: publish selected production webhook routes and nothing else.
The Architecture
ngrok forwards traffic to nginx. nginx uses exact path matching and passes only approved webhook requests to n8n; every other route returns 404.
Internet
│
▼
ngrok tunnel
│
▼
nginx :8080 exact-path allowlist
│
│ POST /webhook/<random-path>
▼
n8n :5678 private Docker networkThis is defense in depth, not authentication. A random path helps reduce opportunistic scanning, but requests still need to be authenticated or verified inside the workflow.
Docker Compose
The n8n editor is bound to loopback, so it remains available locally at http://127.0.0.1:5678 but is not published on every host interface. nginx is also bound to loopback because the ngrok process runs on the same host.
services:
n8n:
image: n8nio/n8n
container_name: n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_data:/home/node/.n8n
nginx:
image: nginx:alpine
container_name: nginx-webhook-proxy
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- n8n
volumes:
n8n_data:If you administer n8n from another machine, do not change the binding to 0.0.0.0 without a plan. Prefer a trusted LAN interface, VPN, or a separate authenticated reverse proxy for the editor.
nginx config (nginx.conf)
server {
listen 8080;
server_tokens off;
client_max_body_size 1m;
# Reveal as little as possible for every route not explicitly allowed.
location / {
return 404;
}
# Exact match: similar paths and subpaths do not pass.
location = /webhook/550e8400-e29b-41d4-a716-446655440000 {
limit_except POST { deny all; }
proxy_pass http://n8n:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}Inside the Compose network, nginx resolves n8n through Docker DNS. It does not use localhost, which would refer to the nginx container itself.
Validate the configuration before restarting:
docker compose exec nginx nginx -t
docker compose restart nginxPoint ngrok at nginx
ngrok http 8080The public tunnel now terminates at nginx rather than n8n. A quick check should return 404 for the root path while the exact webhook path accepts POST requests:
curl -i https://<your-ngrok-id>.ngrok-free.app/
curl -i -X POST https://<your-ngrok-id>.ngrok-free.app/webhook/<your-path>Use an Unpredictable Path
Generate a separate random path for every webhook:
uuidgen
# or
python3 -c "import uuid; print(uuid.uuid4())"An unpredictable path is useful because it removes obvious endpoint names from casual scans. Treat it like a secret URL, but do not treat it as the only security control: URLs can appear in logs, browser history, screenshots, monitoring tools, and provider dashboards.
Verify the Sender
The proxy answers the question “which route may reach n8n?” Authentication answers the more important question: “who sent this request?”
Telegram
When registering a Telegram webhook, set secret_token. Telegram then sends the value in the X-Telegram-Bot-Api-Secret-Token header on every request. Validate that header before processing the payload.
Discord
Discord HTTP interactions must validate X-Signature-Ed25519 and X-Signature-Timestamp against the application's public key. A UUID path or shared header is not a substitute for this signature check. Invalid signatures must be rejected before the workflow performs any action.
For generic callers, n8n's Webhook node can use Basic, Header, or JWT authentication. Choose the strongest mechanism the sender supports and keep secrets in n8n credentials rather than directly in workflow nodes.
Adding Another Webhook
- Create a new production webhook in n8n with its own random path.
- Configure authentication or provider signature verification.
- Add another exact
locationblock tonginx.conf. - Run
nginx -tand reload the container. - Verify both the allowed route and a deliberately invalid route.
Do not reuse one path for unrelated integrations. Separate paths make rotation, logging, and incident response much easier.
What This Setup Does—and Does Not—Protect
| Scenario | Result |
|---|---|
Request to / or the editor routes | Rejected by nginx |
| Request to an unknown webhook path | Rejected by nginx |
GET request to an allowed path | Rejected by nginx |
| Valid path with failed provider verification | Must be rejected by the workflow |
| Valid path with valid authentication | Forwarded and processed |
| Attacker obtains the path but no valid signature or secret | Request reaches verification, then fails |
This design keeps the editor out of the public tunnel and reduces the exposed surface to a small set of routes. It does not replace patching n8n, protecting the host, rotating secrets, monitoring requests, or backing up the instance.