
Your n8n runs in Docker on a VPS and holds the API keys, OAuth tokens and passwords of every service your workflows touch. Whoever logs in as its owner can use those credentials in new workflows, run JavaScript in Code nodes and publish webhooks that anyone can call. This guide closes the usual openings in the order to apply them, for n8n 2.x in Docker on Ubuntu 24.04 or Debian 12.
Quick answer: Publish port 5678 as 127.0.0.1:5678:5678 and reach n8n only through an HTTPS reverse proxy, because Docker's published ports skip UFW. Create the owner account yourself before anyone else can reach the setup screen, give each person their own login and turn on two-factor authentication. Save the encryption key in your password manager and treat every copy of the .n8n volume as a secret. Keep the safe defaults of n8n 2.x, add nodes to NODES_EXCLUDE without dropping the default two, turn off the public API and community nodes you don't use, and set authentication on every Webhook node. Then run n8n audit and update at least once a month.
Step 1: Keep Port 5678 Off the Internet
n8n's Docker guide starts the container with -p 5678:5678, and its own Compose file calls that mapping "the only port that should be internet-facing". Docker publishes a mapping without an address on all host addresses, 0.0.0.0 and [::] (port publishing), and that traffic "gets diverted before it goes through the ufw firewall settings" (packet filtering). So ufw deny 5678 changes nothing, and the editor answers over plain HTTP. A Space-Node VPS for n8n and AI agents has both IPv4 and IPv6, so that means both addresses.
Publish it on the loopback address instead, so only a reverse proxy on the same server reaches it:
services:
n8n:
# keep your image, volumes and environment
ports:
- "127.0.0.1:5678:5678"
cd ~/n8n # the folder with your compose file
sudo docker compose up -d
sudo docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker compose up recreates a container whose configuration changed, "preserving mounted volumes". The PORTS column should now show 127.0.0.1:5678->5678/tcp. Docker warns that before version 28.0.0, hosts on the same network segment could still reach ports published to localhost; our Docker and UFW guide shows how to check your version and test from outside.
Step 2: Claim the Owner Account First
A new n8n instance opens on a signup screen for the owner account (user management), and n8n's owner controller accepts that setup request without a login until an owner exists. Whoever reaches a fresh instance first becomes its owner. So create the account before any proxy forwards traffic to n8n, through an SSH tunnel from your computer:
ssh -N -L 5678:127.0.0.1:5678 user@203.0.113.10
-L forwards your local port 5678 to the server's loopback address and -N runs no remote command (ssh manual). Open http://localhost:5678 in Chrome or Firefox. Browsers accept n8n's secure cookie on localhost (MDN), except Safari for now (WebKit bug 232088). Deploying with scripts? From n8n 2.17.0, N8N_INSTANCE_OWNER_MANAGED_BY_ENV=true with the N8N_INSTANCE_OWNER_* variables creates the owner at startup, with the password as a bcrypt hash.
Then give every person their own account under Settings > Users > Invite instead of sharing the owner login. Without a paid licence you have two roles, Owner and Member; on self-hosted n8n, Admin and Chat user need Enterprise (users and access). Invites work without SMTP, as links you copy, but then users can't reset a forgotten password.
Step 3: Serve the Editor Over HTTPS Only
Now put a reverse proxy in front: n8n's SSL page recommends one, which "should also take care of certificate renewals". The login cookie is HTTPS only already: N8N_SECURE_COOKIE defaults to true (security variables), so leave it on instead of switching it off to make http://your-ip:5678 work. Our n8n reverse proxy guide has complete Nginx and Caddy configurations, with N8N_WEBHOOK_URL and N8N_PROXY_HOPS=1.
Step 4: Turn On Two-Factor Authentication
n8n supports 2FA with an authenticator app, and N8N_MFA_ENABLED is on by default. Each user turns it on under Settings > Personal > Enable 2FA: scan the QR code, enter a code and save the recovery codes (2FA docs). Making it compulsory for everyone under Settings > Security needs a Business or Enterprise licence on self-hosted n8n (security policies). Without one, ask every user to switch it on.
Step 5: Guard the Encryption Key
n8n encrypts saved credentials with a key it generates on first launch and stores in the config file of the .n8n folder (encryption key). Its instance settings code writes the key to that file even when you supply it with N8N_ENCRYPTION_KEY, with file mode 600. With SQLite, n8n's default database, that one volume holds both the credentials and the key that opens them, so treat every copy of it as a secret. Read the key:
sudo docker compose exec n8n cat /home/node/.n8n/config
Save the encryptionKey value in your password manager: without it, a restored database can't decrypt any credential (n8n backup guide). If you set the key as a variable, which queue mode workers need, keep it in the .env file next to your compose file (n8n's Compose file loads it with env_file: .env), not in the compose file itself, and make that file readable by its owner only:
sudo chmod 600 .env
For a new instance, openssl rand -hex 32 makes a key you can set before the first start (openssl rand). Never change the key of an existing instance: when the file and the variable differ, n8n stops with Mismatching encryption keys.
Step 6: Limit What Workflows Can Run
n8n 2.0 changed several risky defaults to safe ones (2.0 breaking changes). Check that your compose file and .env don't switch them back:
| Variable | Default in 2.45.7 | What to do |
|---|---|---|
NODES_EXCLUDE | Blocks Execute Command and Local File Trigger | Keep both, add nodes you don't use |
N8N_BLOCK_ENV_ACCESS_IN_NODE | true: expressions and Code nodes can't read environment variables | Leave it |
N8N_RESTRICT_FILE_ACCESS_TO | ~/.n8n-files | Never set it empty |
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES | true: file nodes can't touch .n8n | Leave it |
N8N_RUNNERS_INSECURE_MODE | false | Leave it |
N8N_COMMUNITY_PACKAGES_ENABLED | true | false if you install none |
N8N_PUBLIC_API_DISABLED | false | true if you don't use the API |
N8N_SSRF_PROTECTION_ENABLED | false | true |
NODES_EXCLUDE replaces the default list instead of adding to it (nodes config), so a list without the default two turns Execute Command back on. To also block Read/Write Files from Disk, which n8n suggests for untrusted users (block nodes), add this to the n8n service and run sudo docker compose up -d:
environment:
- NODES_EXCLUDE=["n8n-nodes-base.executeCommand","n8n-nodes-base.localFileTrigger","n8n-nodes-base.readWriteFile"]
- N8N_PUBLIC_API_DISABLED=true
- N8N_COMMUNITY_PACKAGES_ENABLED=false
- N8N_SSRF_PROTECTION_ENABLED=true
n8n's variable table still lists false for N8N_BLOCK_ENV_ACCESS_IN_NODE, but the 2.0 notes and the source block access unless you set it to false. Community nodes "have full access to the machine that n8n runs on" (risks). SSRF protection, available from 2.12.0, stops nodes such as HTTP Request from calling loopback, private and link-local addresses, including cloud metadata endpoints; if a workflow must reach another container on your Docker network, allow it with N8N_SSRF_ALLOWED_HOSTNAMES. For Code nodes, n8n advises external task runners in their own container, as its withPostgres example sets up.
Step 7: Put Authentication on Every Webhook
A production webhook URL starts your workflow for anyone who has it. n8n's audit says it plainly: "Every unprotected webhook allows your workflow to be called by any third party who knows the webhook URL." The random path n8n generates makes a URL hard to guess, not secret. In the Webhook node, set Authentication to one of these (Webhook node):
- Header auth: the caller must send a header with the name and value you set. Use a long random value.
- Basic auth or JWT auth, when the sender supports those (webhook credentials).
- n8n User Auth (OAuth2): only logged-in users of your instance can call it.
For senders with fixed addresses, add the node's IP(s) Allowlist option; other addresses get a 403. Behind a proxy it needs N8N_PROXY_HOPS, or every request seems to come from the proxy, as our reverse proxy guide explains.
Step 8: Keep the Editor Private (Optional)
With a fixed IP address or a VPN, you can let the internet reach only n8n's webhook paths and keep the editor, login and REST API to yourself. For Nginx, move the proxy lines from the location block in our reverse proxy guide up to the server block, which both locations then inherit (proxy module), and split the paths:
server {
# listen, server_name and the certbot lines stay as they are
client_max_body_size 16m;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_buffering off;
location ~ ^/(webhook|webhook-test|webhook-waiting)/ {
proxy_pass http://127.0.0.1:5678;
}
location / {
allow 198.51.100.7;
deny all;
proxy_pass http://127.0.0.1:5678;
}
}
Run sudo nginx -t && sudo systemctl reload nginx. Nginx checks regular expression locations after the longest prefix and uses the first that matches (location), so webhook paths take the first block. Everything else gets a 403 unless it comes from an allowed address (access module); replace 198.51.100.7 with yours and add an allow line for each other one, IPv6 included. Serving n8n forms or an MCP Server Trigger too? Add form|form-test|form-waiting or mcp|mcp-test to the list (endpoint names). OAuth sign-ins for credentials keep working, because the callback comes from your own browser.
With Caddy, use this site block and run sudo systemctl reload caddy:
n8n.example.com {
@private {
not path /webhook/* /webhook-test/* /webhook-waiting/*
not remote_ip 198.51.100.7
}
respond @private 403
reverse_proxy 127.0.0.1:5678
}
All matchers in a named set must match (matchers), and Caddy runs respond before reverse_proxy (directive order).
Step 9: Run n8n's Security Audit
sudo docker compose exec n8n n8n audit
n8n is the service name in n8n's examples; use yours. The audit prints a JSON report, or No security issues found, across five risk categories: credentials, database, filesystem, nodes and instance. Per its source, the instance part lists published Webhook nodes with authentication set to None that don't lead straight into an If, Switch or Code node, says when "Newer versions contain security updates", and shows settings such as the public API, community packages and NODES_EXCLUDE. Add --categories=instance for that part alone.
Step 10: Update Often and Keep Backups
n8n releases a new minor version most weeks and advises updating at least once a month, after reading the release notes (update n8n). In October 2026 alone, n8n's security advisories page lists 27 advisories, seven rated high, all fixed in newer releases; the high-rated fix for expression isolation shipped in 2.45.5 (checked 30 October 2026). Leave N8N_VERSION_NOTIFICATIONS_ENABLED on, so n8n tells you about new versions and security updates. Pin the version and update with our n8n update and rollback guide; on 30 October 2026, stable is 2.45.7 and 3.0 is early access.
Back up the database and the .n8n volume together as our backup guide shows, and keep a copy off the server, for example with restic. Backups are not included in Space-Node plan prices; you can arrange them through a support ticket.
If Something Stops Working
- Execute Command is missing from the nodes panel. It's excluded by default. Remove it from
NODES_EXCLUDEonly if you trust everyone who can edit workflows. - A Code node or expression fails with
access to env vars denied. That's the 2.x default. Keep secrets in credentials, as n8n's 2.0 notes advise. - A file node says
Access to the file is not allowed.The path is outside~/.n8n-files, which is/home/node/.n8n-filesin the Docker image, or inside.n8n. - Nginx returns 403 and its error log says
access forbidden by rule. Your address changed; update theallowline.
FAQ
Can I turn off the n8n login for a private instance?
No. n8n removed basic auth and the N8N_USER_MANAGEMENT_DISABLED switch in 1.0, and its docs say no supported way to disable the login screen exists. Keep the login and add the IP restriction from Step 8.
I lost my 2FA device and my recovery codes. How do I get back in?
On the server, run sudo docker compose exec n8n n8n mfa:disable --email=you@example.com, then log in and set up 2FA again (command line).
I installed n8n with npm, not Docker. What changes?
Docker's UFW bypass doesn't apply, but n8n listens on all addresses by default (N8N_LISTEN_ADDRESS is ::, per the deployment variables), so set it to 127.0.0.1 behind your proxy. n8n 3.0 drops support for npm installs, so plan a move to Docker (3.0 breaking changes).
What should I do if a copy of my .n8n volume or key leaked?
Change the passwords, API keys and tokens stored in n8n at each service, and revoke the old ones. n8n's key rotation (opt-in on 2.x, on by default in 3.0) replaces only the data key, and the instance key "never changes", so a leaked copy stays readable.
Sources
All opened on 30 and 31 October 2026:
- n8n docs: Security, Set up SSL, Run security audits, Disable the public API, Block specific nodes, Harden task runners, Enable SSRF protection, Manage security policies, Rotate encryption keys, Security variables, Nodes variables, SSRF variables, User management and 2FA variables, Deployment variables, Basic configuration, Set a custom encryption key, User management, Manage users and access, Require two-factor auth, Webhook node, Webhook credentials, Community node risks, Use the command line, Install with Docker, Update n8n, 2.0 breaking changes, 3.0 breaking changes
- n8n source at n8n@2.45.7: security config, nodes config, MFA config, public API config, SSRF config, endpoints config, env access provider, file access helpers, instance settings, owner controller, ownership service, audit command, audit constants, instance risk reporter, mfa:disable command, form page template, n8n Compose file, withPostgres example
- GitHub: n8n releases, n8n security advisories
- Docker: Port publishing, Packet filtering and firewalls, docker container ls, docker compose up, docker compose exec, Set environment variables
- nginx: access module, core module, proxy module, command line, access module source
- Caddy: Request matchers, Directives, respond, Keep Caddy running
- Man pages and references: ssh(1), chmod(1), openssl-rand(1), MDN: Set-Cookie, WebKit bug 232088