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
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:
- Automatic — Amazon Route 53. Connect a hosted zone under Admin → Settings → DNS & Route 53 with a least-privilege IAM access key (the tab shows a copy-paste IAM policy scoped to just that zone). On issue/renew, the relay writes the TXT records, waits for propagation, and validates automatically — with a live progress bar. Credentials are stored encrypted at rest.
- Manual — any DNS provider. The wizard shows the TXT record(s) to add yourself, then you click Verify & issue.
- 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.
- (Optional) On the DNS & Route 53 tab, enter the AWS access key/secret, hosted zone ID, and region, and save.
- 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.
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.
| Setting | Meaning |
|---|---|
| Base domain | Wildcard base for tunnels. Tunnels become <sub>.<base>. |
| App host | The management/UI host. Requests to this host are not treated as ingress. |
| HTTPS enabled | Turns on TLS termination (ports 443/80) and the certificate wizard. |
| Require invite code | On 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 key | Max concurrent live tunnels per API key. 0 = unlimited. |
| Reaper interval (sec) | How often expired/idle tunnels are swept (minimum 5). |
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>]
| Flag | Alias | Description |
|---|---|---|
--server | -s | Ztpr server base URL (e.g. https://app.example.com) |
--key | -k | Your API key (ztpr_…) |
--target | -t | Local URL to forward to (e.g. http://localhost:3000) |
--label | -l | Friendly name shown in the dashboard |
ztpr -s https://app.example.com -k ztpr_xxx -t http://localhost:3000 -l "my dev box"
501 for upgrade requests.Security notes
- API keys are stored as SHA-256 hashes (256 bits of entropy) and shown only once at creation.
- Invite codes are single-use with optional expiry; the first user becomes admin. The invite requirement can be toggled by an admin but is on by default.
- Blocking a user invalidates their sessions (security-stamp bump), rejects their API keys, and immediately reaps their active tunnels.
- A configurable per-key concurrent-tunnel limit guards against exhaustion.
- AWS Route 53 credentials are stored encrypted at rest (ASP.NET Core Data Protection); the keyring lives in
/opt/ztpr/keys/and is preserved across upgrades. Use a least-privilege IAM key scoped to the single hosted zone (the Settings tab provides the policy). - The relay proxies tunnel traffic but never executes it.
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.