Skip to content

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 bgoldd full node (P2P on 8338)
  • A public Electrum server (50001/50002/50003)
  • A public Blockbook indexer (via nginx on 443)
  • 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

sudo apt update
sudo apt upgrade -y
sudo apt autoremove -y     # especially for old kernel images

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:

systemctl status apt-daily.timer
systemctl status apt-daily-upgrade.timer

NTP

A node that drifts more than ~10 minutes past real time will reject blocks. systemd-timesyncd is the default.

sudo timedatectl set-ntp true
timedatectl status

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:

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing

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 AllowUsers if 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:

sudo sshd -t && sudo systemctl reload ssh

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:

AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=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 --upgradable shows no security updates
  • timedatectl shows NTP synchronized
  • sudo ufw status verbose matches your intent
  • sshd -T | grep -E 'port|passwordauth|permitroot' shows your hardened values
  • systemctl status fail2ban shows active (if you've completed section 4)
  • Each service's systemctl show <unit> -p NoNewPrivileges,PrivateTmp shows the directives applied
  • openssl x509 -in /etc/letsencrypt/live/bitcoingold.services/cert.pem -noout -dates shows a cert that is not within 30 days of expiry, AND systemctl list-timers | grep certbot shows 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.io or a self-hosted equivalent.
  • Set up backups for wallet.dat and 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.

Source / further reading