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

  1. Why DuckDNS + Let's Encrypt
  2. Phase 1 — Create a DuckDNS account
  3. Phase 2 — Configure router DDNS
  4. Phase 3 — Open port 3334 on the router
  5. Phase 4 — Run the setup script
  6. Phase 5 — Restart the daemon
  7. Phase 6 — Verify the certificate
  8. Phase 7 — Move your own miners over
  9. Phase 8 — Invite your friends
  10. Sleep prevention on macOS
  11. Maintenance
  12. Troubleshooting
  13. Security notes for this tier
  14. 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.org resolves 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.

  1. Open https://www.duckdns.org/ in your browser.
  2. Sign in via GitHub, Twitter, Reddit, or Google in the top right.
  3. 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
  4. Type a subdomain name (e.g. warppool-oliver or mypool-mining — anything still free) and click "add domain".
  5. The subdomain appears in your list. Write down:
    • Subdomain (just the warppool-oliver part, no .duckdns.org)
    • Token (top right, 32 hex characters)

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

  1. Browser to http://fritz.box/ or http://192.168.178.1/.

  2. Log in with your Fritz!Box password.

  3. Left menu: Internet → Permit Access (or Freigaben).

  4. Tab DynDNS.

  5. Enable "Use DynDNS".

  6. DynDNS provider: "Custom" (or "Benutzerdefiniert").

  7. 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>.

  8. Domain name: <SUBDOMAIN>.duckdns.org (e.g. warppool-oliver.duckdns.org)

  9. User: anything (e.g. none) — DuckDNS only checks the token.

  10. Password: anything (e.g. none).

  11. 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

  1. Internet → Permit Access → Port Forwarding (Portfreigaben).
  2. Click "Add Device for Sharing" (or edit an existing entry if your Mac is already there).
  3. Select your Mac (should appear as MacBook-Pro / mac-mini etc. with IP 192.168.178.10).
  4. Click "New Sharing""Port Sharing".
  5. Application: "Other Application".
  6. Label: WarpPool TLS Stratum.
  7. Protocol: TCP.
  8. Port to device: 3334.
  9. Public port: 3334.
  10. Check "Enable sharing".
  11. OKApply.

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.

  1. Open a terminal.

  2. Switch to the WarpPool folder:

    cd ~/code/dvb-WarpPool
    
  3. Start the script:

    ./scripts/setup-tls-public.sh
    
  4. 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

  5. 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
  6. 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-daemon should 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/):

  1. Browser to http://192.168.178.44/.

  2. Pool Configuration → Main Pool tab.

  3. Set fields:

    FieldValue
    Stratum Hostwarppool-oliver.duckdns.org
    Stratum Port3334
    Stratum User(unchanged: bc1q...YOUR_ADDRESS.BitForgeNano)
    Stratum Password(unchanged, e.g. x)
    TLSEnabled (Bundled CA) ← finally works
  4. Click Save.

  5. Bitaxe says "must restart this device after saving" → click Restart.

  6. Bitaxe reboots. After ~20 seconds it's back.

  7. The Bitaxe UI should show an active pool connection again under "Status", with shares ticking.

  8. 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 — getblocktemplate and submitblock time 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. Use pkill -f dvb-warppool-daemon or re-open the .app to 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"

  1. Router port forward gone? nc -zv warppool-oliver.duckdns.org 3334 from outside — must say "succeeded".

  2. 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 with curl ifconfig.me).

  3. 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 -dates
    

    notAfter=... should be at least 30 days in the future.

  4. 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!