A friend messaged me last month. He had spun up self-hosted n8n on a cheap cloud box over a weekend, wired it to Stripe, his Postgres, Slack, and a couple of Google APIs, and it was humming along beautifully. Then he asked the question that made my stomach drop:
"Wait, is port 5678 supposed to be open to the whole internet?"
It was. He had followed a popular tutorial. The tutorial told him to publish 5678:5678 in his Docker Compose file, skip the encryption key, and get on with building workflows. It worked on the first try. That is exactly the problem.
Here is the thing nobody says loudly enough: people treat n8n as an automation tool. It is really a credential aggregator. One instance can hold the keys to Stripe, your database, Slack, Google, HubSpot, and a dozen other services. Compromise the n8n box and you do not get one API key. You get all of them, at once.
In 2026 that stopped being theoretical.
Why 2026 was a rough year for n8n
This is not fearmongering. n8n had a genuinely bad year for security, and a lot of it hit self-hosted instances specifically:
- Multiple critical CVEs, including a couple rated CVSS 10.0 (sandbox escape leading to full server takeover).
- Leaked API tokens exposing live instances to credential theft.
- Weak encryption keys recovered from public artifacts. Researchers found internet-exposed instances running with known weak keys, which means the "encryption" protecting stored credentials was effectively decorative.
The one I want to anchor on, because it is clean and easy to understand, is CVE-2026-65589.
CVE-2026-65589, in plain English
- Type: information disclosure (CWE-532, sensitive data written to records).
-
Affected: n8n before
1.123.64on the 1.x line, fixed in1.123.64. The 2.x line is patched separately (fixed in2.29.8), so check your major version and upgrade to the fixed release for whichever line you run. - What happens: when you pass credentials as custom HTTP headers inside certain LLM sub-nodes (OpenAI, Anthropic, and others), n8n masks them in the UI but writes the plaintext API key straight into the workflow execution records. Any authenticated user who can view or export executions can read them. Exported execution archives carry the same exposure.
- Severity: CVSS 5.1 (medium). Not known-exploited, no public proof-of-concept.
- Source: the n8n advisory GHSA-89gh-3pgc-v5h2.
Read that again. You mask the key in the UI, you feel safe, and n8n logs it in cleartext where it persists in the database. That is the kind of bug that does not need a hacker in a hoodie. It just needs one more user account with read access to executions.
Content on these CVEs was rephrased from vendor advisories for compliance.
Who this is for
If you self-host n8n, or you are about to, this is for you. If you have not stood n8n up yet, start with the previous episode, Self-Host n8n on AWS EC2 with Docker, which gets it running from an empty box to first login. This article picks up from there: it shows you the risky default setup first (so you can recognize it), then hardens it on AWS with a concrete, verifiable checklist. Everything here is runnable. I put the full insecure-vs-hardened pair, the IAM policy, the Caddy config, and a verify-from-outside script in one repo, linked at the end.
I am deliberately doing this on AWS, not on a generic VPS, and I will explain why the AWS primitives make this easier and safer than the usual guide.
The scoreboard (before vs after)
This is the whole article in one table. Same n8n, locked down. Every row was verified live on a real EC2 instance.
| Item | Before (insecure) | After (hardened) |
|---|---|---|
| Port 5678 to the internet | OPEN (HTTP 200 from public IP) | closed (connection refused) |
| n8n version |
1.100.0 (CVE-affected) |
1.123.64 patched |
| Encryption key | none (derived default) | AWS Secrets Manager |
| DB password | plaintext | AWS Secrets Manager |
| IAM | broad | least-privilege role |
| Editor UI | public | IP-restricted + TLS (Caddy) |
Now let me walk through how you get from the left column to the right one.
Step 0: see the problem
The insecure Compose file looks harmless. That is what makes it dangerous. The red flags:
services:
n8n:
image: n8nio/n8n:1.100.0 # old, CVE-affected
ports:
- "5678:5678" # published to the host = open to the world
# no N8N_ENCRYPTION_KEY
# telemetry on, no execution pruning
Bring it up on a fresh Amazon Linux 2023 box and, from a completely different machine, run:
curl -s -o /dev/null -w 'n8n on public IP -> HTTP %{http_code}\n' http://<PUBLIC_IP>:5678
I got back:
n8n on public IP -> HTTP 200
HTTP 200. Reachable by anyone on the internet who runs a port scan, which is to say, reachable by every automated scanner on the internet within hours. Do not leave this running. Tear it down immediately:
sudo docker compose -f insecure/docker-compose.yml down
One honest note on scope: I am describing the CVE-2026-65589 class accurately from the advisory. I did not stage a live key-leak on camera, and you should not either. The point is to recognize the misconfiguration and fix it, not to build a working attack.
The 4 fixes
Fix 1: patch, and stop publishing 5678
Two changes in the hardened Compose file:
services:
n8n:
image: n8nio/n8n:1.123.64 # patched: fixes CVE-2026-65589
expose:
- "5678" # internal only, NOT published to the host
expose instead of ports is the part people miss. It makes n8n reachable only to other containers on the Docker network. Nothing lands on the host's public interface. Combine that with the AWS security group (next), and 5678 is closed at two layers.
Fix 2: put secrets in AWS Secrets Manager
A .env file on disk is one cat away from leaking. So the encryption key and the database password never touch the repo and never sit in plaintext on the box. Generate them, store them in Secrets Manager, inject them at runtime:
# run once, from an admin session
ENC_KEY="$(openssl rand -hex 32)"
aws secretsmanager create-secret \
--name "n8n/encryption-key" \
--secret-string "$ENC_KEY" \
--region us-east-1
The N8N_ENCRYPTION_KEY matters more than it looks. Without it, n8n derives a default key, which means your stored credentials are effectively unencrypted. Set it, and back it up separately. Lose that key and you lose access to every credential n8n has stored. There is no recovery.
(The repo's create-secrets.sh wraps this in a create-secret || put-secret-value fallback so it is safe to re-run, which is why the instructions ask for both CreateSecret and PutSecretValue on the admin session.)
At deploy time, a small script pulls both secrets into the shell environment (not a file) and starts the stack:
export N8N_ENCRYPTION_KEY="$(aws secretsmanager get-secret-value \
--secret-id n8n/encryption-key --query SecretString --output text --region us-east-1)"
Fix 3: a least-privilege IAM role
Here is where AWS earns its keep. The EC2 instance reads those two secrets through an IAM role that can read only those two secrets. No static credentials on the box. No broad secretsmanager:*. Just this:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOnlyN8nSecretsFromSecretsManager",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:n8n/encryption-key-*",
"arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:n8n/postgres-password-*"
]
}
]
}
If the box is ever compromised, the blast radius from this role is two secrets, read-only. Compare that to a .env file, which hands over everything the moment someone gets shell access. The trailing -* matches the random suffix Secrets Manager appends to secret ARNs.
Fix 4: TLS and an IP-locked editor with Caddy
n8n publishes no ports. Only Caddy publishes 80 and 443, terminates TLS with an automatic Let's Encrypt certificate, and splits traffic:
n8n.example.com {
# Webhooks must stay public so external services can call them.
handle /webhook/* {
reverse_proxy n8n:5678
}
# Everything else (editor UI + REST API) is restricted to your admin IP.
handle {
@blocked not remote_ip 203.0.113.10
respond @blocked "Forbidden" 403
reverse_proxy n8n:5678
}
}
Your editor and REST API are now reachable only from your IP. Your webhooks stay public, because they have to. Everyone else gets a 403.
Step final: prove it
Do not trust that hardening worked. Verify it from outside the instance:
curl -s -m 8 -o /dev/null -w 'port 5678 -> HTTP %{http_code}\n' http://<PUBLIC_IP>:5678
Output:
port 5678 -> HTTP 000
HTTP 000 means curl could not even open a connection: refused or timed out. That is exactly what you want. The door that was wide open is now a wall.
Why "on AWS" beats a generic VPS guide
Every generic hardening guide tells you to install UFW, edit an nginx config, and put secrets in a chmod 600 file. That works. But AWS gives you managed primitives that do the same jobs with less to get wrong:
- Security group is a managed, default-deny firewall. You allow 22 (locked to your IP), 80, and 443. You never open 5678. It is stateful and it is not a config file you can fat-finger.
- Secrets Manager is a managed secret store with rotation available as a managed feature. No plaintext env file on disk to leak.
- IAM least-privilege role scopes access with zero static credentials. If n8n later needs to call an AWS service, you give it a tighter role, separate from any human's keys.
That last point is the setup for where this series goes next. Once your secrets and identity live in AWS, giving n8n a scoped execution role to call something like Amazon Bedrock is a small, safe step instead of pasting an API key into a header (the exact thing CVE-2026-65589 punished).
The checklist to keep
If you remember nothing else, work down this list:
-
Patch first. n8n
>= 1.123.64. - AWS network. Security group allows only 22 (your IP), 80, 443. Never 5678.
- Server/OS. SSH keys only, no root or password login, unattended security updates.
-
n8n config. Real
N8N_ENCRYPTION_KEY, disable public signup,WEBHOOK_URL=https://..., session timeout, prune executions, telemetry off. -
Reverse proxy and TLS. No published n8n ports, TLS at Caddy, editor IP-restricted, only
/webhook/*public. - Secrets. Encryption key and DB password in Secrets Manager, read via a least-privilege IAM role.
- Credentials inside n8n. Use built-in credential objects, not custom LLM headers. Prefer OAuth. Rotate.
- Monitoring. Review the Executions tab for odd sources, add an external uptime check, centralize logs.
What I would tell my friend now
The weekend tutorial got him running. It just skipped the part about making him safe to leave running. Those are different jobs, and the second one is the one that matters once real credentials are involved.
It took me an afternoon to build the hardened version. Closing 5678, patching the image, moving two secrets into Secrets Manager, scoping an IAM role, and putting Caddy in front. Compared to the cost of leaking a Stripe key, that is the cheapest afternoon you will ever spend.
The full repo has the insecure and hardened Compose files, the IAM policy, the Caddyfile, the scripts, and a hardening checklist with the reasoning behind every item: github.com/simplynadaf/secure-n8n-on-aws.
This is the hardening episode of a series on running n8n properly on AWS. The previous episode got n8n running from an empty box to first login; this one locked it down. Next up: running a real AI agent inside n8n with Amazon Bedrock, using that scoped IAM role instead of an OpenAI key.
Question for you: if you self-host n8n, is your port 5678 published to the host right now? Go check your Compose file. I will wait. Tell me what you find in the comments.
Follow me for more on AWS architecture, DevOps, and AI Infrastructure:
Portfolio | LinkedIn | Dev.to | YouTube | Email | AWS Builder Center | X
Top comments (0)