One command to expose a local service through Cloudflare Zero Trust.
zt up portainer 9000 --allow you@example.com
# → https://portainer.yourdomain.com (ZT-protected, running in ~15s)
zt up <name> <port> automatically:
- Creates a Cloudflare Tunnel
- Configures ingress rules
- Upserts a CNAME DNS record
- Creates a Zero Trust Access application
- Starts
cloudflaredin the background - Saves state locally
zt down <name> reverses all of the above.
- A domain on Cloudflare
cloudflaredinstalled and in PATH- A Cloudflare API token with the following permissions:
Account / Cloudflare Tunnel / EditZone / DNS / EditAccount / Access: Apps and Policies / Edit
- Cloudflare dashboard → My Profile → API Tokens → Create Token
- Use Custom token, add the permissions above
- Set Account Resources → your account
- Set Zone Resources → your domain
curl -fsSL https://raw.githubusercontent.com/casablanque-code/cfzt/main/install.sh | bashDetects OS and architecture, downloads the correct binary from the latest release, places it in /usr/local/bin/zt.
go install github.com/casablanque-code/cfzt/cmd/zt@latestgit clone https://github.com/casablanque-code/cfzt
cd cfzt
go build -o zt ./cmd/zt
sudo mv zt /usr/local/bin/Download from Releases and place in your PATH.
zt initYou'll be prompted for:
| Field | Where to find it |
|---|---|
| API Token | Cloudflare → My Profile → API Tokens |
| Account ID | Cloudflare dashboard → right sidebar |
| Domain | Your domain as it appears in Cloudflare (e.g. example.com) |
Config is saved to ~/.zt-config.json (mode 0600).
zt up <name> <port># Zero Trust protected — prompts email login via Cloudflare Access
zt up portainer 9000 --allow you@example.com
# Multiple allowed emails
zt up vault 8200 --allow alice@example.com --allow bob@example.com
# Access app with bypass policy (no login required, but still proxied through CF)
zt up grafana 3000
# Completely public, no Access app created
zt up api 8080 --publicService becomes available at https://<name>.<domain>.
zt down portainerStops the cloudflared process, removes the DNS record, deletes the tunnel and Access app from Cloudflare.
zt list # or: zt lsNAME URL PORT STATUS PID
portainer https://portainer.example.com 9000 running 17423
grafana https://grafana.example.com 3000 stopped -
zt status portainer| Flag | Description |
|---|---|
--public |
No Zero Trust gate — skip Access app entirely |
--allow <email> |
Restrict access to this email, repeatable |
~/.zt-config.json # credentials (0600)
~/.zt-state.json # tunnel state (0600)
~/.zt/tunnels/<name>/
config.yml # cloudflared config
<tunnel-id>.json # tunnel credentials
cloudflared.log # cloudflared process log
cloudflared not found in PATH
Install it: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/
502 Bad Gateway after zt up
The tunnel is up but the local service isn't running or isn't listening on the specified port. Check with curl http://localhost:<port> and look at the log:
tail -f ~/.zt/tunnels/<name>/cloudflared.logTunnel shows stopped but URL still works
The cloudflared process was restarted outside of zt. Run zt down <name> + zt up <name> <port> to resync state.
tunnel already exists
Run zt down <name> first, or check zt list. If the tunnel is stale on Cloudflare's side, zt up will clean it up automatically.
zone not found for domain
Make sure the domain is added to Cloudflare and the API token has Zone / DNS / Edit permission.
Authentication error on Access app creation
The API token is missing Account / Access: Apps and Policies / Edit permission. Edit the token in Cloudflare dashboard and add it.
MIT