Skip to main content

Validator Server Hardening Guide

Production validators are high-value targets. A compromised validator can be jailed, have its stake at risk, or be used to attack the network. This guide covers the security hardening every Vexidus validator should apply before going live.


Quick Hardening Checklist

StepPriorityTime
Firewall (UFW)Critical5 min
SSH HardeningCritical10 min
Fail2banCritical5 min
Dedicated UserCritical5 min
Automatic UpdatesHigh2 min
RPC LockdownHigh5 min
Key PermissionsHigh2 min
Systemd HardeningMedium5 min
Log MonitoringMedium10 min
Backup StrategyMedium15 min

Firewall (UFW)

Every validator must run a firewall. The principle: deny everything, allow only what's needed.

Install and configure

sudo apt update && sudo apt install -y ufw

# Default policy: deny all incoming, allow all outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (required for remote management)
sudo ufw allow 22/tcp

# Allow P2P (required for consensus — QUIC uses UDP)
# Use your actual P2P port (default 9944)
sudo ufw allow 9944/udp

# Allow HTTP/HTTPS only if you're running a web server (explorer, docs, etc.)
# Most validators do NOT need these:
# sudo ufw allow 80/tcp
# sudo ufw allow 443/tcp

# Enable the firewall
sudo ufw enable
sudo ufw status verbose

What NOT to expose

PortServiceExpose?
9944/udpP2P (QUIC)Yes — required for consensus
9933/tcpRPCNo — localhost only (see RPC Lockdown)
9100/tcpPrometheus metricsNo — localhost only
22/tcpSSHYes — but harden it (see below)

Verify

sudo ufw status numbered

You should see only SSH and your P2P port allowed.


SSH Hardening

SSH is the #1 attack vector for internet-facing servers. These settings eliminate the most common attacks.

Prerequisites

Before changing SSH config, ensure you have key-based auth working:

# On your LOCAL machine, generate a key if you don't have one
ssh-keygen -t ed25519 -C "validator-admin"

# Copy your public key to the server
ssh-copy-id -i ~/.ssh/id_ed25519.pub your_user@your_server_ip

# Test key-based login (should not ask for password)
ssh -i ~/.ssh/id_ed25519 your_user@your_server_ip
danger

Do NOT disable password auth until you've confirmed key-based auth works. You will lock yourself out.

Apply hardening

Edit /etc/ssh/sshd_config:

sudo nano /etc/ssh/sshd_config

Set these values (uncomment if needed):

# Disable password authentication (key-only)
PasswordAuthentication no
ChallengeResponseAuthentication no

# Disable root login (use a regular user + sudo)
PermitRootLogin no

# Limit authentication attempts
MaxAuthTries 3
MaxSessions 3

# Disable unused features
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no

# Use only protocol 2
Protocol 2

# Idle timeout (disconnect after 10 minutes of inactivity)
ClientAliveInterval 300
ClientAliveCountMax 2

If your server only has a root user (no separate admin user), set PermitRootLogin prohibit-password instead of no — this allows key-based root login while still blocking password auth.

Check for cloud-init override

Some cloud providers (OVH, Hetzner) override SSH settings via cloud-init. Check:

ls /etc/ssh/sshd_config.d/
cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null

If you see PasswordAuthentication yes in any file there, change it to no:

