Tutorial · Staking

Harden a Staking Server: Firewall, SSH Keys and Automatic Updates

A staking server holds an unlocked wallet and sits on the internet all day. Three settings close almost every door that gets tried: a firewall that allows only SSH and the Divi peer port, SSH that accepts keys and nothing else, and an operating system that patches itself. Every setting here was tested on 2026-10-21 and 22 on an Ubuntu 22.04 machine, in a spare user, a spare SSH service and a container, so that machine's own SSH and firewall never changed.

About 45 minutes · Tested on Ubuntu 22.04 · Checked 2026-10-21

1Before you start

You need a server you reach over SSH with sudo rights, and a Divi node on it (install divid or install IronDivi). How staking works explains why the wallet has to be unlocked; this page is about keeping everyone else out while it is.

Ports open to the internet
2
Ways to log in
1 key
Security patches
daily, unattended
Keep one session open the whole time

Every step below can lock you out if done in the wrong order. Stay logged in on one terminal, make each change, and test it from a second terminal before you close the first. The order that bit us, and the order that works, is called out at each step.

The ports, from the node source (chainparams.cpp and chainparamsbase.cpp in Divi Core; config.rs in IronDivi):

PortWhatFirewall
51472Mainnet peer-to-peer. Other nodes connect here.allow
51473Mainnet RPC (Divi Core). Your commands, and anyone else's if it is reachable.never
51471Mainnet RPC (IronDivi default).never
51474 / 51475Testnet peer-to-peer / RPC.allow / never
22SSH.allow

2A user for the node, and a key for you

Run the node as its own user with no password, so a bug in the node cannot become root, and log in as yourself with a key. On the server:

sudo adduser --disabled-password --gecos "" divi
If you used useradd instead, the account is locked for SSH too

We created the proof user with useradd -m, installed a key, and every login was refused with Permission denied (publickey). The server log said why: "User tutj not allowed because account is locked". useradd leaves ! in the password field and OpenSSH reads that as "locked". Fix it without setting a password: sudo usermod -p '*' divi. adduser --disabled-password writes * from the start.

On your own computer, make a key with a passphrase. Ed25519 is the type to pick (older OpenSSH, such as Ubuntu 22.04's, still defaults to RSA, so name it); -a 100 makes guessing the passphrase slow:

ssh-keygen -t ed25519 -a 100 -C "staking-admin $(date +%F)"

Put the passphrase in your password manager (the inheritance entry is a good place: your family may need to reach this server). Then copy the public half to the server, into the account you log in with:

ssh-copy-id -i ~/.ssh/id_ed25519.pub you@your-server

And test it from a second terminal before the next step:

ssh -o PasswordAuthentication=no -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 you@your-server id
# ours:
uid=1001(tutj) gid=1001(tutj) groups=1001(tutj)

3SSH: keys only, no root, no passwords

Ubuntu's /etc/ssh/sshd_config starts with Include /etc/ssh/sshd_config.d/*.conf, so put your settings in a drop-in file instead of editing the main file:

sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no
AllowUsers you
EOF
sudo sshd -t && sudo systemctl reload ssh
sudo sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|kbdinteractiveauthentication) '
# want:
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no

sshd -t checks the files and prints nothing when they are fine; a typo would otherwise stop SSH on the next restart. sshd -T prints the settings the server will actually use, which is the check that matters, because of the next callout.

The file name decides who wins

For each setting, sshd uses the first value it reads, and it reads the drop-in files in name order. On cloud servers, cloud-init writes its SSH settings, PasswordAuthentication among them, to 50-cloud-init.conf, and on many providers that value is yes. We tested exactly that: with 10-hardening.conf, sshd -T said passwordauthentication no; renamed to 99-hardening.conf, it said yes and passwords stayed on. Keep the low number, and trust sshd -T, not the file you wrote.

AllowUsers is the line that keeps the divi user, and every other account on the box, off SSH entirely. Now, from the second terminal, prove each closed door:

# a password attempt, keys off on purpose
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password -o BatchMode=yes you@your-server true
you@your-server: Permission denied (publickey).
# root, even with your key
ssh -i ~/.ssh/id_ed25519 root@your-server true
root@your-server: Permission denied (publickey).
# a user not on AllowUsers, with your key
ssh -i ~/.ssh/id_ed25519 divi@your-server true
divi@your-server: Permission denied (publickey).
# and you, with the key
ssh -i ~/.ssh/id_ed25519 you@your-server hostname

The word in brackets is the list of methods the server still offers. (publickey) alone means password login is gone. Those four results are what our spare SSH service on port 54099 answered with the settings above and the key from step 2. We ran it as a second SSH service with its own config file rather than editing the machine's real one, so the tee and reload lines are the standard steps, not ones we ran there.

Order

Key installed and tested (step 2) before PasswordAuthentication no. If you turn passwords off first and the key does not work, the only way back in is the hosting provider's console.

4Firewall: SSH and the peer port, nothing else

ufw is installed on Ubuntu and is a thin front for the kernel firewall. Two defaults and two rules, then switch it on:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 51472/tcp
sudo ufw enable
sudo ufw status verbose

Ours, on ufw 0.36.1:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
51472/tcp                  ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)
51472/tcp (v6)             ALLOW IN    Anywhere (v6)
Order

ufw allow 22/tcp (or whatever port your SSH is on) before ufw enable. Enabling with "deny incoming" and no SSH rule cuts your own session at the next packet.

Then test from another machine. The difference between the two failures below is how you will tell "firewall" from "nothing running" for the rest of the server's life:

# peer port: allowed and listening
nc -z -w 5 your-server 51472 && echo open
open
# RPC: the firewall drops the packet,
# so this waits the full 5 s, then fails
nc -z -w 5 your-server 51473
# allowed, nothing listening
# (in our test box):
# refused at once, 0.0 s
nc -z -w 5 your-server 22

