A Cloudflare Tunnel connects your server to Cloudflare's network through outbound-only connections made by a small daemon called cloudflared. Visitors reach your application through Cloudflare, so the server does not need any inbound HTTP or SSH port open to the internet. In this tutorial you will install cloudflared on Ubuntu 24.04, create a named tunnel, route two hostnames to local services (a web app and SSH), run the tunnel as a systemd service, and put a Zero Trust Access login in front of it.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user with
sudoprivileges. - A domain whose DNS is managed by Cloudflare (the zone must be active in your Cloudflare account). This guide uses
your_domainas a placeholder. - A web service listening locally. The examples assume something on
http://localhost:8080; any HTTP app works. - Outbound connectivity to Cloudflare on port
7844(TCP and UDP).cloudflareduses it for its tunnel connections; most providers allow outbound traffic by default. - A Cloudflare Zero Trust organization for Step 7. The free plan is enough; you create it the first time you open the Zero Trust dashboard.
NoteCloudflare also offers "remotely managed" tunnels configured entirely from the dashboard with a token. This guide uses a locally managed tunnel, where the configuration lives in a YAML file on the server and can be versioned like any other config.
Step 1 - Installing cloudflared
Cloudflare publishes an APT repository for cloudflared. Installing from it means apt upgrade keeps the daemon current, which matters because old versions eventually stop being supported.
Download the repository signing key:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | sudo tee /etc/apt/keyrings/cloudflare-public-v2.gpg > /dev/null
Add the repository. Cloudflare uses the distribution name any, so the same line works on every supported Debian and Ubuntu release:
echo "deb [signed-by=/etc/apt/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
Install the package:
sudo apt update
sudo apt install cloudflared
Check the installed version:
cloudflared --version
cloudflared version 2026.x.x (built ...)
Step 2 - Authenticating cloudflared with your account
Before the CLI can create tunnels and DNS records, it needs a certificate that ties it to your Cloudflare account and zone. Run:
cloudflared tunnel login
The command prints a URL. Open it in a browser on any machine, log in to Cloudflare and select your_domain. When you authorize it, cloudflared saves the certificate on the server:
You have successfully logged in.
If you wish to copy your credentials to a server, they have been saved to:
/home/your_user/.cloudflared/cert.pem
This cert.pem can create and delete tunnels in your account, so keep it private. It is only needed for management commands, not for running the tunnel.
Step 3 - Creating the tunnel and DNS records
Create a named tunnel. The name is only a label for you:
cloudflared tunnel create web01
Tunnel credentials written to /home/your_user/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel.
Created tunnel web01 with id 6ff42ae2-765d-4adf-8112-31c55c1551ef
Write down the tunnel ID (the UUID). The JSON file holds the secret the daemon uses to run this tunnel. The service you install later runs as root and reads its configuration from /etc/cloudflared, so copy the credentials there and restrict the permissions:
sudo install -d -m 0755 /etc/cloudflared
sudo install -m 0600 ~/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json /etc/cloudflared/
Replace the UUID with your own in this and all following commands.
Now create DNS records that point hostnames at the tunnel. cloudflared adds a proxied CNAME to <tunnel-id>.cfargotunnel.com for each one:
cloudflared tunnel route dns web01 app.your_domain
cloudflared tunnel route dns web01 ssh.your_domain
INF Added CNAME app.your_domain which will route to this tunnel tunnelID=6ff42ae2-765d-4adf-8112-31c55c1551ef
INF Added CNAME ssh.your_domain which will route to this tunnel tunnelID=6ff42ae2-765d-4adf-8112-31c55c1551ef
List your tunnels to confirm it exists:
cloudflared tunnel list
Step 4 - Writing the ingress rules
The configuration file tells cloudflared which tunnel to run and where to send each hostname. Rules are matched from top to bottom, and the last rule must be a catch-all without a hostname.
Create the file:
sudo nano /etc/cloudflared/config.yml
Add the following, adjusting the tunnel ID, hostnames and local ports:
tunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef
credentials-file: /etc/cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json
ingress:
- hostname: app.your_domain
service: http://localhost:8080
- hostname: ssh.your_domain
service: ssh://localhost:22
- service: http_status:404
A few notes on this file:
service: http://localhost:8080sends traffic forapp.your_domainto your local web app. Cloudflare terminates HTTPS for visitors, so the local app can stay on plain HTTP bound to127.0.0.1.- If a local service only speaks HTTPS with a self-signed certificate, use
service: https://localhost:8443and add anoriginRequestblock withnoTLSVerify: trueunder that rule. - The final
http_status:404rule answers any hostname you did not list.
Validate the file:
cloudflared tunnel --config /etc/cloudflared/config.yml ingress validate
Validating rules from /etc/cloudflared/config.yml
OK
You can also check which rule a given URL matches:
cloudflared tunnel --config /etc/cloudflared/config.yml ingress rule https://app.your_domain
Using rules from /etc/cloudflared/config.yml
Matched rule #0
hostname: app.your_domain
service: http://localhost:8080
Step 5 - Running the tunnel as a systemd service
Run the tunnel once in the foreground to catch errors before turning it into a service:
sudo cloudflared tunnel --config /etc/cloudflared/config.yml run
Look for lines saying Registered tunnel connection. Normally there are four, spread over two Cloudflare data centers. Press CTRL+C to stop it.
Install the systemd service. With a locally managed tunnel, cloudflared service install picks up /etc/cloudflared/config.yml, writes a unit file, and enables and starts it:
sudo cloudflared service install
Check the service:
sudo systemctl status cloudflared
● cloudflared.service - cloudflared
Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled; preset: enabled)
Active: active (running) since ...
Follow its logs with:
sudo journalctl -u cloudflared -f
From any machine, request the web hostname:
curl -I https://app.your_domain
The response should come from your app, with Cloudflare headers such as server: cloudflare and cf-ray. The tunnel is working.
You can also see the active connections from the server:
cloudflared tunnel info web01
Step 6 - Connecting to SSH through the tunnel
The ssh.your_domain rule forwards to the local SSH daemon, but a plain ssh client cannot speak to Cloudflare's edge directly. The client machine needs cloudflared as a proxy.
On your local machine, install cloudflared (on macOS, brew install cloudflared; on Linux, the same APT repository from Step 1). Then add a host entry to ~/.ssh/config on the client:
Host ssh.your_domain
ProxyCommand cloudflared access ssh --hostname %h
User your_user
Connect as usual:
ssh ssh.your_domain
Once you have confirmed that SSH through the tunnel works, and only then, you can stop accepting SSH on the public interface. With UFW, removing the rule looks like this:
sudo ufw delete allow OpenSSH
sudo ufw status
Warningkeep a second way into the server (for example, the VNC console in your provider's panel) before closing port 22. If the tunnel or
cloudflaredstops, SSH through it stops too.
Step 7 - Protecting applications with Zero Trust Access
At this point anyone who knows app.your_domain can reach the app. Cloudflare Access adds a login step at Cloudflare's edge, so unauthenticated requests never reach your server. It is especially useful for admin tools and for the SSH hostname.
In the Cloudflare dashboard, open Zero Trust and do the following:
- Under Settings > Authentication, make sure at least one login method is enabled. One-time PIN (a code sent by email) works without any extra setup.
- Go to Access > Applications and select Add an application, then Self-hosted.
- Give it a name and add
app.your_domainas the application domain. Addssh.your_domainas well, or create a second application for it. - Add a policy with the action Allow and an Include rule such as Emails with your address, or Emails ending in
@your_company_domain. - Save the application.
Menu names in the Zero Trust dashboard change from time to time; if a label differs, look for the Access applications section.
Test it by opening https://app.your_domain in a private browser window. You should see the Cloudflare Access login page instead of your app. After entering an allowed email and the one-time code, you are redirected to the application.
For SSH, the next ssh ssh.your_domain opens a browser window for the same login, and cloudflared stores the resulting token locally for the session duration.
Troubleshooting
The service fails to start. Check the logs with sudo journalctl -u cloudflared -n 50. The most common causes are a wrong path in credentials-file, a UUID in tunnel: that does not match the JSON file, or a YAML indentation error. Run the ingress validate command from Step 4 again.
Error 1033 in the browser. Cloudflare cannot find a running connector for the tunnel. Make sure the service is active and that outbound port 7844 is not blocked by a firewall between the server and the internet.
502 Bad Gateway. The tunnel is up, but cloudflared cannot reach the local service. Test it from the server with curl -I http://localhost:8080 and check that the port and scheme in config.yml are correct.
The hostname does not resolve. Confirm that the CNAME exists in the Cloudflare DNS dashboard. If route dns failed because a record with that name already existed, delete the old record and run the command again.
Conclusion
Your server now publishes a web application and SSH through a Cloudflare Tunnel, with no inbound ports needed for them and an identity check in front of both. The tunnel runs as a systemd service and its configuration lives in a single YAML file you can back up or manage with configuration management.
As next steps, you can bind your local services to 127.0.0.1 only so they are never reachable directly, add more hostnames to the ingress list as you deploy new tools, and connect a proper identity provider (Google, GitHub, Okta) to Zero Trust instead of one-time PINs.