sudo sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config.d/*.conf

Restart SSH

# Keep your current session open! Test in a NEW terminal first.
sudo systemctl restart sshd

# In a NEW terminal, verify you can still log in:
ssh -i ~/.ssh/id_ed25519 your_user@your_server_ip

Fail2ban

Fail2ban automatically bans IPs that show malicious behavior (brute-force SSH attempts, etc.).

Install and configure

sudo apt install -y fail2ban

Create /etc/fail2ban/jail.local:

[DEFAULT]
bantime = 86400
findtime = 600
maxretry = 3
backend = systemd

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400

This bans any IP that fails SSH login 3 times within 10 minutes, for 24 hours.

Start and enable

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Verify
sudo fail2ban-client status sshd

Check banned IPs

sudo fail2ban-client status sshd

Unban an IP (if you accidentally lock yourself out from another machine)

sudo fail2ban-client set sshd unbanip YOUR_IP

Dedicated System User

Never run your validator as root. Create a dedicated system user with no login shell:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin vexidus
sudo mkdir -p /opt/vexidus/data
sudo chown -R vexidus:vexidus /opt/vexidus

Your systemd service should specify User=vexidus and Group=vexidus.


Automatic Security Updates

Enable unattended security patches so critical vulnerabilities are patched automatically:

sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

Verify it's active:

cat /etc/apt/apt.conf.d/20auto-upgrades

You should see:

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

RPC Lockdown

Your RPC port (default 9933) gives full read access to chain state and can submit transactions. Never expose it to the public internet.

The firewall config above already blocks port 9933. If you need to query RPC remotely, use an SSH tunnel:

# From your local machine
ssh -L 9933:localhost:9933 your_user@your_server_ip

# Now query via localhost on your machine
curl http://localhost:9933 -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'

Option B: Nginx reverse proxy with rate limiting

If you need to expose RPC publicly (e.g., for an explorer), put it behind nginx with rate limiting:

# /etc/nginx/sites-available/vexidus-rpc
upstream vexidus_rpc {
server 127.0.0.1:9933;
}

limit_req_zone $binary_remote_addr zone=rpc:10m rate=30r/s;

server {
listen 443 ssl;
server_name rpc.yourvalidator.com;

ssl_certificate /etc/letsencrypt/live/rpc.yourvalidator.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rpc.yourvalidator.com/privkey.pem;

location / {
limit_req zone=rpc burst=60 nodelay;
proxy_pass http://vexidus_rpc;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

This limits each IP to 30 requests/second with a burst of 60. Adjust based on your expected traffic.


Key File Permissions

Your validator key is the most sensitive file on the server. Lock it down:

# Owned by the vexidus user, readable only by owner
sudo chown vexidus:vexidus /opt/vexidus/validator.key
sudo chmod 600 /opt/vexidus/validator.key

# Verify
ls -la /opt/vexidus/validator.key
# Should show: -rw------- 1 vexidus vexidus

Also secure any .env files or configuration files that contain sensitive data:

sudo chmod 600 /opt/vexidus/validator.toml

Systemd Hardening

Add security directives to your systemd service file (/etc/systemd/system/vexidus-validator.service):

[Service]
# ... your existing ExecStart, User, etc. ...

# Security hardening
ProtectSystem=full
ProtectHome=read-only
NoNewPrivileges=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true

# File descriptor limit (RocksDB needs many open files)
LimitNOFILE=65536

After editing:

sudo systemctl daemon-reload
sudo systemctl restart vexidus-validator

Log Monitoring

Watch for suspicious activity

# Failed SSH attempts
sudo grep "Failed password" /var/log/auth.log | tail -20

# Successful logins
sudo grep "Accepted" /var/log/auth.log | tail -20

# fail2ban actions
sudo grep "Ban" /var/log/fail2ban.log | tail -20

Simple alerting script

Save as /opt/vexidus/alert-check.sh:

#!/bin/bash
# Run via cron every 5 minutes: */5 * * * * /opt/vexidus/alert-check.sh

RPC="http://localhost:9933"
THRESHOLD=60 # Alert if no new block in 60 seconds

# Check if node is running
if ! systemctl is-active --quiet vexidus-validator; then
echo "ALERT: vexidus-validator is DOWN" | logger -t vexidus-alert
exit 1
fi

# Check block production
BLOCK=$(curl -sf "$RPC" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"vex_getNetworkStats","params":[],"id":1}' \
| jq -r '.result.blockHeight // 0')

if [ "$BLOCK" = "0" ]; then
echo "ALERT: RPC unreachable or returning 0 blocks" | logger -t vexidus-alert
fi

