Securing n8n, Credentials, OAuth, and Encryption
TL;DR — n8n’s credentials store encrypts at rest with
N8N_ENCRYPTION_KEY. Use OAuth where the service supports it; static tokens otherwise. Restrict UI exposure to a VPN / SSO-gated reverse proxy. Audit quarterly: rotate keys, prune old credentials, review who has access.
After the standup bot, an obvious question: how do we keep all these credentials safe? A self-hosted n8n with workflows touching Jira, Slack, GitHub, Notion, and Postgres has access to a lot. This post is the security model + the operational hygiene.
The credentials model
n8n separates “credentials” from “workflows.” A credential is a named, typed bundle of auth data (API token, OAuth tokens, basic auth, etc.). Workflows reference credentials by name; the actual values are stored encrypted in the n8n database.
The data flow:
- You enter credential values in n8n’s UI (or via API)
- n8n encrypts with
N8N_ENCRYPTION_KEYand stores the ciphertext in Postgres - At workflow execution, n8n decrypts in-memory and uses
- Encrypted values never leave the n8n process
This means: workflow JSON files (exportable, version-controllable) never contain real credential values. They reference credentials by ID or name. Safe to commit.
Setting N8N_ENCRYPTION_KEY correctly
Already covered briefly in self-host post. Re-emphasizing:
N8N_ENCRYPTION_KEY=<32-byte hex string>
Generate once: openssl rand -hex 32. Store in your secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). Inject into the n8n container at startup.
If lost: you can’t decrypt existing credentials. Workflows that depended on them break. Recovery requires recreating every credential.
If rotated: every existing credential needs re-encryption. n8n doesn’t have a built-in rotation tool in 0.176; you’d export, re-import. Doable but disruptive. Don’t rotate casually.
OAuth vs API tokens
For each service n8n integrates with, you have a choice:
OAuth flow: Service provides an OAuth client; n8n redirects through the UI to authorize. Tokens are refreshed automatically. Permissions scoped to what’s needed.
Static API token: Generate a long-lived token in the service; paste into n8n. Simpler; expires only when revoked.
The tradeoff:
| Aspect | OAuth | API token |
|---|---|---|
| Setup complexity | Higher (OAuth client config) | Lower |
| Revocation | Granular per app | Per token |
| Audit trail | Per-app activity log | Per-token if the service supports it |
| Permission scope | Granular | Often broad |
| Rotation | Automatic | Manual |
| Compromise blast radius | Limited to scopes | Full token permissions |
For services that support OAuth well (Slack, Google, Microsoft, Zoom, HubSpot), use OAuth. For services where OAuth is painful (Jira, GitHub PATs for some operations, internal APIs), use scoped API tokens.
OAuth setup in n8n — Slack example
The dance for Slack OAuth in n8n:
- In Slack app config (created during slash commands setup): add OAuth Redirect URL:
https://n8n.example.com/rest/oauth2-credential/callback - In n8n: Add Credential → Slack OAuth2 API
- Fill Client ID and Client Secret from Slack app
- Click “Connect” — opens Slack’s authorization page
- Approve → Slack redirects to n8n’s callback with code
- n8n exchanges code for tokens, stores encrypted
Now workflows reference this credential by name. n8n refreshes tokens automatically. If a token is revoked Slack-side, n8n surfaces the auth error on next use.
Service account vs personal credentials
Hard rule: production workflows should NEVER use personal credentials.
When an engineer with admin Jira access leaves the company, you don’t want every workflow’s Jira credential to go with them. Use service accounts:
- Jira: create a dedicated user
n8n-bot@yourorg, license seat, scoped project permissions, API token - Slack: dedicated bot user attached to a Slack app
- GitHub: dedicated machine user OR GitHub App (preferred for production)
- Notion: dedicated integration, scoped to specific databases
- Postgres: dedicated DB user with least-privilege permissions
Pay the seat license. Cheaper than the day you have to re-credential everything.
Network exposure
n8n’s UI is sensitive. Anyone with access can:
- Read workflows (which expose internal API structure)
- Read execution data (which contains real payloads)
- Run workflows manually
- Edit credentials
Restrict access. Patterns in order of strictness:
Pattern 1: VPN-only. n8n is on internal DNS, no public IP. Engineers VPN to access. Webhooks reach n8n via a separate, narrowly-scoped public webhook receiver service. Complex but most secure.
Pattern 2: SSO-gated reverse proxy. n8n is public, but Traefik/Cloudflare/oauth2-proxy in front requires SSO. Engineers log in via Google/Okta/Github. Webhooks bypass the SSO via specific URL paths.
Pattern 3: Public + basic auth. n8n’s N8N_BASIC_AUTH_*. Single shared password. Works; not great for >5 users.
Pattern 4: Public + no auth. Don’t.
For a small team, pattern 3 is acceptable. For 10+ people, pattern 2 (SSO) becomes important. Pattern 1 is overkill unless you’re regulated.
Production target: never default-public. Make the engineering team go through some friction; the friction is the security.
Webhook URL security
Webhook URLs aren’t behind your auth — they’re public by design. Defenses:
- Signature verification at the workflow level (covered in webhooks 101)
- Long random URL paths so URLs aren’t guessable
- Per-webhook tokens in URL or header that you verify
- Rate limiting at the reverse proxy
For webhooks from major services (Slack, GitHub, Stripe), use their signature schemes. For internal webhooks where there’s no signature option, generate long random URL paths (/webhook/xfbc4z9hn3p8s2q-deploy-trigger).
Audit + hygiene checklist
Quarterly:
- Review who has access to n8n’s UI; remove ex-employees
- List all credentials in n8n; identify any using personal accounts; migrate to service accounts
- Verify each credential’s permissions are still minimal
- Rotate API tokens that haven’t been rotated in 12 months
- Check
EXECUTIONS_DATA_PRUNEis on and old execution data is being deleted - Verify
N8N_ENCRYPTION_KEYis in your secrets manager - Backup test: restore a Postgres dump to a test n8n, confirm credentials decrypt
- Webhook URLs: any that have been pasted publicly should be regenerated
Common Pitfalls
Sharing n8n login across the team. Use individual SSO logins (pattern 2). When someone leaves, you can offboard without rotating shared credentials.
Storing credentials in workflow JSON or .env instead of n8n’s credentials store. Defeats the encryption. Always use the credential UI.
Service account credentials with admin privilege. Scope down. n8n-bot doesn’t need to delete projects.
No backup of N8N_ENCRYPTION_KEY. A disk failure = lost encryption key = lost all credentials.
Webhook URLs in chat logs / commit history. Treat them as secrets. Use environment variables; rotate when exposed.
Public n8n UI to ngrok during development. Anyone scanning ngrok subdomains can find it. Use auth on dev too.
Logging credential values in Code nodes. console.log(credentials.apiKey) writes to execution history. n8n’s UI can show executions. Never log raw secrets.
Skipping EXECUTIONS_DATA_PRUNE. Execution history contains every value that flowed through every node, forever. Old executions = old PII / secrets in the DB. Set retention.
Wrapping Up
Encrypt credentials, prefer OAuth, gate the UI, audit quarterly. None of this is exotic — it’s the same operational hygiene every secrets-bearing service needs. The reason it gets neglected for internal automation is “it’s just internal.” Right up until it isn’t. Friday: monitoring n8n in production — closing the loop with observability.