Server hardening¶
The default Ubuntu install is not safe for a public-facing Bitcoin Gold (BTG) service. This course walks you through the changes you need to make on any host that runs:
- A public
bgolddfull node (P2P on8338) - A public Electrum server (
50001/50002/50003) - A public Blockbook indexer (via
nginxon443) - A public DNS seeder (UDP/53)
- A mining pool (Stratum on
3333/3334/3335)
The end state matches what's in production at bitcoingold.services.
1. OS basics¶
Keep the system up to date¶
Enable unattended-upgrades for security patches:
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades # answer "Yes"
Verify the timer is active:
NTP¶
A node that drifts more than ~10 minutes past real time will reject blocks. systemd-timesyncd is the default.
Disable root login over SSH¶
sudo nano /etc/ssh/sshd_config
# Set:
# PermitRootLogin no
# PasswordAuthentication no
# KbdInteractiveAuthentication no
sudo systemctl restart sshd
Test before closing your current session
If you break SSH while locked out as root, recovery is via the hypervisor console. Test in a second session first.
2. Firewall: ufw¶
ufw is the simplest path to a sane default-deny firewall on Ubuntu. The pattern:
Open only what you actually expose¶
| Service | Ports | Comment |
|---|---|---|
| SSH (change to non-22) | <your-port>/tcp |
"Bitcoin Gold (BTG) SSH" |
| Bitcoin Gold (BTG) P2P | 8338/tcp |
"Bitcoin Gold (BTG) P2P" |
| Bitcoin Gold (BTG) testnet P2P | 18338/tcp |
only if you run testnet |
| ElectrumX TCP | 50001/tcp |
"Electrum TCP" |
| ElectrumX SSL | 50002/tcp |
"Electrum SSL" |
| ElectrumX WS | 50003/tcp |
"Electrum WS" |
| Stratum | 3333-3335/tcp |
"Pool" (if applicable) |
| DNS | 53/udp + 53/tcp |
"DNS" (only on the seeder host) |
| HTTP/HTTPS | 80, 443/tcp |
"Web" (only on the Blockbook / web host) |
Example for a full public-infrastructure host:
sudo ufw allow 8338/tcp comment "Bitcoin Gold (BTG) P2P"
sudo ufw allow 50001/tcp comment "ElectrumX TCP"
sudo ufw allow 50002/tcp comment "ElectrumX SSL"
sudo ufw allow 50003/tcp comment "ElectrumX WS"
sudo ufw allow 3333/tcp comment "Stratum"
sudo ufw enable
sudo ufw status verbose
Don't open 8332 (RPC) to the network
RPC is for local services only. bitcoingold.conf should have rpcbind=127.0.0.1 and rpcallowip=127.0.0.1. Never open 8332 in ufw.
3. SSH hardening¶
Beyond disabling root login:
- Move SSH to a non-standard port (e.g. 2222 or 4444). Reduces noise from automated scanners.
- Use key-only auth (no passwords).
- Limit users with
AllowUsersif the host is single-purpose.
# /etc/ssh/sshd_config.d/hardening.conf
Port 4444
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
X11Forwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
LoginGraceTime 30
Reload:
4. fail2ban¶
Even with key-only auth, monitoring SSH and any web admin endpoint for failed authentications is worth it.
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.d/btg.conf > /dev/null <<'EOF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = 4444
logpath = %(sshd_log)s
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
5. systemd sandboxing¶
Every service in the operator courses uses these directives — they cost nothing and significantly reduce blast radius if the daemon is compromised.
[Service]
# Don't gain new privileges
NoNewPrivileges=true
# Private /tmp and /var/tmp
PrivateTmp=true
# /usr, /boot, /etc read-only
ProtectSystem=full
# /home, /root, /run/user inaccessible
ProtectHome=true
# Only allow writes to specific paths
ReadWritePaths=/var/lib/bitcoingold
# /dev only contains pseudo-devices
PrivateDevices=true
# Cannot create new executable memory mappings
MemoryDenyWriteExecute=true
# Restrict syscall set
SystemCallArchitectures=native
SystemCallFilter=@system-service
# Drop dangerous capabilities
CapabilityBoundingSet=
AmbientCapabilities=
For services that need to bind a privileged port (e.g. DNS seeder on 53), keep only CAP_NET_BIND_SERVICE:
6. Auto-updates (security only)¶
The default unattended-upgrades config from dpkg-reconfigure covers -security and -updates. For Bitcoin Gold (BTG) hosts that are not on the critical uptime path (Blockbook, dashboard, web), accept these without manual review.
For the full node itself, don't auto-update bgoldd — see the Full node course for the manual upgrade flow. The reason: a forced upgrade during a reorg can leave the daemon on a stale chain briefly.
7. Cert renewal (and why this server sidesteps it)¶
The server in production at bitcoingold.services previously had snap.certbot.renew.timer failing, and the wildcard cert (*.bitcoingold.services) expired 2026-05-09 because the renewal was using the manual plugin without a --manual-auth-hook. The cert was manually renewed on 2026-06-02 and now expires 2026-08-31 (verified via openssl s_client against library.bitcoingold.services:443), but the underlying snap.certbot.renew.timer is still failing — it has not been migrated to DNS-01, so renewal will break again in ~90 days.
The library subdomain (library.bitcoingold.services) is covered by the wildcard cert at /etc/letsencrypt/live/bitcoingold.services/, so it doesn't need its own renewal. To bring the wildcard renewal back online so it stops being a recurring fire-drill:
# Switch to the DNS-01 challenge via Cloudflare (or your DNS provider)
sudo snap install certbot-dns-cloudflare
sudo tee /etc/letsencrypt/cloudflare.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = <your-CF-token>
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d bitcoingold.services \
-d "*.bitcoingold.services"
Then re-enable the snap timer:
sudo systemctl unmask snap.certbot.renew.timer
sudo systemctl enable --now snap.certbot.renew.timer
sudo systemctl list-timers | grep certbot
For subdomains that don't fit the wildcard, expose port 80 to Let's Encrypt's HTTP-01 challenge (the ufw allow 80/tcp rule is enough).
8. Logrotate¶
Daemons that write a lot to disk (full node, ElectrumX, Blockbook) need their logs capped. See the per-service logrotate config in each course.
The system-wide default (/etc/logrotate.d/rsyslog) handles syslog; for application logs you need explicit configs.
9. AppArmor (optional)¶
Ubuntu 22.04+ has AppArmor by default. For daemons, write a profile that denies anything not in the service's expected path set. The aa-status tool shows which profiles are loaded.
A practical minimum for the Bitcoin Gold (BTG) node:
sudo apt install -y apparmor-utils
sudo aa-genprof /usr/local/bin/bgoldd
# Interactive — answer prompts to record then enforce
This is a 30-minute exercise; the systemd directives above cover 90 % of the benefit for far less time.
10. What a hardened Bitcoin Gold (BTG) host looks like¶
| Layer | Setting |
|---|---|
| OS | Ubuntu 22.04 LTS, security updates auto-applied |
| NTP | systemd-timesyncd, drift < 100 ms |
| Firewall | ufw default deny, only service ports open |
| SSH | Non-standard port, key-only, no root |
| fail2ban | Not yet installed on the production bitcoingold.services host — apply section 4 above. The reference target is: active on SSH + nginx-http-auth. |
| Service sandboxing | NoNewPrivileges, PrivateTmp, ProtectSystem=full, ReadWritePaths only |
| TLS | Wildcard Let's Encrypt cert, DNS-01 renewal (timer still broken — see section 7) |
| Monitoring | systemd journal + logrotate + (planned) fail2ban logs (consider adding Prometheus / Grafana) |
| Backups | Off-host copies of wallet.dat (Core), peers.dat (seeder), blockchaincfg.json (Blockbook) |
Verification checklist¶
After applying this course, confirm:
-
apt list --upgradableshows no security updates -
timedatectlshows NTP synchronized -
sudo ufw status verbosematches your intent -
sshd -T | grep -E 'port|passwordauth|permitroot'shows your hardened values -
systemctl status fail2banshows active (if you've completed section 4) - Each service's
systemctl show <unit> -p NoNewPrivileges,PrivateTmpshows the directives applied -
openssl x509 -in /etc/letsencrypt/live/bitcoingold.services/cert.pem -noout -datesshows a cert that is not within 30 days of expiry, ANDsystemctl list-timers | grep certbotshows the renewal timer is healthy
What to do next¶
- Wire monitoring in: Prometheus node-exporter + blackbox-exporter for the P2P port, Grafana dashboards.
- Add alerting: dead-man-switch via
healthchecks.ioor a self-hosted equivalent. - Set up backups for
wallet.datand any stateful service data. - Test disaster recovery: a restored node should re-sync from the network, not from a corrupted chainstate.
- Add a SIEM if you operate > 3 hosts — even a simple Loki + Grafana stack catches most anomalies.