Cloudflare Tunnels
Cloudflare Tunnels Set up Cloudflare Tunnels to securely expose your SSH server and other services without opening ports or configuring firewall rules
Infrastructure Quest #7 Intermediate

Cloudflare Tunnels

Set up Cloudflare Tunnels to securely expose your SSH server and other services without opening ports or configuring firewall rules

cloudflaretunnelsnetworkingsshinfrastructure
Download as:

What are Cloudflare Tunnels?

Cloudflare Tunnels create an outbound-only connection from your server to Cloudflare’s edge network. This lets you expose services (SSH, web apps, APIs) without opening inbound ports on your firewall or configuring port forwarding.

Your server connects out to Cloudflare. Clients connect to Cloudflare. Cloudflare routes traffic between them. No public IP or open ports required on your end.

Prerequisites

You also need:

  • A Cloudflare account with a domain added to it
  • Docker installed on the server (for the Docker approach)

Token vs Config File Approach

Cloudflare supports two ways to configure tunnels:

ApproachProsCons
Token-basedQuick setup, managed via Cloudflare dashboardLess portable, harder to version
Config fileVersion-controllable, multi-service, full flexibilityRequires local config management

We recommend the config file approach for long-term use — it is more transparent, reproducible, and easier to manage alongside your dotfiles.

Important: Token-based tunnels are always remotely managed by the Cloudflare dashboard. Even if you add a local config.yml, the dashboard config will override it. To use a truly local config, you must create the tunnel via cloudflared tunnel create (config file approach).

Verify the Tunnel Works

Check the tunnel logs to confirm all connections registered:

docker logs cloudflared-tunnel --tail 10

You should see INF Registered tunnel connection for each connector (typically 4 connections). For config-file tunnels, you should not see an Updated to new configuration line — that indicates dashboard override.

Test an HTTP service through the tunnel:

curl -s -o /dev/null -w "%{http_code}" https://app.yourdomain.com

Client-Side Setup

HTTP vs SSH: HTTP services (web apps, dashboards) work through the browser without any client-side setup. SSH is not HTTP — it requires cloudflared installed on the client machine to proxy the connection.

To connect to your SSH server through the tunnel, install cloudflared on your client machine and use it as a proxy.

1. Install cloudflared

brew install cloudflared

2. Configure SSH to Use the Tunnel

Add a host entry to your ~/.ssh/config:

Host my-server
  HostName ssh.yourdomain.com
  User your-username
  ProxyCommand cloudflared access ssh --hostname %h

# No Port directive needed — the tunnel config handles the port mapping

3. Connect

ssh my-server

Cloudflared proxies the connection through the tunnel. Combined with 1Password’s SSH agent, this gives you end-to-end secure access without exposed ports.

Adding More Services

Extend your config.yml to expose additional services. A simple convention is to use the service name as the subdomain — this keeps DNS records predictable and self-documenting:

ingress:
  # Remote access
  - hostname: ssh.yourdomain.com
    service: ssh://localhost:2222

  # Web applications
  - hostname: app.yourdomain.com
    service: http://localhost:3000

  # Dashboards & monitoring
  - hostname: dashboard.yourdomain.com
    service: http://localhost:8080
  - hostname: monitoring.yourdomain.com
    service: http://localhost:9090

  # Media services
  - hostname: media.yourdomain.com
    service: http://localhost:8096

  # Catch-all (required)
  - service: http_status:404

Connecting to HTTPS Services

When an upstream service uses HTTPS with a self-signed certificate (common with network controllers, reverse proxies, and admin panels), cloudflared will reject the connection by default. Add noTLSVerify to skip certificate verification for the local connection:

  - hostname: controller.yourdomain.com
    service: https://localhost:8443
    originRequest:
      noTLSVerify: true

This is safe because the traffic between cloudflared and the upstream service is local (localhost or the same Docker network). The connection from the client to Cloudflare’s edge is still fully encrypted.

DNS Records and Restart

Remember to add DNS records for each new hostname:

docker run --rm -v /opt/cloudflared/data:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel route dns my-tunnel app.yourdomain.com

Then restart the tunnel container — cloudflared does not hot-reload config changes:

docker restart cloudflared-tunnel

Migrating from Token-Based to Config File

If you started with a token-based tunnel and want to switch to config-file management:

  1. Stop the existing container:
docker compose stop cloudflared
  1. Authenticate with Cloudflare to get a cert.pem:
sudo chown 65532:65532 /opt/cloudflared/data
docker run --rm -v /opt/cloudflared/data:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel login
  1. Delete the old tunnel (token-based tunnels are always remotely managed):
docker run --rm -v /opt/cloudflared/data:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel delete <OLD_TUNNEL_ID>
  1. Delete public hostnames from the Cloudflare Zero Trust dashboard for the old tunnel.

  2. Create a new tunnel:

docker run --rm -v /opt/cloudflared/data:/home/nonroot/.cloudflared \
  cloudflare/cloudflared:latest tunnel create my-tunnel
  1. Create your config.yml and add DNS routes as described above.

  2. Update docker compose — remove TUNNEL_TOKEN env var and add the --config flag to the command.

  3. Start the new tunnel:

docker compose up -d cloudflared

Troubleshooting

Tunnel shows “inactive” in Cloudflare dashboard

  • Check the container is running: docker ps | grep cloudflared
  • View logs: docker logs cloudflared-tunnel
  • Verify the credentials file path in config.yml matches the container mount

connection refused when connecting via SSH

  • Ensure SSH is running on the server: sudo systemctl status sshd
  • Verify the service URL in config.yml matches your SSH port
  • Check that --network host is used in the Docker run command

SSL errors connecting to HTTPS upstream

If cloudflared logs show x509: certificate signed by unknown authority, the upstream service is using a self-signed certificate. Add noTLSVerify to that ingress rule:

  - hostname: service.yourdomain.com
    service: https://localhost:8443
    originRequest:
      noTLSVerify: true

Config changes not taking effect

Cloudflared does not hot-reload its config file. After editing config.yml, restart the container:

docker restart cloudflared-tunnel

Check the healthcheck or logs to verify the tunnel reconnected successfully.

DNS not resolving

  • Confirm the CNAME record exists in your Cloudflare DNS dashboard
  • Wait a few minutes for DNS propagation
  • Test with: dig ssh.yourdomain.com

Resources

🔗
Cloudflare Tunnels Documentation developers.cloudflare.com

Official documentation for setting up and managing Cloudflare Tunnels

🔗
SSH Through Cloudflare Tunnels developers.cloudflare.com

Step-by-step guide for exposing SSH through a Cloudflare Tunnel