Skip to main content
Your CRM holds your customers, your deals, your emails and your calendar. When a security fix ships in a Twenty release, the fix itself tells attackers where to look, and every instance that hasn’t upgraded yet is a target. Upgrading quickly is the real fix, but most self-hosted instances run a few versions behind. The best protection while you catch up is simple: make sure nobody on the internet can reach your instance in the first place. This page shows how to do that with Tailscale, Cloudflare, or your own reverse proxy.

Private by default

Split your instance into three zones: Each path covers everything under it. Open the public and cloud AI zones only if you use the features behind them, see Public paths. Otherwise, keep the whole instance private. AI assistants that run on your team’s computers, like Claude Code, need nothing opened, see AI clients.
What you give up: hosted automation tools that call the Twenty API from their own cloud (Zapier, Make, n8n Cloud) can’t reach a private instance. Run them inside your network instead (self-hosted n8n works well), or use Twenty workflows. Don’t make /graphql or /rest public to work around this: the app itself runs on those endpoints, so exposing them exposes everything.

Choose a setup

Tailscale

Fully private. Simplest setup, best for small teams.

Cloudflare

No open ports, login with your identity provider, public paths where needed.

Reverse proxy

Caddy or Nginx with IP allowlists. No third-party service.

Option 1: Tailscale

Tailscale puts your server and your team’s devices on a private network. Twenty gets an HTTPS address that only works for people on that network.
1

Bind Twenty to localhost

In docker-compose.yml, change the server port mapping so the container is only reachable from the server itself:
2

Join the server to your tailnet

Install Tailscale on the server, then:
3

Serve Twenty over HTTPS

Twenty is now at https://twenty.<your-tailnet>.ts.net, with a certificate managed by Tailscale. If the command asks you to, enable HTTPS certificates for your tailnet in the admin console.
4

Update SERVER_URL

Then restart with docker compose up -d.
Use Tailscale access controls to limit the server to the people who need it, not your whole tailnet. Connecting Google or Microsoft accounts still works: the OAuth redirect happens in your teammate’s browser, which is on the tailnet. Invitation emails link to the tailnet address, so new teammates need to join the tailnet before they accept.
Tailscale Funnel publishes a whole port to the internet, not individual paths. You can’t use it to make /webhooks public while keeping the app private. If you need public paths or AI connectors, use option 2 or 3.

Option 2: Cloudflare Tunnel and Access

A Cloudflare Tunnel connects your server to Cloudflare with an outbound connection, so no ports are open on the server. Cloudflare Access then asks your team to log in with your identity provider before any request reaches Twenty.
1

Protect the hostname with Access

Do this before creating the tunnel, so the hostname is never reachable without a login. Create self-hosted Access applications. Access applies the most specific path, so the path rules below win over the hostname rule.Create one application per path. Only add the bypass rules for features you use.Local AI clients like Claude Code can’t complete a browser login, so they rely on the office or VPN IPs in the last rule. If your team works from anywhere, add an Allow policy for your team to that application instead, enable WARP authentication in Access, and have them run the Cloudflare WARP client.
2

Create a tunnel

In the Cloudflare Zero Trust dashboard, create a tunnel, pick Docker as the environment and copy the token. Add a public hostname, crm.example.com, pointing to http://server:3000.
3

Run cloudflared next to Twenty

Add this service to docker-compose.yml and remove the ports section from server. cloudflared reaches Twenty over the Docker network, so the server doesn’t need to publish any port.
Add CLOUDFLARE_TUNNEL_TOKEN to your .env.
4

Update SERVER_URL

Then restart with docker compose up -d.
If you use Cloudflare’s WAF or bot protection, skip challenges on the MCP and OAuth paths. Claude and ChatGPT can’t solve a challenge page.

Option 3: Your own reverse proxy

If you already run a reverse proxy, allowlist your office or VPN IP addresses for the app and open only the public paths. Bind Twenty to localhost first, as in option 1, so nothing bypasses the proxy.
Replace 203.0.113.0/24 with your office or VPN addresses. CHATGPT_CONNECTOR_IPS holds OpenAI’s ranges, see AI clients; leave it empty if you don’t use ChatGPT. If your proxy sits behind a load balancer, match on the client IP it forwards (client_ip with trusted_proxies in Caddy, set_real_ip_from in Nginx), not on the load balancer’s address.

