Securing a self-hosted n8n instance
The defaults leave an encryption key that can orphan every credential you own, an unauthenticated webhook on every new trigger, and nodes that can reach anything they can route to. An hour of work closes all of it.
By Parminder Kumar Sharma · · 10 min read

Who this is for
You have a working n8n instance. Maybe you followed the first workflow tutorial, maybe you inherited one. It does something useful and it is about to do something that matters, which is the moment to stop and close the gaps the defaults leave open.
Roughly an hour. You need shell access to wherever n8n runs.
What an attacker is actually reaching for
- Way inExposed editor or webhook
- The prizeStored credentials
- The actual targetYour other systems

- 1The encryption key Set it explicitly, or a lost key orphans every credential
- 2TLS and secure cookies The editor holds every credential in the instance
- 3Blocked node types No command-executing node, no class of outcome
- 4Outbound allowlist Constrains where data can be sent
The one that will ruin your week
Before anything else, the encryption key.
n8n encrypts stored credentials with a key. If that key is not set explicitly, one is generated and written to disk. If it later changes (a rebuilt container, a lost volume, a migration to a new host), every credential you have stored becomes undecryptable. Not recoverable. Re-entered by hand, one at a time.
$ openssl rand -hex 324f1d8a09c2b7e63f5a0d94e18c7b3f26a91d5e4c8b02f7a6d3e91c5b8074af2eGenerate once. Store it in your password manager before you paste it anywhere else.
Set the encryption key explicitly
Set N8N_ENCRYPTION_KEY to the value you just generated, in your environment file or your orchestrator's secret store.
Do this before you add credentials if the instance is new. If it already holds credentials encrypted under a generated key, find that key first (it lives in the n8n data directory) and set it explicitly to the same value rather than a new one.
You should see: The container starts without warning about a generated key, and existing credentials still open.
Close the front door
Put it behind TLS
A reverse proxy in front of n8n handling certificates is the usual arrangement. What you must not do is expose the editor over plain HTTP on a routable address: the login and every credential you type crosses the network in clear.
You should see: The editor loads over https and an http request redirects rather than serving.
Force secure cookies
Set N8N_SECURE_COOKIE to true. This keeps the editor's session cookie on HTTPS only. It is one line and it closes session theft over an unencrypted hop.
You should see: The session cookie carries the Secure flag in your browser's developer tools.
Turn on two-factor authentication
Available in user management. The editor holds every credential in the instance, so an account compromise is a compromise of everything the workflows can reach.
You should see: A second factor is demanded on next sign-in.
Disable the public API if you are not using it
It is enabled by default and most instances never call it. An unused authenticated surface is still a surface.
You should see: Requests to the API path are refused.
What is actually exposed, and to whom
Before changing anything, be clear about what a self-hosted instance puts on the network, because the answer is broader than the editor.
The four surfaces, and who can reach each by default
| Surface | What it is | Who reaches it by default |
|---|---|---|
| The editor | The UI where workflows are built and credentials entered | Anybody who can reach the port. Authentication is a setting, not a default |
| Webhook endpoints | Production URLs that trigger workflows | Anybody on the internet, by design. That is what a webhook is |
| The credential store | Encrypted secrets for every connected service | Anybody with editor access, and anybody holding the encryption key |
| The execution data | Full input and output of every run, retained | Anybody with editor access. It routinely contains the data the workflow moved |
The fourth row is the one that surprises people. Execution data is a debugging feature and it retains payloads, so an instance that processes personal data is storing that data twice: once wherever the workflow sent it, and once in the execution log of the tool that sent it. That is a retention decision somebody has to make deliberately.
Reduce what a compromise reaches
This is the part that separates hardening from box-ticking. Assume something gets in. What can it do?
What the defaults give you
- Every node type available, including ones that run commands.
- Nodes can reach any host they can route to.
- Credentials scoped to whole accounts.
- Execution data retained in full, including whatever passed through.
What you want instead
- Command-executing nodes blocked unless a workflow needs them.
- Outbound connections restricted to hosts you name.
- Credentials scoped to the one resource the workflow touches.
- Sensitive execution data redacted or pruned.
Block the nodes you do not use
n8n can exclude node types by environment variable. The Execute Command and SSH nodes are the obvious candidates: if no workflow needs to run shell commands, removing the capability removes an entire class of outcome.
You should see: The blocked node no longer appears in the node picker.
Restrict where nodes can connect
n8n supports controlling which hosts and IP ranges workflow nodes may connect to. This is server-side request forgery protection, and it is also the single most effective limit on what a prompt injection or a malicious webhook payload can achieve, because it constrains where data can be sent.
You should see: A request to a host outside your allowlist fails.
Scope every credential down
The default when you create a credential is usually broad. A Google account credential that can read every document in the drive is fine until a workflow is manipulated into reading every document in the drive.
You should see: Each credential can reach the one sheet, channel or repository its workflow needs.
Deal with execution data
Executions store what passed through them. If that includes personal data or secrets, redact it or prune it on a schedule. This is a retention decision, and it is the one most likely to appear in a data protection review.
You should see: A completed execution no longer displays the sensitive fields.
The encryption key, and why it is the whole game
Credentials are encrypted at rest with a key n8n generates on first run and writes to disk if you do not supply one. Two consequences follow, and both catch people.
If you did not set it, you cannot move. Restoring a database backup onto a new host without the original key leaves every credential unreadable. The workflows survive; the connections do not. This is the single most common self-hosted recovery failure and it is discovered during an incident.
If it is on the same disk as the database, it protects very little. The threat encryption at rest defends against is somebody obtaining the data without the running system. A key sitting beside the ciphertext defeats that entirely.
Set it explicitly, store it where the database backup is not, and record who can retrieve it. It is worth writing into the same place the disaster recovery plan lives, because that is when it will be wanted.
Run the audit
n8n ships a security audit that inspects the instance and reports what it finds: credentials not in use, nodes with risky capability, instances missing basic settings. It is quick and it will find at least one thing you had forgotten.
Run it, read it, and treat the findings as a list rather than a score.
Patching, which is the part you own now
Self-hosting moves patching from somebody else's schedule onto yours, and this project moves quickly enough that the difference is material rather than theoretical.
Two habits are worth building before anything else.
Pin a version and know which one you are on. An image tagged latest is a version you cannot name, which means you cannot tell whether an advisory applies to you. Pin explicitly, record the tag somewhere outside the host, and treat an upgrade as a change rather than as a side effect of a restart.
Subscribe to the release notes and read them for security items. Nobody will tell you. There is no support contract and no vendor pushing an update to your instance, and an unpatched self-hosted instance stays unpatched for exactly as long as nobody looks.
Before upgrading, take a database backup and confirm you hold the encryption key separately, because an upgrade is the most likely moment to discover you do not. After upgrading, run one workflow that uses a credential. A successful start-up proves the process runs; it does not prove the credential store decrypted.
What good looks like
Take this with you
Before an n8n instance carries production work
- N8N_ENCRYPTION_KEY is set explicitly, stored in a password manager, and has never been rotated without re-entering credentials.
- The editor is behind TLS and N8N_SECURE_COOKIE is true.
- Two-factor authentication is enabled on every account that can sign in.
- The public API is disabled unless something actually calls it.
- Command-executing nodes are blocked unless a specific workflow needs them.
- Outbound connections are restricted to hosts you have named.
- Every credential is scoped to the narrowest resource its workflow touches.
- Every Webhook node has authentication set to something other than None.
- Execution data retention is a decision somebody made, not a default.
- The built-in security audit has been run and its findings closed or accepted.
Verified
Settings and environment variable names checked against n8n's own hosting and security documentation on 5 August 2026. Environment variable names are stable across releases; the location of settings in the editor moves, so trust the variable rather than the screenshot.


