puente
v0.3.0
Published
Centralized, self-hosted manager for Cloudflare Tunnels across your machines — expose local ports on your own subdomains, orchestrate remote nodes over SSH.
Maintainers
Readme
A centralized, self-hosted control panel for Cloudflare Tunnels across all your machines.
Run it on one computer, then expose local ports — on that machine or on any remote server you reach over SSH — on your own subdomains. No open firewall ports, no public IP, no reverse proxy to babysit.
port 5006 on this PC → puente.example.com
port 6060 on this PC → grafana.example.com
port 7008 on a remote host (SSH) → vw.example.comEverything is driven from one beautiful web UI. puente talks to the Cloudflare API to create the tunnels + DNS, and orchestrates the cloudflared connector on each machine (locally or over SSH) for you.
Install & run
You need Node.js ≥ 22. Pick your package manager:
# Run once, without installing
npx puente
# Global install (recommended)
npm install -g puente # then: puente
pnpm add -g puente # then: pnpm approve-builds -g && puente
bun add -g puente # then: puentepnpm users: since pnpm 10, dependency build scripts are blocked by default, so pnpm asks to approve the native deps (
better-sqlite3,ssh2,cpu-features). Approve them once after installing withpnpm approve-builds -g, or keep a one-off run non-interactive withpnpm --allow-build=better-sqlite3,ssh2,cpu-features dlx puente.npxandbunbuild them automatically with no prompt.
The scoped name
@pauldvlp/puenteis an identical alias published at the same version —npx @pauldvlp/puenteworks too if you prefer it.
Then just run:
puente # starts the panel on http://localhost:5006 and opens your browser
puente setup # same, and drops you straight into the guided setupThat's it. The first screen walks you through everything.
The panel keeps running in the background and gives your terminal back, so closing it
(or pressing Ctrl+C) no longer takes the tunnels down. Manage it with puente status,
puente logs -f, puente restart and puente stop. Need it attached to the terminal
instead — a container, a systemd unit, CI? Add --foreground.
…or with Docker
# from a clone of the repo
docker compose up -d # panel on http://localhost:5006, state in a named volumeState persists in the puente-data volume (mounted at /data). Override the port
with the PUENTE_PORT environment variable.
First-run setup (about 2 minutes)
- Create your admin account — a single local login for the panel.
- Connect Cloudflare — paste a scoped API token (see below). puente verifies it, shows your domains, and stores it encrypted on disk.
- Add a node — “This machine”, or a remote host over SSH.
- Provision it — puente installs
cloudflared, creates the tunnel, and starts the connector. - Publish a route — map a local port to a subdomain. Live in seconds.
Cloudflare API token — exact permissions
Create the token at dash.cloudflare.com → My Profile → API Tokens → Create Token → Create Custom Token, and add exactly these four permissions:
| Category | Permission group | Access | Why | | ----------- | ----------------- | -------- | ------------------------------------------------------ | | Account | Cloudflare Tunnel | Edit | Create & configure tunnels and their ingress rules | | Zone | DNS | Edit | Create the proxied CNAME records that route subdomains | | Zone | Zone | Read | List your domains so you can pick where to publish | | Account | Account Settings | Read | Auto-discover your account id |
Then set Account Resources = your account and Zone Resources = All zones (or the specific domains you want to manage), create the token, and copy it once. The panel shows this same checklist while you set up.
Prefer not to use a token? You can also authorize
cloudflaredwith the browser login flow (cloudflared tunnel login) — but a scoped API token is the recommended, least-privilege path and the only one puente can fully automate.
Managing remote machines over SSH
Add a node of type Remote (SSH) with its host, port and user. puente can reuse aliases from your ~/.ssh/config.
No key set up yet? Use “Passwordless SSH” on the node. Enter the host password once; puente generates a dedicated ed25519 key, installs it in the remote ~/.ssh/authorized_keys with correct permissions, verifies key auth works, and never stores the password. From then on, everything is key-based.
When you provision a remote node, puente (over SSH):
- detects the OS/architecture (
uname), - installs
cloudflaredif it's missing (correct binary for that platform), - installs it as a persistent service (
cloudflared service install, viasudo -n) so it survives reboots — or falls back to a background process if passwordless sudo isn't available, - wires up the tunnel token and ingress.
CLI
puente [start] # start the panel in the background (default command)
# --port <n>, --host <h>, --no-open, --foreground
puente setup # start + open the guided setup wizard
puente stop # stop the background panel (SIGTERM, then SIGKILL)
puente restart # stop + start, reusing the port/host it was on
puente status # running? pid, URL, uptime — exits 3 when stopped
puente logs [-n 50] [-f] # show (or follow) the background log
puente doctor # check the local environment (Node, cloudflared, ssh)
puente info # print paths and versionA background panel records its pid and URL in ~/.puente/daemon.json and writes its output
to ~/.puente/puente.log (rotated to puente.log.1 past 5 MB). A second puente start
detects the running one instead of fighting it for the port; a state file left behind by a
crash is cleaned up automatically.
Environment variables: PUENTE_PORT (default 5006), PUENTE_DATA_DIR (default ~/.puente).
Concepts
- Node — a machine that runs a
cloudflaredconnector. One node = one Cloudflare Tunnel. The machine running puente is thelocalnode; everything else is ansshnode. - Route — a mapping from a subdomain (e.g.
vw.example.com) to a local service on a node (e.g.http://localhost:7008). Publishing a route updates the tunnel's ingress rules and creates the proxied DNS record.
Alerts (Pro)
A webhook channel receives a POST with this body — versioned and safe to parse:
{
"v": 1,
"trigger": "node.down",
"text": "Node vps-fra is down (was healthy)",
"severity": "critical",
"subject": { "kind": "node", "id": "node_...", "name": "vps-fra" },
"from": "healthy",
"to": "down",
"at": "2026-08-20T22:31:00.000Z"
}Slack and Discord channels get the shape each of them expects instead ({ text } and
{ content }). Recoveries are sent as well as failures, and a 10-minute cooldown per subject keeps
a flapping tunnel from turning into a thousand messages — recoveries ignore the cooldown, because a
late all-clear is useless. Nothing fires for inactive: that is what stopping a connector on
purpose looks like, and paging someone for doing what they asked is how alerting gets muted.
Data & security
- All state lives in
~/.puente/(SQLite database, encryption key, managed SSH keys). - Secrets (Cloudflare token, tunnel tokens) are encrypted at rest with AES-256-GCM; the 256-bit key is generated on first run and stored
chmod 600. - The admin password is hashed with scrypt. Sessions use signed JWTs.
- Nothing is sent anywhere except the Cloudflare API. There is no telemetry.
How it works
┌──────────────── your browser ────────────────┐
│ React + Vite dashboard (this UI) │
└───────────────────┬──────────────────────────┘
│ REST + SSE (live status)
┌───────────────────▼──────────────────────────┐
│ puente control plane (NestJS + SQLite) │
│ • Cloudflare API (tunnels, ingress, DNS) │
│ • SSH orchestration (node-ssh) │
│ • cloudflared lifecycle (local & remote) │
└───────┬───────────────────────────┬──────────┘
local │ child_process SSH │
┌───────▼───────┐ ┌──────▼───────────┐
│ cloudflared │ │ cloudflared │
│ (this PC) │ │ (remote node) │
└───────────────┘ └──────────────────┘
╲ ╱
╲ Cloudflare edge ╱
▼ (your subdomains) ▼Tunnels are remotely-managed (config_src: cloudflare): configuration lives in Cloudflare, and each connector only needs its run token — so puente can reconfigure routes at any time via the API, from one place.
Requirements
- Node.js ≥ 22 on the machine running puente.
- A Cloudflare account with at least one domain (zone).
- For remote nodes: SSH access.
cloudflaredis installed automatically if missing. better-sqlite3ships prebuilt binaries; on unusual platforms a C/C++ toolchain + Python may be needed to build it.
Contributing & security
puente is open source (AGPL-3.0), with a commercially-licensed ee/ directory for the Pro
edition — see LICENSING.md.
Contributions are welcome — see
CONTRIBUTING.md.
Found a vulnerability? Please report it privately per our
Security Policy.
Source & issues: https://github.com/pauldvlp/puente
AGPL-3.0-or-later © Paul Barahona