Public paths

These are the paths that services outside your network call. Open only the ones you need. By default Twenty polls mailboxes and calendars, so Google and Microsoft never call your instance. They only do if you set IS_CONNECTED_ACCOUNT_WEBHOOK_SUBSCRIPTION_ENABLED=true. How each public path is protected:
  • Workflow webhooks only check that the workspace and workflow exist. Anyone who knows the URL can trigger the workflow, so treat the URL as a secret and validate the payload in the workflow before acting on it.
  • App and logic function routes are public unless the route requires authentication (isAuthRequired). For public routes, verify the caller in your own code, for example with the sending service’s signature.
  • Google, Microsoft, SES and Resend webhooks are verified by Twenty, using the provider’s signature or token.
  • Email links can only record an open, a click or an unsubscribe for the recipient they were sent to.

AI clients (MCP)

AI assistants are one of the main ways your team will use Twenty, through its MCP server. What you need to open depends on where the assistant runs, not on which assistant it is.

Local clients: nothing to open

Claude Code, Codex CLI, Cursor and other tools that run on your computer connect to Twenty from that computer. If it’s on your tailnet or VPN, they reach /mcp the same way your browser reaches the app. This is the safest setup, so prefer it when you can. Claude Desktop is the exception: connectors added in Settings → Connectors run in Anthropic’s cloud, even in the desktop app. To keep the connection local, add Twenty as a local MCP server in claude_desktop_config.json instead:

Cloud connectors: open with care

Claude (web, mobile, and connectors in the desktop app) and ChatGPT call your server from Anthropic’s and OpenAI’s infrastructure, not from your device. They can’t reach a private instance, so you have to open /mcp, /oauth and /.well-known to their IP ranges.
An IP allowlist checks which network a request comes from, not who sent it. Anyone with a Claude or ChatGPT account can add your URL as a connector and send requests to these paths.
What keeps them out is login. To get a token, a user has to sign in on Twenty’s /authorize page, which stays in the private zone. Someone outside your network can start the connection flow but can’t finish it, and /mcp rejects every request without a valid token. What stays exposed is the code that runs before login: OAuth discovery, client registration (/oauth/register) and the token endpoint. The allowlist narrows that: internet-wide scanners can’t reach these paths directly, and requests have to come through Claude’s or ChatGPT’s infrastructure. It doesn’t make that code safe on its own, so upgrades still matter. If you use cloud connectors:
  • Open only /mcp, /oauth and /.well-known. Never open /authorize or the rest of the app.
  • Allow only the vendors you use:
    • Anthropic: 160.79.104.0/21, from Anthropic’s IP addresses page.
    • OpenAI: the list in chatgpt-connectors.json. OpenAI describes it as best effort and it changes over time, so refresh it on a schedule:
  • Upgrade quickly when release notes mention MCP or OAuth fixes.

Verify your setup

Test from both sides of your network after any change:
The header should point to https://crm.example.com/.well-known/.... If it shows http:// or 127.0.0.1, your proxy isn’t forwarding the Host and X-Forwarded-Proto headers.

Baseline checklist

Postgres and Redis are not published. The default docker-compose.yml doesn’t publish them; keep it that way if you edit it.
Nothing bypasses your proxy or tunnel. With a proxy on the host, Twenty’s port is bound to 127.0.0.1. With cloudflared in Docker, the server service publishes no port. Docker writes its own firewall rules, so ports it publishes skip ufw and similar host firewalls.
Secrets are long and random, and you know how to rotate them.
Workspace members use SSO or two-factor authentication.
The database is backed up on a schedule, and you have tested a restore.
TAG is pinned to a version, and you upgrade on purpose.

Stay up to date

None of the above replaces upgrading. It buys you time.
  • Watch releases on GitHub: Watch → Custom → Releases.
  • When a release includes a security fix, upgrade as soon as its advisory says to: the same day for critical issues. Follow the upgrade guide.
  • Report vulnerabilities to security@twenty.com, not in public issues.