That timing came from our test: the rules above inside a fresh Ubuntu 22.04 container, a listener on 51472 and 51473, and nc from the host. 51472 connected, 51473 waited the full five seconds, and 22 was refused instantly because the container ran no SSH. On your real server, 22 will of course connect.

Do not add the RPC port, ever. The node does not need it open to stake, and an open RPC port with a weak password is a wallet-emptying hole. If you run the node on testnet, allow 51474 instead of 51472.

5Automatic updates

Ubuntu's unattended-upgrades installs security patches on its own. It is usually present; make sure, and turn it on:

sudo apt-get install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades

The last command should print:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

Then do a dry run to see what it would do and where it looks:

sudo unattended-upgrade --dry-run -d 2>&1 | grep -i "allowed origins\|will be upgraded\|No packages"

On our fresh container it listed Allowed origins are: o=Ubuntu,a=jammy, o=Ubuntu,a=jammy-security, … and Packages that will be upgraded: liblzma5 libudev1; on a machine that is already patched it says "No packages found that can be upgraded unattended and no pending auto-removals". Two timers drive it, apt-daily.timer (fetch) and apt-daily-upgrade.timer (install); systemctl list-timers apt-daily* shows when they next fire.

Two things it does not do

It does not reboot. Automatic-Reboot is "false" by default in /etc/apt/apt.conf.d/50unattended-upgrades, and for a staking server leave it that way: a reboot comes back with the wallet locked and staking stopped until you unlock it. Kernel updates will tell you a reboot is pending; plan it. It does not update the node. divid and irondivid are not packages; see Upgrade a node, and put it on the yearly checkup.

6The node itself: RPC on localhost, a staking-only unlock

RPC stays on the loopback address. Both nodes do this by default: Divi Core binds RPC to loopback unless rpcallowip is set (and logs a warning that it ignored rpcbind otherwise), and IronDivi's default RPC listen address is 127.0.0.1. Check it, and leave it:

ss -tlnp | grep -E 'divid|irondivid' | awk '{print $4, $6}'
# ours, two testnet nodes on custom ports.
# 0.0.0.0 = every interface (peer port),
# 127.0.0.1 = this machine only (RPC)
0.0.0.0:54000 users:(("irondivid",…
127.0.0.1:54001 users:(("irondivid",…
0.0.0.0:54010 users:(("divid",…
127.0.0.1:54011 users:(("divid",…
Do not set rpcallowip "just to be safe"

Divi Core's rule (rpcserver.cpp) is: no rpcallowip, bind loopback; rpcallowip set and no rpcbind, bind every address. A third lane node with rpcallowip=127.0.0.1 in its conf showed up in ss as *:54031: the allow list still rejects other hosts, but the port is now visible to the network and the firewall is the only thing hiding it. If you must have the line, add rpcbind=127.0.0.1 next to it.

The RPC password lives in divi.conf or irondivi.conf; make the file chmod 600, owned by the node's user.

Unlock for staking only. Divi Core's walletpassphrase takes a third argument. With it, the wallet can sign stakes but not spend, so a stolen RPC session, or a mistake of your own, cannot send the coins. A timeout of 0 keeps it unlocked "until the wallet is closed", which is what a staking server wants. Do not be alarmed by unlocked_until: with timeout 0 it shows the moment you unlocked, not an expiry, because no relock timer is set (rpcserver.cpp).

 ./divi-cli walletpassphrase "your wallet passphrase" 0 true
 ./divi-cli getwalletinfo | grep encryption_status
"encryption_status": "unlocked-for-staking",

Proof that it is really spend-proof, from our lane node:

./divi-cli sendtoaddress yDaA4oLzqvYMWEoqExAyde1ggUoKG4pKAV 1
error code: -13
error message:
Error: Please enter the wallet passphrase with walletpassphrase first.

That is the same error a fully locked wallet gives, so getwalletinfo is how you tell the two apart. getstakingstatus reports "walletunlocked": true in this state. IronDivi's walletpassphrase takes only the passphrase and a timeout (its help text shows no third argument), so on IronDivi a staking unlock is a full unlock; keep RPC on localhost and the conf file private, which is what the rest of this page is for.

The leading space is not a typo

A command typed at the shell is saved in ~/.bash_history, passphrase included. Ubuntu's default .bashrc sets HISTCONTROL=ignoreboth, which skips any line that starts with a space. So type a space before ./divi-cli walletpassphrase …, as in the example above, or run the command through a small script that reads the passphrase from a file only your user can read.

If something goes wrong

You seeWhat it means and what to do
Permission denied (publickey) for the user you just created, key installedAccount locked by useradd; the server's /var/log/auth.log says "not allowed because account is locked". sudo usermod -p '*' user.
Permission denied (publickey) for yourselfWrong key, wrong user, or the ~/.ssh folder on the server is not mode 700 / authorized_keys not 600. Use the first terminal you kept open to fix it.
sshd -t prints an errorDo not reload. Fix the line it names; the running service is untouched until you reload.
Locked out after ufw enableNo SSH rule before enabling. Use the provider's console: sudo ufw allow 22/tcp.
nc to the peer port times outRule missing, or the node is not running. sudo ufw status and ss -tlnp on the server answer which.
Error: Wallet is already unlocked for staking.You ran the staking-only unlock twice. Harmless. walletlock first if you want to change the timeout.
Staking stopped after a nightThe server rebooted (kernel update) or the unlock had a timeout. getwalletinfo shows locked; unlock again with timeout 0.
WARNING: option -rpcbind was ignored because -rpcallowip was not specifiedDivi Core refused to expose RPC, which is the behaviour you want. Remove the rpcbind line.