# Check disk usage
DISK_PCT=$(df /opt/vexidus/data | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK_PCT" -gt 85 ]; then
echo "ALERT: Disk usage at ${DISK_PCT}%" | logger -t vexidus-alert
fi
chmod +x /opt/vexidus/alert-check.sh
# Add to cron
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/vexidus/alert-check.sh") | crontab -

For production alerting, integrate with services like PagerDuty, Grafana alerts, or a simple webhook to Slack/Discord.


Backup Strategy

What to back up

ItemLocationFrequencyCritical?
Validator key/opt/vexidus/validator.keyOnce (store offline)YES — loss = unrecoverable stake
Node identity/opt/vexidus/data/node_keyOncePreserves PeerId across restarts
Chain state/opt/vexidus/data/WeeklyCan re-sync from peers if lost
Config files/opt/vexidus/validator.toml, systemd serviceOn changeEasy to recreate

Automated backup script

#!/bin/bash
# /opt/vexidus/backup.sh — run weekly via cron
BACKUP_DIR="/opt/vexidus/backups"
DATE=$(date +%Y%m%d)

mkdir -p "$BACKUP_DIR"

# Back up config and key (encrypted)
tar czf "$BACKUP_DIR/config-$DATE.tar.gz" \
/opt/vexidus/validator.key \
/opt/vexidus/validator.toml \
/etc/systemd/system/vexidus-validator.service

# Keep only last 4 backups
ls -t "$BACKUP_DIR"/config-*.tar.gz | tail -n +5 | xargs rm -f 2>/dev/null

echo "Backup complete: $BACKUP_DIR/config-$DATE.tar.gz"
Offsite backups

Store an encrypted copy of your validator key in a separate location (encrypted USB, password manager, or cloud storage with client-side encryption). Never store keys in plaintext on cloud services.


Advanced Hardening (Optional)

Restrict SSH to specific IPs

If you always connect from the same IP or VPN:

sudo ufw delete allow 22/tcp
sudo ufw allow from YOUR_HOME_IP to any port 22 proto tcp

Disable IPv6 (if not needed)

echo "net.ipv6.conf.all.disable_ipv6 = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Install and run Lynis (security audit)

sudo apt install -y lynis
sudo lynis audit system

Review the report and address any warnings relevant to your setup.

Rate limit SSH connections at the firewall level

# Limit SSH to 6 connection attempts per 30 seconds per IP
sudo ufw limit ssh

This is in addition to fail2ban and provides defense-in-depth.


Verification Checklist

After hardening, verify everything is correct:

# Firewall is active and configured
sudo ufw status verbose

# SSH password auth is disabled
sudo sshd -T | grep passwordauthentication
# Should output: passwordauthentication no

# Fail2ban is running
sudo fail2ban-client status sshd

# Node is running as dedicated user
ps aux | grep vexidus-node | grep -v grep
# Should show 'vexidus' user, NOT root

# Key file has correct permissions
ls -la /opt/vexidus/validator.key
# Should show: -rw------- 1 vexidus vexidus

# RPC is not externally accessible
# From another machine:
curl --connect-timeout 5 http://YOUR_SERVER_IP:9933
# Should timeout or connection refused

# P2P port is open
# From another machine:
nc -zuv YOUR_SERVER_IP 9944
# Should show connection succeeded

Summary

LayerWhatWhy
FirewallUFW deny-all + whitelist P2P/SSHBlocks all unnecessary ports
SSHKey-only, no root, rate limitedEliminates brute-force attacks
Fail2banAuto-ban after 3 failuresBlocks persistent attackers
User isolationDedicated vexidus userLimits blast radius of compromise
RPC lockdownLocalhost onlyPrevents unauthorized chain queries/transactions
Auto-updatesUnattended security patchesCloses known OS vulnerabilities
MonitoringLog checks + alertingEarly detection of issues
BackupsKey + config, offsiteRecovery from data loss

These measures follow the principle of defense-in-depth — no single layer is sufficient alone, but together they make your validator significantly harder to compromise.


Questions? Join the Vexidus validator community or open an issue at github.com/Vexidus-Labs/Vexidus.