Solo + Friends — Full TLS Setup Guide
This guide turns a self-hosted dvb-WarpPool into a friends-only pool reachable from the internet via TLS with a real Let's Encrypt certificate. The OSS distribution covers this tier (see scaling.md) — no commercial extensions needed.
Target audience: you run WarpPool on a Mac or Linux at home with a Fritz!Box (or similar router with DDNS support). You want to invite 5-10 trusted friends to point their miners at your pool over the internet.
Time required: about 15-20 minutes.
Contents
- Why DuckDNS + Let's Encrypt
- Phase 1 — Create a DuckDNS account
- Phase 2 — Configure router DDNS
- Phase 3 — Open port 3334 on the router
- Phase 4 — Run the setup script
- Phase 5 — Restart the daemon
- Phase 6 — Verify the certificate
- Phase 7 — Move your own miners over
- Phase 8 — Invite your friends
- Sleep prevention on macOS
- Maintenance
- Troubleshooting
- Security notes for this tier
- Cheat sheet
Why DuckDNS + Let's Encrypt
Bitaxe firmware in "Bundled CA" mode (the secure default) validates the server certificate against the Mozilla CA root bundle. A self-signed certificate, no matter how well crafted, is rejected. We need a real cert.
Real certs come free from Let's Encrypt — but only for a DNS name, not a
bare IP. So we also need a "name for your Mac on the internet". DuckDNS
provides exactly that: a free <yourname>.duckdns.org subdomain that always
resolves to your current home IP, even when your ISP rotates it.
What you'll have at the end:
<sub>.duckdns.orgresolves to your router's public IP, automatically- The router forwards port 3334 to your Mac
- Your Mac serves a valid Let's Encrypt cert for
<sub>.duckdns.org - Bitaxes trust the cert (because Let's Encrypt is in the bundle)
- External miners can connect with full TLS verification
Phase 1 — Create a DuckDNS account
Time: 2 minutes.
- Open https://www.duckdns.org/ in your browser.
- Sign in via GitHub, Twitter, Reddit, or Google in the top right.
- You land on your dashboard. Note:
- Token — a 32-character hex string at the top right
- The empty subdomain field with an "add domain" button next to it
- Type a subdomain name (e.g.
warppool-oliverormypool-mining— anything still free) and click "add domain". - The subdomain appears in your list. Write down:
- Subdomain (just the
warppool-oliverpart, no.duckdns.org) - Token (top right, 32 hex characters)
- Subdomain (just the
You'll need both in Phase 2 and Phase 4.
Tip: Do not post the token in Slack/Discord/etc. Anyone with it can point your subdomain at a different IP.
Phase 2 — Configure router DDNS
Time: 3 minutes.
DDNS = Dynamic DNS. Your router calls DuckDNS every few minutes to say "the IP
for warppool-oliver is now X". That keeps your subdomain pointed at home
even when your provider rotates the IP.
The exact menu paths differ per router. Below is the Fritz!Box workflow (very common in Germany). For other routers, look for "DynDNS" / "Dynamic DNS" in the WAN / Internet section.
Fritz!Box
-
Browser to http://fritz.box/ or http://192.168.178.1/.
-
Log in with your Fritz!Box password.
-
Left menu: Internet → Permit Access (or Freigaben).
-
Tab DynDNS.
-
Enable "Use DynDNS".
-
DynDNS provider: "Custom" (or "Benutzerdefiniert").
-
Update URL: paste exactly this line (replace
<SUBDOMAIN>and<TOKEN>with your values):https://www.duckdns.org/update?domains=<SUBDOMAIN>&token=<TOKEN>&ip=<ipaddr>The
<ipaddr>at the end is a placeholder that the Fritz!Box replaces with your current IP — leave it literally as<ipaddr>. -
Domain name:
<SUBDOMAIN>.duckdns.org(e.g.warppool-oliver.duckdns.org) -
User: anything (e.g.
none) — DuckDNS only checks the token. -
Password: anything (e.g.
none). -
Click Apply / Übernehmen.
Verification
In a terminal on your Mac:
dig +short warppool-oliver.duckdns.org @8.8.8.8
Should return your public IP (compare with https://www.whatismyip.com/). If empty: wait 30 seconds and retry — the router needs to push its first update.
Phase 3 — Open port 3334 on the router
Time: 3 minutes.
External miners connect to <sub>.duckdns.org:3334 → the request hits your
router → the router needs to know which device to forward it to (your Mac).
Fritz!Box
- Internet → Permit Access → Port Forwarding (Portfreigaben).
- Click "Add Device for Sharing" (or edit an existing entry if your Mac is already there).
- Select your Mac (should appear as
MacBook-Pro/mac-minietc. with IP192.168.178.10). - Click "New Sharing" → "Port Sharing".
- Application: "Other Application".
- Label:
WarpPool TLS Stratum. - Protocol: TCP.
- Port to device:
3334. - Public port:
3334. - Check "Enable sharing".
- OK → Apply.
Verification
On an external device (e.g. your phone with WiFi off, using mobile data):
nc -zv warppool-oliver.duckdns.org 3334
Should print Connection to ... port 3334 [tcp/*] succeeded!.
If it works from your Mac but not from outside, you have a NAT loopback issue on the router. Run the external test from a truly external device.
Phase 4 — Run the setup script
Time: 2 minutes.
Now we fetch the Let's Encrypt cert.
-
Open a terminal.
-
Switch to the WarpPool folder:
cd ~/code/dvb-WarpPool -
Start the script:
./scripts/setup-tls-public.sh -
The script will prompt:
? DuckDNS Subdomain (without .duckdns.org):→
warppool-oliver? DuckDNS Token (32-hex from your account settings):→ paste the token from Phase 1
-
Then it runs:
- Installs
acme.sh(the Let's Encrypt CLI client) if missing — ~30s - Fetches the cert via DNS-01 challenge:
- Sets a TXT record at DuckDNS
- Waits for DNS propagation (~30-60s)
- Let's Encrypt verifies the TXT record → confirms domain ownership
- Cert is issued and saved locally
- Installs cert + key into
~/Library/Application Support/dvb-WarpPool/tls/{cert,key}.pem - The old self-signed files are backed up as
.bak-<unix-ts> - Registers itself in cron for 60-day auto-renewal
- Installs
-
At the end you'll see:
✓ TLS setup for warppool-oliver.duckdns.org complete Next steps: 1) Restart the WarpPool daemon ... 2) On the Bitaxe (Pool Configuration): ... 3) Verify the Fritz!Box settings: ... 4) Test (from outside or on LAN): ...
Phase 5 — Restart the daemon
Time: 30 seconds.
The daemon only reads the cert at startup, so restart it once:
pkill -f dvb-warppool-daemon
sleep 2
open /Applications/dvb-WarpPool.app
After 3-5 seconds the daemon is back. Verify:
tail -20 ~/Library/Application\ Support/dvb-WarpPool/daemon.log | grep -i "tls"
Should show:
spawning TLS stratum listener bind=0.0.0.0:3334 cert=/Users/.../tls/cert.pem
stratum-v1 TLS server listening bind=0.0.0.0:3334 max_conn=64
Phase 6 — Verify the certificate
Time: 1 minute.
Check that the cert appears valid from outside.
echo | openssl s_client -connect warppool-oliver.duckdns.org:3334 \
-servername warppool-oliver.duckdns.org 2>&1 \
| grep -E "Verify return code|subject=|issuer=|Server certificate"
Good (what you want):
subject=CN=warppool-oliver.duckdns.org
issuer=C=US, O=Let's Encrypt, CN=R10
Verify return code: 0 (ok)
Bad (something didn't take):
subject=CN=dvb-WarpPool ← still the self-signed cert
Verify return code: 18 (self-signed certificate)
If you see the bad result:
- Did you actually restart the daemon?
pgrep -f dvb-warppool-daemonshould show a different PID than before the restart. - Does config.toml point at the new cert?
grep tls_cert_path ~/Library/Application\ Support/dvb-WarpPool/config.toml
Verify return code: 0 means the cert is valid — Bitaxe firmware will accept
it.
Phase 7 — Move your own miners over
Time: 2 minutes per miner.
Example for a BitForge Nano (web UI at http://192.168.178.44/):
-
Browser to
http://192.168.178.44/. -
Pool Configuration → Main Pool tab.
-
Set fields:
Field Value Stratum Host warppool-oliver.duckdns.orgStratum Port 3334Stratum User (unchanged: bc1q...YOUR_ADDRESS.BitForgeNano)Stratum Password (unchanged, e.g. x)TLS Enabled (Bundled CA) ← finally works -
Click Save.
-
Bitaxe says "must restart this device after saving" → click Restart.
-
Bitaxe reboots. After ~20 seconds it's back.
-
The Bitaxe UI should show an active pool connection again under "Status", with shares ticking.
-
In the WarpPool admin at
http://localhost:18334/: the worker table should still show the Bitaxe, with a 🔒 lock icon next to its name indicating the TLS connection. The "Active Miners" tile shows a "N TLS" sub-line counting your TLS connections.
→ Successfully migrated to TLS.
Repeat for any other miner you own (NerdOctaxe, NerdQaxe++, Avalon Q, etc.). ~2 minutes per miner.
Phase 8 — Invite your friends
Send them:
Hey, you can mine on my WarpPool:
- Pool:
warppool-oliver.duckdns.org- Port:
3334- TLS: yes, "Enabled (Bundled CA)" / "Strict TLS"
- Username:
<YOUR_OWN_bech32_address>.<any_worker_name>Solo pool — 100% of the reward goes to whoever finds the block. If the pool is down, fall back to your backup.
Important: every friend uses their own wallet address as the stratum user. WarpPool is solo mining — the reward goes directly to the address the miner authorizes with.
Sleep prevention on macOS
Once your friends are connecting from outside, the pool needs to stay reachable
24/7. By default, macOS suspends the system after 3 hours of idle time
(pmset sleep 180). While suspended:
- Bitcoin Core is frozen —
getblocktemplateandsubmitblocktime out - Stratum TCP connections drop (Bitaxes log
Connection reset by peer) - The hashrate chart shows repeated drops to 0 throughout the night, with brief recovery whenever the Mac briefly wakes for Power Nap or notifications
This is handled automatically. Since v1.0.6, the Mac launcher wraps the
daemon spawn with caffeinate -i -w <pid>:
-i= prevent system idle sleep (CPU + network + Bitcoin Core stay up)-w PID= caffeinate exits automatically when the daemon dies → no zombie processes, no manual cleanup- Display sleep keeps working — you can let the screen go dark, the screensaver runs, etc.
Verify it's active:
ps -A -o pid,ppid,command | grep "caffeinate -i -w $(pgrep -f dvb-warppool-daemon | head -1)"
You should see one caffeinate -i -w <daemon-pid> line.
The admin profile page (/admin/profile) shows a green info card "✓ macOS
sleep mode disabled" when this is active. If the card is missing, the
launcher couldn't spawn caffeinate (very unlikely — it's a system binary on
every macOS install).
What this does NOT handle
- Explicit sleep (Apple menu → Sleep, or closing a MacBook lid). The user asked for sleep, so the pool sleeps too. Open the lid / wiggle the mouse and the pool comes back within seconds (daemon stays running across short suspensions; longer suspensions trigger Bitaxe reconnects).
- Hard shutdown / power loss. Use a UPS if you're worried about this.
- Force-quit of the launcher. If you kill the launcher process directly
(
Activity Monitor → Force Quit), the daemon survives but caffeinate's parent process is gone, so caffeinate may exit. Usepkill -f dvb-warppool-daemonor re-open the.appto do a clean restart.
Power consumption
caffeinate -i is only a sleep-prevention flag — it doesn't pin the CPU
or keep the disk spinning. Your Mac mini / MacBook idles at whatever
wattage it normally would when the screen is off. The actual pool workload
(Stratum I/O + occasional Bitcoin Core RPC) is in the single-digit-watt
range on Apple Silicon. No noticeable impact on a Mac mini's electricity
bill.
Disabling sleep prevention
If you want the Mac to be allowed to sleep (e.g. you don't care about pool uptime overnight), kill the caffeinate process by hand:
pkill caffeinate
The daemon keeps running until macOS suspends it.
There's currently no config flag to disable sleep prevention — it's the correct behaviour for nearly every pool operator, and disabling it makes the pool unreliable. Open an issue if you have a real use case for opt-out.
Maintenance
Cert renewal
acme.sh does it automatically every 60 days (Let's Encrypt certs are valid
for 90 days). You don't have to do anything except restart the daemon
after a renewal happens, so it loads the new cert.
A pragmatic cron line that handles this for you:
crontab -e
Add:
0 4 * * * cd $HOME && ~/.acme.sh/acme.sh --cron > /dev/null 2>&1 && pkill -f dvb-warppool-daemon ; sleep 3 ; open /Applications/dvb-WarpPool.app
Runs daily at 4am: checks renewal status, restarts the daemon if anything changed. If there's nothing to renew, the restart causes no harm — the daemon is back in ~5s and Bitaxes reconnect automatically.
Force a renewal manually
To test:
~/.acme.sh/acme.sh --renew --domain warppool-oliver.duckdns.org --force
The --force bypasses the 60-day limit.
IP changes
As long as the router stays in place, everything keeps working — it updates DuckDNS continuously. Only when you swap the router (new ISP, new hardware) do you need to re-do the DDNS config (Phase 2).
Stopping the pool
Nothing special — close the .app or pkill -f dvb-warppool-daemon. Friends'
Bitaxes automatically fail over to backup, then come back when your pool is
back up.
Troubleshooting
Bitaxe says "TLS connection failed"
-
Router port forward gone?
nc -zv warppool-oliver.duckdns.org 3334from outside — must say "succeeded". -
DuckDNS not resolving, or pointing at the wrong IP?
dig +short warppool-oliver.duckdns.org @8.8.8.8— must show your current public IP (compare withcurl ifconfig.me). -
Cert expired? (shouldn't happen because of auto-renewal, but just in case):
echo | openssl s_client -connect warppool-oliver.duckdns.org:3334 \ -servername warppool-oliver.duckdns.org 2>&1 \ | openssl x509 -noout -datesnotAfter=...should be at least 30 days in the future. -
Bitaxe firmware cache: some Bitaxe firmware versions cache a failed cert. Power cycle the Bitaxe (unplug for 10s, replug). The reset button is not enough.
Script aborted somewhere
Running it again is OK — the script is idempotent. If e.g. acme.sh is
already installed, it's skipped.
To start completely fresh:
~/.acme.sh/acme.sh --remove --domain warppool-oliver.duckdns.org
rm -rf ~/.acme.sh/warppool-oliver.duckdns.org/
Then re-run the script.
Cert doesn't load after restart
Daemon log still shows the old cert?
lsof -p $(pgrep -f dvb-warppool-daemon) | grep cert.pem
Should point at the new cert. If not: pkill -KILL -f dvb-warppool-daemon
(instead of regular pkill) and re-open the .app.
"Rate Limit Exceeded" from Let's Encrypt
You can only issue 5 certs per week per domain. If you've been testing and issued multiple: wait, or use the staging server:
~/.acme.sh/acme.sh --issue --dns dns_duckdns --domain warppool-oliver.duckdns.org \
--server https://acme-staging-v02.api.letsencrypt.org/directory
Staging certs are NOT in the Mozilla CA bundle — they won't work for Bitaxe, but they're fine for verifying that the infrastructure is set up correctly.
Fully rolling back to self-signed
If you want to go back to LAN-only operation:
~/.acme.sh/acme.sh --remove --domain warppool-oliver.duckdns.org
cd ~/Library/Application\ Support/dvb-WarpPool/tls/
# Restore the .bak-* files the script created:
ls -la *.bak-*
# Copy the most recent cert.pem.bak-XXX and key.pem.bak-XXX back:
cp cert.pem.bak-<LATEST> cert.pem
cp key.pem.bak-<LATEST> key.pem
pkill -f dvb-warppool-daemon ; sleep 2 ; open /Applications/dvb-WarpPool.app
Then put the Bitaxe back on port 3333 without TLS (or TLS "Insecure" mode).
Security notes for this tier
This setup gives you:
- Encrypted Stratum traffic (TLS 1.3 with AES-256-GCM)
- Server identity verified via Let's Encrypt → no man-in-the-middle attack can impersonate your pool
- Friends who type the wrong hostname get a cert mismatch warning → fail-safe rather than silently failing into a stranger's pool
It does not give you:
- DDoS protection (you have a 64-connection cap, no per-IP limiting)
- Authentication beyond bech32 address format (anyone with your hostname can point a miner at you and submit shares — to their own address)
- Worker IP privacy (visible in your admin panel + logs)
For 5-10 friends this is fine. If your DuckDNS hostname leaks publicly and random people start mining to your pool, your only recourse is changing the hostname. For real abuse resistance you need tier 3 — see scaling.md.
Cheat sheet
# Initial setup (one time)
cd ~/code/dvb-WarpPool && ./scripts/setup-tls-public.sh
# Daemon restart (after each cert renewal)
pkill -f dvb-warppool-daemon ; sleep 2 ; open /Applications/dvb-WarpPool.app
# Force a renewal
~/.acme.sh/acme.sh --renew --domain warppool-oliver.duckdns.org --force
# Check cert status
echo | openssl s_client -connect warppool-oliver.duckdns.org:3334 \
-servername warppool-oliver.duckdns.org 2>&1 \
| openssl x509 -noout -dates -subject -issuer
# DNS + port reachability from outside
dig +short warppool-oliver.duckdns.org @8.8.8.8
nc -zv warppool-oliver.duckdns.org 3334
Good luck!