Table of Contents – Cloudflare Tunnel Zero Trust
Overview
Expose self-hosted apps safely without opening inbound ports. Cloudflare Tunnel creates an outbound-only connection from your host to Cloudflare Edge, gives you free HTTPS, and lets you gate access with Zero Trust (SSO/MFA).
External reference:
Cloudflare Tunnel Docs
Prerequisites
- A domain on Cloudflare (free plan works).
- One Linux host that runs the origin app (e.g.,
localhost:8080). - sudo access and a browser for initial login.
Placeholders: app.example.com, my-tunnel, <TUNNEL-UUID>.
Quick Architecture

Install/Setup
Ubuntu / Debian
# Install cloudflared (official repo)
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/ jammy main' | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
sudo apt update && sudo apt install -y cloudflared
cloudflared --version
RHEL / Rocky / CentOS Stream / Fedora
# Install cloudflared (RPM)
sudo rpm -ivh https://pkg.cloudflare.com/cloudflared-latest-x86_64.rpm || sudo dnf install -y https://pkg.cloudflare.com/cloudflared-latest-x86_64.rpm
cloudflared --version
Arch / Manjaro
# Install cloudflared (community or AUR)
sudo pacman -Sy --noconfirm cloudflared || yay -S --noconfirm cloudflared
cloudflared --version
openSUSE / SLE
# Install cloudflared (RPM)
sudo zypper install -y https://pkg.cloudflare.com/cloudflared-latest-x86_64.rpm
cloudflared --version
Base Configuration
Authenticate
# Open a browser to authenticate your account
cloudflared tunnel login
# A cert is saved to ~/.cloudflared/cert.pem (back this up)
Create Tunnel
# Create a named tunnel and note its UUID
cloudflared tunnel create my-tunnel
cloudflared tunnel list
Config File
# ~/.cloudflared/config.yml
tunnel: my-tunnel
credentials-file: /home/$USER/.cloudflared/<TUNNEL-UUID>.json
ingress:
- hostname: app.example.com
service: http://localhost:8080
- service: http_status:404
Route DNS
Goal: point your subdomain (e.g., app.example.com) to the tunnel my-tunnel by creating a DNS CNAME that resolves to <UUID>.cfargotunnel.com.
One-step command
cloudflared tunnel route dns my-tunnel app.example.com
What happens: the command creates:
app.example.com → <UUID>.cfargotunnel.com (proxied at Cloudflare).
Verify
# 1) DNS CNAME exists?
dig +short CNAME app.example.com
# 2) Final resolution (Cloudflare IPs)
dig +short app.example.com
# 3) Tunnel status
cloudflared tunnel list
cloudflared tunnel info my-tunnel
journalctl -u cloudflared -n 50 --no-pager
# 4) External check
curl -I https://app.example.com
Quick fixes
- No CNAME result: run the command again on the right account/zone; ensure the domain is active on Cloudflare.
- 502/404: verify your app is listening locally:
ss -ltnp | grep 8080andcurl -I http://localhost:8080; also confirmhostnameinconfig.ymlmatches exactly. - Down after reboot: enable the service (see next section).
Reload/Enable & Health Checks
# Run as a systemd service (persistent)
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared --no-pager
# Health checks
cloudflared tunnel info my-tunnel
cloudflared tunnel list
sudo journalctl -u cloudflared -n 100 --no-pager
curl -I https://app.example.com
Security/Hardening
Harden the tunnel by putting your app behind identity (Zero Trust), exposing only required paths, and locking down credentials on disk.
A) Gate access with Zero Trust (SSO/MFA)
- Cloudflare dashboard → Zero Trust → Access → Applications → Add → Self-hosted.
- Application name: any (e.g.,
My App). - Application domain: your public host (e.g.,
app.example.com). - Identity providers: pick Google/GitHub/Email OTP.
- Policies → Add: Type=Allow; Include emails ending
@yourdomain.com; Require MFA. - Save → open
https://app.example.comin Incognito → expect Access login first.
Docs: Cloudflare Zero Trust — Self-hosted apps
B) Expose only what you need (limit paths/methods)
Put admin on a separate hostname and return 404 for everything else by default.
# ~/.cloudflared/config.yml
tunnel: my-tunnel
credentials-file: /home/$USER/.cloudflared/<TUNNEL-UUID>.json
ingress:
- hostname: app.example.com
service: http://localhost:8080
- hostname: admin.example.com
service: http://localhost:8080/admin
- service: http_status:404
Apply & verify:
sudo systemctl restart cloudflared
sudo systemctl status cloudflared --no-pager
curl -I https://app.example.com/ # 200/301/302
curl -I https://app.example.com/secret # 404 by default rule
curl -I https://admin.example.com/ # Access login (admins only)
C) Protect & rotate local credentials
Protect these files: ~/.cloudflared/cert.pem and ~/.cloudflared/<TUNNEL-UUID>.json (or /etc/cloudflared/ if root-managed).
# If running as your user
chmod 700 ~/.cloudflared
chmod 600 ~/.cloudflared/*.json ~/.cloudflared/cert.pem
# If running as a system service (root)
sudo chown -R root:root /etc/cloudflared
sudo chmod 700 /etc/cloudflared
sudo chmod 600 /etc/cloudflared/*.json /etc/cloudflared/cert.pem
Performance & Optimization
Test flags in foreground, then persist via systemd.
A) Foreground test (temporary)
cloudflared --version
cloudflared tunnel run my-tunnel --metrics 127.0.0.1:4321 --protocol http2 --retries 5 --transport-loglevel warn
B) Persist via systemd drop-in
command -v cloudflared
sudo mkdir -p /etc/systemd/system/cloudflared.service.d
sudo tee /etc/systemd/system/cloudflared.service.d/10-flags.conf >/dev/null <<'EOF'
[Service]
Environment="CF_ARGS=--metrics 127.0.0.1:4321 --protocol http2 --retries 5 --transport-loglevel warn"
ExecStart=
ExecStart=/usr/bin/cloudflared --config /etc/cloudflared/config.yml --no-autoupdate tunnel run $CF_ARGS
EOF
sudo systemctl daemon-reload
sudo systemctl restart cloudflared
sudo systemctl status cloudflared --no-pager
Backup & Restore
Backup
tar czf ~/cloudflared-backup.tgz ~/.cloudflared
sha256sum ~/cloudflared-backup.tgz > ~/cloudflared-backup.tgz.sha256
Restore
sha256sum -c ~/cloudflared-backup.tgz.sha256
tar xzf ~/cloudflared-backup.tgz -C ~/
sudo cloudflared service install
sudo systemctl enable --now cloudflared
Troubleshooting (Top issues)
1) Tunnel connects but 502/404 at origin → local app/port mismatch.
ss -ltnp | grep 8080 || echo 'App not listening on 8080'
curl -I http://localhost:8080
tail -n 100 ~/.cloudflared/*.log 2>/dev/null || true
2) Tunnel down after reboot → service not installed/enabled.
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared --no-pager
journalctl -u cloudflared -n 100 --no-pager
3) DNS not resolving → CNAME missing or wrong zone.
dig +short CNAME app.example.com
cloudflared tunnel route dns my-tunnel app.example.com
Key Takeaways & Next Steps
- Outbound-only tunnels remove router/NAT changes.
- Zero Trust (SSO/MFA) protects access behind identity.
- Back up credentials and config; run as a service for reliability.
