User Guide

Operator & reference guide

Self-hosting the relay

DNS setup (one-time, manual)

Point a wildcard and the app host at your server's static IP:

*.tun.example.com   A   <static-ip>
app.example.com     A   <static-ip>

For request routing, Ztpr tracks subdomain reservations internally and routes purely by the Host header — it never needs your DNS provider at request time. For HTTPS certificate issuance it can optionally manage the DNS-01 TXT records for you via Amazon Route 53 (see HTTPS & TLS); otherwise you add them by hand.

Quick install (from a release)

Each tagged release publishes a self-contained Linux bundle — no .NET runtime required on the box.

# Replace OWNER/REPO with your GitHub repository
curl -fsSL https://github.com/OWNER/REPO/releases/latest/download/ztpr-linux-x64.tar.gz | tar xz
sudo ./ztpr/install.sh

deploy/install.sh creates the ztpr system user, installs to /opt/ztpr, writes the systemd unit, and starts the service. You then finish setup — domains and HTTPS — from the admin UI (see Admin settings), so there's no config file to hand-edit.

sudo systemctl status ztpr
journalctl -u ztpr -f

Build a bundle locally (no CI)

To deploy without GitHub Actions, build the same self-contained bundle on your own machine (requires the .NET 9 SDK), then copy it over and install:

pwsh deploy/build-release.ps1            # produces ztpr-linux-x64.tar.gz
scp ztpr-linux-x64.tar.gz user@host:~
ssh user@host
tar xzf ztpr-linux-x64.tar.gz && cd ztpr
chmod +x install.sh update.sh && sudo ./install.sh
The chmod +x is needed once because tarballs built on Windows don't carry the Unix executable bit.

Manual install

dotnet publish src/Ztpr.Server -c Release -r linux-x64 --self-contained true -o /opt/ztpr
sudo useradd --system --home /opt/ztpr ztpr
sudo chown -R ztpr:ztpr /opt/ztpr
sudo cp deploy/ztpr.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ztpr

The SQLite database (ztpr.db) and issued certificates live in the working directory and persist across restarts.

HTTPS / Let's Encrypt (wildcard via DNS-01)

TLS uses a single wildcard certificate (*.tun.example.com) from Let's Encrypt, obtained with the Certes ACME client. One cert covers every random tunnel subdomain — no per-subdomain issuance and no rate-limit worries. SSL terminates at the relay; your local service only ever sees plain HTTP.

Wildcards are validated with a DNS-01 challenge (a TXT record). There are two ways to satisfy it:

  1. In Admin → Settings → General, toggle HTTPS on and restart. The relay listens on 443 (and 80 for the app-host redirect). Port 443 must be reachable.
  2. (Optional) On the DNS & Route 53 tab, enter the AWS access key/secret, hosted zone ID, and region, and save.
  3. On the HTTPS certificate tab, run the wizard: contact email, agree to the Terms of Service, pick staging/production.
    • With Route 53 connected: click Issue certificate automatically and watch the progress log finish.
    • Otherwise: click Start, add the shown _acme-challenge.* TXT records, wait for propagation (dig TXT _acme-challenge.tun.example.com +short), then click Verify & issue.
Do a first run with staging on to confirm the flow (staging certs are untrusted by browsers), then switch staging off and re-run for the real certificate.
Certs last ~90 days, so re-run the wizard before expiry — the TXT values change each time. With Route 53 connected this is one click; otherwise add the new records by hand. The Settings page shows the current cert and days remaining.

Admin settings (runtime config)

Operational settings live in the database and are edited from Admin → Settings — no need to touch appsettings.json. Changes apply at runtime.

SettingMeaning
Base domainWildcard base for tunnels. Tunnels become <sub>.<base>.
App hostThe management/UI host. Requests to this host are not treated as ingress.
HTTPS enabledTurns on TLS termination (ports 443/80) and the certificate wizard.
Require invite codeOn by default. When off, anyone can self-register; when on, new users need a single-use invite.
Route 53 (DNS & Route 53 tab)Optional AWS access key, secret (stored encrypted), hosted zone ID, and region. When set, DNS-01 TXT records are created and verified automatically during certificate issuance/renewal.
Max session duration (hours)Hard cap on a tunnel's lifetime. 0 = no lifetime cap.
Idle timeout (min)Release a tunnel after this long without traffic. 0 = never release on idle.
Max tunnels per keyMax concurrent live tunnels per API key. 0 = unlimited.
Reaper interval (sec)How often expired/idle tunnels are swept (minimum 5).
First boot uses safe defaults (BaseDomain=lvh.me, HTTPS off, invites required). Set your real domain and enable HTTPS from the UI after the first admin signs in.

appsettings reference

You rarely need to edit this — domains, TLS, and limits are managed at runtime (above). The file mainly holds the database location and optional bootstrap defaults:

"ConnectionStrings": {
  "Sqlite": "Data Source=ztpr.db"
}

Any value set in Admin → Settings takes precedence over an appsettings bootstrap default once saved.

Client CLI reference

ztpr --server <url> --key <api-key> --target <local-url> [--label <name>]
FlagAliasDescription
--server-sZtpr server base URL (e.g. https://app.example.com)
--key-kYour API key (ztpr_…)
--target-tLocal URL to forward to (e.g. http://localhost:3000)
--label-lFriendly name shown in the dashboard
ztpr -s https://app.example.com -k ztpr_xxx -t http://localhost:3000 -l "my dev box"
Tunneled WebSocket upgrades are not yet forwarded (plain HTTP only); the ingress returns 501 for upgrade requests.

Security notes

Trust model: the operator can see traffic

TLS is terminated at the relay — it decrypts inbound HTTPS with the wildcard certificate and forwards plain HTTP over the tunnel to your client. So whoever runs the relay can, in principle, observe the decrypted contents of every tunnel. Treat the relay operator as trusted.

There is no end-to-end encryption that hides traffic from the operator. That would require TLS to terminate on the client (SNI passthrough, with the client holding the cert/key), which is not implemented. If you need confidentiality from the host, run the relay on infrastructure you control.