Mahbubur Riad
Back to blog
Hosting & Server 8 min read

Step‑by‑Step Migration from Cloudflare Tunnel to Self‑Hosted Tailscale Zero‑Trust Access

Jun 16, 2026 · Mahbubur Riad

Learn how to replace Cloudflare Tunnel with a self‑hosted Tailscale setup. This guide covers installation, auth, DNS, security hardening and a practical migration checklist for VPS‑based services.

On this page

Step‑by‑Step Migration from Cloudflare Tunnel to Self‑Hosted Tailscale Zero‑Trust Access

If you’ve been using Cloudflare Tunnel (formerly Argo Tunnel) to expose private services on a VPS, you might be looking for a solution that gives you full control, lower cost, or tighter integration with your existing Zero‑Trust policies. Tailscale, built on WireGuard, can be run entirely on your own infrastructure and provides the same "no‑open‑ports" experience while letting you manage access with ACLs, device tags, and identity‑based routing.

This post walks you through the entire migration process – from installing Tailscale on a fresh VPS to replicating Cloudflare Tunnel’s DNS routing and hardening the connection. The steps are written for developers and sysadmins who are comfortable with Linux command‑line, systemd, and basic networking.


Table of Contents

  1. Why switch? – Quick comparison
  2. Prerequisites
  3. Installing Tailscale on your VPS
  4. Setting up authentication & device authorization
  5. Replicating Cloudflare Tunnel DNS with Tailscale Magic DNS
  6. Migrating a sample service (e.g., a local web app)
  7. Security hardening checklist
  8. Monitoring & troubleshooting
  9. FAQ
  10. Conclusion

