Lock Down Self-Hosted n8n: Expose Only Webhooks

Minimize your n8n attack surface by restricting external access to webhook paths only, using NPM Access Lists and Custom Locations with a single allow all directive.

A padlocked server rack with chains and blue LED indicators, representing locking down self-hosted n8n
Minimize your attack surface — expose only what needs to be public

If you're self-hosting n8n, there's a good chance the only reason it's publicly accessible is for webhooks. The editor, admin panel, API — none of that needs to be reachable from the internet. But if you're running it behind a reverse proxy like Nginx Proxy Manager, the default setup exposes everything.

Recent n8n CVEs have been a good reminder that minimizing your attack surface matters. Here's a quick way to lock it down so only webhook endpoints are externally accessible, while the rest stays internal-only.

The Approach

The idea is simple: use NPM's built-in Access Lists to restrict the main site to internal traffic, then use Custom Locations with an explicit allow all; to punch through for just the webhook paths.

No manual allow/deny blocks in the Advanced config needed — just NPM's own tools used the way they're designed.

Step 1: Set an Access List on the Proxy Host

In your n8n proxy host's Details tab, change the Access List from "Publicly Accessible" to a restrictive ACL that only permits your internal subnets. If you don't already have one, create an Access List in NPM with your internal IP ranges — your server VLAN, trusted client VLAN, Docker networks, whatever needs UI access.

This locks down the entire site. Every path, including webhooks, will now be blocked from external traffic. That's the starting point — restrict everything, then selectively open what needs to be public.

Step 2: Add Custom Locations for Webhooks

Go to the Custom Locations tab and add two entries:

  • /webhook/ — forwarded to your n8n instance
  • /webhook-test/ — same target (optional, only if you need to test webhooks externally)

In each Custom Location's nginx config box, add a single line:

allow all;

That's it. The allow all; inside the location block overrides the server-level access restriction inherited from the Access List. Requests to /webhook/ and /webhook-test/ pass through to n8n regardless of source IP, while everything else — the editor, the API, the admin panel — stays locked behind your ACL.

Why This Works

NPM's Access Lists translate to nginx allow/deny directives at the server level. Custom Locations generate their own separate location blocks in the nginx config. When you add allow all; inside a Custom Location's config box, it takes precedence over the server-level restriction for that specific path. Nginx evaluates access rules at the most specific matching context, so the location-level directive wins.

Verify It Works

After saving, test from outside your network — pull up the root URL on your phone over cellular. You should get a 403. Then hit a webhook URL and confirm it responds normally.

If the 403 isn't working and you can still see the UI externally, your $remote_addr might be showing an internal IP — this happens if your reverse proxy sits behind another proxy layer or Cloudflare. You'd need set_real_ip_from directives to fix that, but for a straightforward NPM setup it should just work.

Why Bother?

n8n is a powerful tool with full code execution capabilities. Every CVE that drops is a reminder that exposing the entire application when you only need two URL paths is unnecessary risk. This takes five minutes and meaningfully reduces your attack surface without breaking any integrations.