n8n API Tokens: 6 Dangerous Leaks and Proven Fixes

n8n API tokens open your whole automation server: whoever holds one can read and change workflows through the public API. The old version of this post listed five leak scenarios with made-up configuration, such as an N8N_API_TOKEN variable and a YAML api: block. Neither exists in n8n. This rewrite tests where tokens really leak. I ran n8n 2.45.7 in a podman container with a fresh SQLite database, created an owner and API keys through its REST API, and checked each risk. All tokens below are lab values from a container that no longer exists, and real keys are redacted.

What n8n API tokens look like

The n8n API authentication page says to send the key in a header named X-N8N-API-KEY. The server enforced it:

no key: 401
bad key: 401
good key: 200
out of scope (GET /credentials?): 403

(The last line was a request to /api/v1/executions with a key created for workflow scopes only.) The key itself was a 289-character JWT. Its middle part is only base64, so anyone who sees a key can read it:

{'sub': '9f37f607-1933-482a-b134-0c6bbc644cd0', 'iss': 'n8n', 'jti': '6149a606-a123-47bc-b79f-01bc236cd4b5', 'iat': 1793603051, 'exp': 1793689451, 'aud': 'public-api'}

That shows the user ID and the expiry time. It is signed, so it cannot be changed, but it is not secret.

Leak 1: n8n API tokens stored in clear in the database

I copied database.sqlite out of the container and read the user_api_keys table:

user_api_keys columns: ['id', 'userId', 'label', 'apiKey', 'createdAt', 'updatedAt', 'scopes', 'audience', 'lastUsedAt']
row: [('lab', 'JWT stored in clear', 289)]
credentials_entity: [('lab header', 'U2FsdGVkX19vi3FsbHac')]

The API key is stored as the full JWT, while saved credentials are encrypted. Anyone who gets a copy of the n8n data folder, a backup or a volume snapshot therefore holds every working API key. Protect backups of ~/.n8n like a password file.

Leak 2: the encryption key sits next to the data

The data folder held a file named config with mode 600:

-rw-------    1 node     node            56 Nov  2 07:03 config
{
	"encryptionKey": "<redacted>"
}

This is the key that protects saved credentials. With the data folder, the CLI decrypted everything:

$ n8n export:credentials --all
lab header httpHeaderAuth U2FsdGVkX19vi3FsbHacBJA0tozeVCF1gs6fbcPo...

$ n8n export:credentials --all --decrypted
lab header {'name': 'Authorization', 'value': 'Bearer LAB-CREDENTIAL-TOKEN-456'}

So encryption protects a stolen database only when the key is stored somewhere else. The n8n encryption key page describes N8N_ENCRYPTION_KEY. Set it once and keep it. When I started the same volume with a different value, n8n refused to start: Error: Mismatching encryption keys. The encryption key in the settings file /home/node/.n8n/config does not match the N8N_ENCRYPTION_KEY env var.

Leak 3: hardcoded tokens in workflow nodes

Many people paste a bearer token into the header field of an HTTP Request node. I created such a workflow through the API. The token was then in three places in plain text:

$ curl -s -H 'X-N8N-API-KEY: ...' .../api/v1/workflows/ENv1uVhIFwgN10aQ | grep -o 'LAB-HARDCODED-TOKEN-123'
LAB-HARDCODED-TOKEN-123

$ n8n export:workflow --id=ENv1uVhIFwgN10aQ | grep -o 'Bearer LAB-HARDCODED-TOKEN-123'
Bearer LAB-HARDCODED-TOKEN-123

hardcoded token in workflow_entity: (1,)

A search of the raw database files found the hardcoded token twice and the credential token zero times. Every workflow export you commit to Git or share in a forum carries such tokens with it. Use a credential instead: it is encrypted, and exports leave it out unless you add --decrypted.

n8n API tokens leak test: decrypted credential export, X-N8N-API-KEY sniffed over HTTP, readable JWT payload and 401 after revoke
Four of the leaks, and the fix that works at once.

Leak 4: n8n API tokens sent over plain HTTP

I attached a tcpdump container to the n8n network namespace and sent one API request to the HTTP port:

$ grep -a -c 'X-N8N-API-KEY' /tmp/p.txt
1
$ grep -a 'X-N8N-API-KEY' /tmp/p.txt | head -1 | cut -c1-60
X-N8N-API-KEY: <JWT redacted>

The full key was readable on the wire. Put n8n behind TLS, for example a reverse proxy with a certificate, before you call the API from another machine. The same goes for the editor login cookie. I started the lab with N8N_SECURE_COOKIE=false only because it had no HTTPS.

Leak 5: tokens in container environment variables

$ docker inspect -f '{{json .Config.Env}}' n8n-lab | tr ',' '\n' | grep MY_API
"MY_API_TOKEN=lab-token-in-env"

Anyone who can run docker inspect or podman inspect on the host can read every variable you pass with -e. Access to the container engine is access to your secrets.

Leak 6: keys that never expire

When you create n8n API tokens you choose an expiry. The docs say that on an Enterprise plan you also choose scopes. My test key, created with a 3-second expiry, was refused after 6 seconds, and a deleted key was refused at once:

before delete: 200
delete: 200
{"message":"Unauthorized"} after delete: 401
{"message":"Unauthorized"} expired key: 401

This is the real rotation for n8n API tokens. The old post showed a script that called a generate_new_token function, which does not exist. Create a new key in the UI, update the client, then delete the old key and check that it returns 401.

How to audit n8n API tokens you already have

Start with an inventory. The user_api_keys table shown above has a label and a lastUsedAt column, so you can see which n8n API tokens exist and which ones nobody has used. Delete every key without a clear owner. Next, search your workflow exports for words like Bearer or Authorization: in this lab a plain grep found the hardcoded token in seconds. Finally, check how clients reach the server. If any of them call the API over plain HTTP, assume those n8n API tokens are already exposed and replace them.

Checklist for n8n API tokens

  • Give each client its own key with a label, so you can revoke one without breaking the others.
  • Set an expiry date on every key.
  • Serve n8n over HTTPS only.
  • Store secrets as credentials, never in node fields.
  • Keep the encryption key out of the data volume, and encrypt backups of ~/.n8n.
  • Limit who can run docker inspect or open the database file.

The same rules apply to any self-hosted app with a token API. The Linkwarden non-root post shows how file ownership in Docker volumes decides who can read such data.

FAQ

Is there an N8N_API_TOKEN variable? The authentication docs do not mention one: n8n API tokens are created in the UI and sent as X-N8N-API-KEY. In the lab the variable was not set and nothing needed it.

Can I read an n8n API token from the database? In n8n 2.45.7, yes: the apiKey column held the full key.

Does deleting a key stop it at once? Yes. The next request with that key got 401.

Comments

Popular posts from this blog

How to Play Minecraft Bedrock Edition on Linux: A Comprehensive Guide for Tech Professionals

Linux desktop frozen? My 5 go-to tricks to try - before forcing a hard reboot

The Ultimate Guide: How to Set Up DXVK in Wine on Linux for Enhanced Gaming Performance