Skip to main content

Hardening the server

This guide covers the baseline security hardening for a Cardano stake pool server running Ubuntu 22.04 LTS. Apply it to each machine in your setup: relays, block producer, and monitoring host.

1. Non-root user

Never operate as root. Create a dedicated operator account with sudo access:

sudo useradd -m -s /bin/bash cardano-op
sudo passwd cardano-op
sudo usermod -aG sudo cardano-op

Log out and reconnect as cardano-op for all subsequent steps. Lock the root account:

sudo passwd -l root

2. SSH key authentication

On your local machine, generate an ED25519 key pair:

ssh-keygen -t ed25519 -C "stake-pool-ops"

Copy the public key to the server:

ssh-copy-id -i ~/.ssh/id_ed25519.pub cardano-op@<server-ip>

Verify you can log in with the key before continuing. Then harden /etc/ssh/sshd_config on the server:

Port 2222 # change to any unprivileged port
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin without-password
PermitEmptyPasswords no
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
Compression no
TCPKeepAlive no
KbdInteractiveAuthentication no
MaxAuthTries 3
LoginGraceTime 30

Validate and reload:

sudo sshd -t && sudo systemctl reload ssh
caution

Back up your private key before disabling password authentication. If you lose the key you will be locked out.

3. System updates

sudo apt-get update -y && sudo apt-get upgrade -y && sudo apt-get autoremove -y
sudo reboot

Enable automatic security updates (security patches only — will not reboot):

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

4. Firewall — nftables

Ubuntu ships with nftables. Create /etc/nftables.conf appropriate for your node role.

note

Replace 2222 with your actual SSH port, and adjust Cardano ports and IP ranges to match your setup.

Relay node
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
chain input {
type filter hook input priority 0; policy drop;

# established / related
ct state established,related accept

# loopback
iifname "lo" accept

# ICMP (needed for path MTU discovery)
ip protocol icmp accept
ip6 nexthdr icmpv6 accept

# SSH — restrict to your management IP if possible
tcp dport 2222 accept

# Cardano P2P — accept from any peer
tcp dport 3001 accept
}

chain forward {
type filter hook forward priority 0; policy drop;
}

chain output {
type filter hook output priority 0; policy accept;
}
}
Block producer — same-datacenter relays (no WireGuard)
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
chain input {
type filter hook input priority 0; policy drop;

ct state established,related accept
iifname "lo" accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept

# SSH — management IP only
ip saddr <your-management-ip> tcp dport 2222 accept

# Cardano — relays only
ip saddr { <relay-1-ip>, <relay-2-ip> } tcp dport 6000 accept
}

chain forward {
type filter hook forward priority 0; policy drop;
}

chain output {
type filter hook output priority 0; policy accept;
}
}
Block producer — relays in a different datacenter (WireGuard)

See section 5 — WireGuard for the WireGuard setup first, then use this ruleset which accepts Cardano traffic only from the WireGuard interface:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
chain input {
type filter hook input priority 0; policy drop;

ct state established,related accept
iifname "lo" accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept

# SSH — management IP only
ip saddr <your-management-ip> tcp dport 2222 accept

# WireGuard tunnel — accept the UDP handshake port
udp dport 51820 accept

# Cardano — only from WireGuard addresses
iifname "wg0" ip saddr { 10.0.0.2, 10.0.0.3 } tcp dport 6000 accept
}

chain forward {
type filter hook forward priority 0; policy drop;
}

chain output {
type filter hook output priority 0; policy accept;
}
}

Enable and apply:

sudo systemctl enable nftables
sudo nft -f /etc/nftables.conf
sudo nft list ruleset # verify

5. WireGuard — relay ↔ BP across datacenters

If your relays and block producer are in different datacenters, do not expose the block producer's Cardano port to the public internet. Run a WireGuard VPN between them and route Cardano traffic through the tunnel.

Install on each machine:

sudo apt-get install -y wireguard

Generate a key pair on each machine:

wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key
sudo chmod 600 /etc/wireguard/private.key

Block producer/etc/wireguard/wg0.conf:

[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <bp-private-key>

[Peer]
# relay-1
PublicKey = <relay-1-public-key>
AllowedIPs = 10.0.0.2/32
PersistentKeepalive = 25

[Peer]
# relay-2
PublicKey = <relay-2-public-key>
AllowedIPs = 10.0.0.3/32
PersistentKeepalive = 25

Relay 1/etc/wireguard/wg0.conf:

[Interface]
Address = 10.0.0.2/24
PrivateKey = <relay-1-private-key>

[Peer]
PublicKey = <bp-public-key>
Endpoint = <bp-public-ip>:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25

Enable on each machine:

sudo systemctl enable --now wg-quick@wg0

Verify the tunnel is up:

sudo wg show
ping 10.0.0.1 # from relay, should reach BP

Update your block producer's Cardano topology to use the WireGuard addresses (10.0.0.2, 10.0.0.3) instead of public IPs. The nftables ruleset in section 4 already restricts Cardano traffic to the wg0 interface.

6. fail2ban

fail2ban detects repeated login failures and bans the source IP.

sudo apt-get install -y fail2ban
sudo systemctl enable --now fail2ban

Create /etc/fail2ban/jail.local:

[DEFAULT]
bantime = 1h
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 5w
findtime = 10m
maxretry = 3

[sshd]
enabled = true
port = 2222
mode = aggressive
maxretry = 3
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd # verify

7. sysctl hardening

These settings harden the kernel's network stack for a server role. Edit /etc/sysctl.d/99-cardano.conf:

# SYN flood protection
net.ipv4.tcp_syncookies = 1

# Ignore ICMP broadcast requests (smurf protection)
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Ignore bogus ICMP error responses
net.ipv4.icmp_ignore_bogus_error_responses = 1

# Reject source-routed packets
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0

# Reject ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0

# Reverse path filtering — drop packets that appear to come from an unexpected interface
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Restrict dmesg to root
kernel.dmesg_restrict = 1

Apply:

sudo sysctl -p /etc/sysctl.d/99-cardano.conf

8. systemd unit hardening

The systemd service files in Running cardano-node already run the node as a dedicated cardano user with a RuntimeDirectory. Add these directives to the [Service] section for defense-in-depth:

NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/cardano /run/cardano
PrivateDevices=yes

Reload after editing:

sudo systemctl daemon-reload && sudo systemctl restart cardano-node

Verification checklist

# SSH key auth works, password auth rejected
ssh -o PasswordAuthentication=yes cardano-op@<server-ip> # should fail

# nftables active
sudo nft list ruleset

# WireGuard up (if applicable)
sudo wg show

# fail2ban watching SSH
sudo fail2ban-client status sshd

# sysctl applied
sysctl net.ipv4.tcp_syncookies net.ipv4.conf.all.rp_filter