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.
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):
| Port | What | Firewall |
|---|---|---|
| 51472 | Mainnet peer-to-peer. Other nodes connect here. | allow |
| 51473 | Mainnet RPC (Divi Core). Your commands, and anyone else's if it is reachable. | never |
| 51471 | Mainnet RPC (IronDivi default). | never |
| 51474 / 51475 | Testnet peer-to-peer / RPC. | allow / never |
| 22 | SSH. | 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
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.
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.
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)
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.
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",…
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.
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 see | What it means and what to do |
|---|---|
Permission denied (publickey) for the user you just created, key installed | Account 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 yourself | Wrong 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 error | Do not reload. Fix the line it names; the running service is untouched until you reload. |
Locked out after ufw enable | No SSH rule before enabling. Use the provider's console: sudo ufw allow 22/tcp. |
nc to the peer port times out | Rule 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 night | The 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 specified | Divi Core refused to expose RPC, which is the behaviour you want. Remove the rpcbind line. |