Why switch? – Quick comparison {#why-switch}

Feature Cloudflare Tunnel Tailscale (self‑hosted)
Control plane Hosted by Cloudflare (no server to manage) Runs on your own machines – you own the control plane
Cost Free tier limited to 5 tunnels, paid plans for higher traffic Free for up to 100 devices, unlimited tunnels on self‑hosted subnet routers
Zero‑Trust policies Cloudflare Access policies (requires Cloudflare account) ACLs written in a simple JSON file, can be version‑controlled
Performance Traffic exits Cloudflare edge – latency depends on nearest PoP Direct peer‑to‑peer WireGuard tunnels, typically lower latency
Port exposure No inbound ports needed No inbound ports needed (subnet router uses outbound UDP 51820)
Vendor lock‑in High – moving away requires re‑creating tunnels Low – Tailscale is open source, can replace the coordination server
Self‑hosting Not possible Yes – you can run the control server (Headscale) if you want full independence

If you value full ownership, lower latency, and the ability to keep all traffic inside your own network, Tailscale is a solid alternative. The migration is straightforward because both solutions expose services on a local port and forward them over a secure tunnel.


Prerequisites {#prerequisites}

Item Minimum requirement
VPS Ubuntu 20.04 LTS (or any recent Debian‑based distro) with sudo access
Domain A DNS zone you control (e.g., example.com)
Cloudflare account Only needed to export existing DNS records – you can delete the tunnel later
Tailscale account Free personal account or an organization account for ACLs
Root/ sudo To install packages and edit systemd services

Make sure your VPS has outbound UDP 51820 allowed (the default WireGuard port). No inbound ports are required.


Installing Tailscale on your VPS {#install-tailscale}

The official install script works on most Linux distributions. Run the following as root or with sudo:

Bash
# Add the Tailscale repository and install the package
curl -fsSL https://tailscale.com/install.sh | sudo bash

# Start the Tailscale daemon
sudo systemctl enable --now tailscaled

You should see the service running:

Bash
$ sudo systemctl status tailscaled
● tailscaled.service - Tailscale node agent
   Loaded: loaded (/lib/systemd/system/tailscaled.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2026-06-16 10:12:34 UTC; 5s ago

Authenticate the node

Run the tailscale up command. For a one‑off migration you can use the interactive web flow:

Bash
sudo tailscale up --login-server https://login.tailscale.com

Your terminal will output a URL. Open it in a browser, log in with the same identity you use for Cloudflare Access, and approve the device. The node will appear in the Tailscale admin console under Machines.

Tip: If you want the node to act as a subnet router (exposing the whole VPS private network), add --advertise-routes=10.0.0.0/24 (replace with your actual subnet).


Setting up authentication & device authorization {#auth}

Tailscale’s ACL system lives in a JSON file that you edit in the admin console (https://login.tailscale.com/admin/acls). Below is a minimal example that mirrors a typical Cloudflare Access policy – only members of the devops group can reach the internal.example.com service.

JSON
{
  "ACLs": [
    {
      "Action": "accept",
      "Users": ["group:devops"],
      "Ports": ["10.0.0.2:443"]
    }
  ],
  "Groups": {
    "devops": ["[email protected]", "[email protected]"]
  },
  "TagOwners": {
    "tag:service": ["group:devops"]
  }
}

Explanation

  • 10.0.0.2 is the private IP of the VPS (or the IP of a subnet router).
  • The ACL only allows members of the devops group to connect to port 443.
  • You can later tag devices (tag:service) to apply broader rules.

After saving the ACL file, Tailscale pushes the policy to all nodes within a few seconds.


Replicating Cloudflare Tunnel DNS with Tailscale Magic DNS {#dns}

Cloudflare Tunnel automatically creates DNS entries like app.example.com that resolve to a CNAME pointing to cfdotunnel.com. With Tailscale you can achieve the same user‑friendly name using Magic DNS combined with split‑DNS on the client side.

1. Enable Magic DNS

In the Tailscale admin console go to Settings → DNS and toggle Enable Magic DNS. Add your custom domain (e.g., example.com) under Search Domains.

2. Create a DNS record for the service

Assume your VPS private IP on the Tailscale network is 100.101.102.103. Create a DNS entry in the DNS Names section:

Name Type Value
internal.example.com CNAME 100.101.102.103.tailnet-yourorg.ts.net

Tailscale will serve this name to any device that is logged into the same Tailnet.

3. Configure client machines

On each client (your laptop, CI runners, etc.) run:

Bash
sudo tailscale up --accept-dns=true

The client will now resolve internal.example.com to the Tailscale IP and route the traffic through the encrypted mesh.

Note: If you need the name to resolve for users outside the Tailnet (e.g., a partner), you can add a regular CNAME in your public DNS pointing to the Magic DNS name, but be aware that the traffic will still need to traverse the Tailscale network – the partner device must be a Tailscale node.


Migrating a sample service (e.g., a local web app) {#migrate-service}

Let’s migrate a simple Flask app that is currently exposed via Cloudflare Tunnel on port 8080.

1. Verify the app runs locally

Bash
# Inside the VPS
python3 -m venv venv && source venv/bin/activate
pip install flask
cat > app.py <<'EOF'
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
    return "Hello from Flask via Tailscale!"
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080)
EOF
python app.py &

Check curl http://127.0.0.1:8080 returns the greeting.

2. Create a systemd service for the app

Bash
sudo tee /etc/systemd/system/flask-app.service > /dev/null <<'EOF'
[Unit]
Description=Flask Application
After=network.target

[Service]
User=www-data
WorkingDirectory=/home/ubuntu
Environment="PATH=/home/ubuntu/venv/bin"
ExecStart=/home/ubuntu/venv/bin/python /home/ubuntu/app.py
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now flask-app.service

3. Advertise the service via Tailscale

Add a tag to the node so the ACL can reference it:

Bash
sudo tailscale set --advertise-tags=tag:web

Update the ACL to use the tag:

JSON
{
  "ACLs": [
    {
      "Action": "accept",
      "Users": ["group:devops"],
      "Ports": ["tag:web:8080"]
    }
  ]
}

Now any authorized user can reach the service using the Magic DNS name you configured earlier (internal.example.com:8080).

4. Test from a client machine

Bash
# On your laptop (already logged into the same Tailnet)
curl http://internal.example.com:8080
# Expected output:
# Hello from Flask via Tailscale!

If the request succeeds, the migration is complete. You can now delete the Cloudflare Tunnel configuration from the Cloudflare dashboard.


Security hardening checklist {#hardening}

✅ Item Why it matters
Enable --ssh on the node Gives you password‑less, audited SSH access over the Tailscale mesh (no open port 22).
Restrict ACLs to specific groups Prevents accidental exposure of services to all Tailnet members.
Use tailscale up --advertise-exit-node only on trusted machines Exit nodes route all traffic; limit to vetted hosts.
Rotate machine keys regularly Run sudo tailscale logout && sudo tailscale up on a schedule (e.g., quarterly).
Enable --auto-update Keeps the WireGuard kernel module and tailscaled binary patched.
Run services under non‑root users Reduces impact if a container is compromised.
Audit logs in the admin console Tailscale records every connection attempt – review for anomalies.
Consider self‑hosting Headscale If you need absolute control, replace the SaaS control plane with Headscale.

Monitoring & troubleshooting {#monitoring}

1. Check node health

Bash
sudo tailscale status --json | jq '.Peer'

Look for Online: true and the TailscaleIP.

2. Verify ACL enforcement

From a client that should not have access, try:

Bash
curl -m 5 http://internal.example.com:8080 || echo "Blocked as expected"

You should see a timeout or a 403 response from the Tailscale daemon.

3. View connection logs

Bash
sudo journalctl -u tailscaled -f

Typical log lines:

Text
2026-06-16T10:15:01.123Z INFO  tailscaled: peer 100.101.102.104 ([email protected]) connected
2026-06-16T10:15:10.456Z WARN  tailscaled: ACL denied connection from [email protected] to tag:web:8080

4. Export metrics (optional)

Tailscale exposes Prometheus metrics on localhost:9090/metrics. Add a scrape job to your monitoring stack to keep an eye on latency and packet loss.


FAQ {#faq}

1. Do I need to keep Cloudflare Tunnel running during migration?

No. The migration can be done in a cut‑over window. Keep the tunnel active until you have verified the Tailscale endpoint works, then delete the Cloudflare configuration.

2. Can I use Tailscale on a Windows or macOS workstation?

Absolutely. Tailscale provides native clients for Windows, macOS, Linux, iOS, Android, and even Docker containers.

3. What happens to existing DNS records that point to *.cfargotunnel.com?

Replace them with the Magic DNS name you created (internal.example.com). If you need a seamless transition, add a temporary CNAME to the Magic DNS name and let the TTL expire.

4. Is the traffic still encrypted end‑to‑end?

Yes. Tailscale uses WireGuard, which encrypts every packet with modern ChaCha20‑Poly1305. No traffic passes through a third‑party edge.

5. How do I scale to dozens of services without creating a huge ACL file?

Group services by tags (tag:web, tag:db) and use CIDR ranges in the ACL. You can also generate ACL JSON programmatically from a source‑of‑truth like a Git repo.


Conclusion {#conclusion}

Migrating from Cloudflare Tunnel to a self‑hosted Tailscale setup gives you full ownership of the control plane, lower latency, and a flexible ACL system that integrates nicely with existing CI/CD workflows. By following the steps above—installing Tailscale, configuring Magic DNS, replicating your ACLs, and hardening the node—you can retire the Cloudflare tunnel with minimal downtime.

If you run into edge‑cases or want a deeper dive into running a Headscale server for complete self‑hosting, feel free to check out the resources on mahbuburriad.com.

Related

Related posts