Skip to content

Production Hardening

Extra hardening and tuning to apply after the Setup Server guide. These steps are optional but strongly recommended for any server exposed to the internet.


Automatic Security Updates

Enable unattended security patches so critical vulnerabilities are patched without manual intervention.

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

Select Yes when prompted. Verify it's enabled:

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

Expected output:

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

To also enable automatic reboots when a kernel update requires it (e.g. overnight at 4 AM):

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Uncomment and set:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Warning

Automatic reboots will restart your K3s node and all game server pods on it. Agones will reschedule pods on other nodes if available. For single-node setups, schedule reboots during maintenance windows.


Fail2Ban

Blocks IPs that repeatedly fail SSH login attempts. Even with password auth disabled, this reduces log noise and resource waste from brute-force bots.

sudo apt install -y fail2ban

Create a local config (survives package upgrades):

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 3

[sshd]
enabled = true
port    = ssh
filter  = sshd
logpath = /var/log/auth.log

Start and enable:

sudo systemctl enable --now fail2ban

Check banned IPs:

sudo fail2ban-client status sshd

SSH Hardening

Additional SSH lockdown beyond disabling password auth.

sudo nano /etc/ssh/sshd_config
MaxAuthTries 3
MaxSessions 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers ubuntu
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
Setting Effect
MaxAuthTries 3 Disconnect after 3 failed auth attempts per connection
MaxSessions 3 Limit multiplexed sessions per connection
LoginGraceTime 30 30 seconds to authenticate before disconnect
ClientAliveInterval 300 Send keepalive every 5 minutes
ClientAliveCountMax 2 Drop connection after 2 missed keepalives (10 min idle)
AllowUsers ubuntu Only ubuntu can SSH in — blocks all other usernames
X11Forwarding no Disable X11 forwarding (not needed on a headless server)

Restart SSH:

sudo sshd -t && (sudo systemctl restart ssh 2>/dev/null || sudo systemctl restart sshd)

Warning

Test in a new terminal before closing your current session.


Swap

Game servers and the backend can spike in memory usage. A small swap file prevents the OOM killer from terminating processes during spikes.

Check if swap exists:

swapon --show

If empty, create a 2 GB swap file:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Set swappiness low so the kernel prefers RAM and only uses swap under pressure:

echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.d/99-mip.conf
sudo sysctl -p /etc/sysctl.d/99-mip.conf

Kernel Tuning for Game Servers

Tune network and file descriptor limits for high-throughput UDP game traffic.

sudo nano /etc/sysctl.d/99-mip.conf
# Increase UDP buffer sizes for game server traffic
net.core.rmem_max = 26214400
net.core.rmem_default = 1048576
net.core.wmem_max = 26214400
net.core.wmem_default = 1048576

# Increase connection tracking for many concurrent players
net.netfilter.nf_conntrack_max = 131072

# Increase the backlog queue for incoming packets
net.core.netdev_max_backlog = 5000

# Increase max open files system-wide
fs.file-max = 2097152

# Reduce TIME_WAIT socket accumulation
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15

# Protect against SYN flood attacks
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 4096

# Ignore ICMP redirects (prevents MITM routing attacks)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

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

# Log martian packets (packets with impossible source addresses)
net.ipv4.conf.all.log_martians = 1

Apply:

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

File Descriptor Limits

Game servers and the backend open many connections. Raise the per-process limit:

sudo nano /etc/security/limits.d/99-mip.conf
ubuntu  soft  nofile  65536
ubuntu  hard  nofile  65536
*       soft  nofile  65536
*       hard  nofile  65536

Log out and back in for limits to take effect. Verify with ulimit -n.


Time Synchronization

Accurate time is critical for JWT expiry, Redis TTLs, and log correlation across nodes. Ubuntu uses systemd-timesyncd by default, but chrony is more reliable for production.

sudo apt install -y chrony
sudo systemctl enable --now chrony

Verify sync:

chronyc tracking

Check that Leap status shows Normal and System time offset is sub-millisecond.

Info

All nodes in the cluster (master + workers) should use the same NTP source. Chrony handles this automatically with Ubuntu's default NTP pool.


Log Rotation

Prevent logs from filling the disk. Most services use journald, but UE server logs inside pods write to files.

journald

Cap the journal to 500 MB:

sudo nano /etc/systemd/journald.conf
[Journal]
SystemMaxUse=500M
MaxRetentionSec=7day
sudo systemctl restart systemd-journald

Docker logs

Docker container logs can grow unbounded. Set a global default:

sudo nano /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "3"
  }
}
sudo systemctl restart docker

Disable Unused Services

Reduce the attack surface by disabling services you don't need:

sudo systemctl disable --now snapd snapd.socket 2>/dev/null || true
sudo systemctl disable --now ModemManager 2>/dev/null || true
sudo systemctl disable --now cups cups-browsed 2>/dev/null || true
sudo systemctl disable --now avahi-daemon 2>/dev/null || true

List what's still listening:

sudo ss -tlnp

Investigate anything unexpected. On a properly configured MIP server, you should only see K3s (6443), Kubelet (10250), and your backend ports.


Shared Memory Hardening

Prevent shared memory exploits:

echo 'tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0' | sudo tee -a /etc/fstab
sudo mount -o remount /run/shm

Login Banners

Warn unauthorized users. Useful for compliance and legal protection:

sudo nano /etc/issue.net
*********************************************************************
  WARNING: Unauthorized access to this system is prohibited.
  All connections are monitored and recorded.
  Disconnect IMMEDIATELY if you are not an authorized user.
*********************************************************************

Enable the banner in SSH:

sudo nano /etc/ssh/sshd_config
Banner /etc/issue.net
sudo systemctl restart ssh 2>/dev/null || sudo systemctl restart sshd

Checklist

Step Status
Unattended security upgrades dpkg-reconfigure unattended-upgrades
Fail2Ban installed and enabled fail2ban-client status sshd
SSH hardened (MaxAuthTries, AllowUsers) Review /etc/ssh/sshd_config
Swap configured swapon --show
Kernel tuning applied sysctl -p /etc/sysctl.d/99-mip.conf
File descriptor limits raised ulimit -n → 65536
Time sync (chrony) chronyc tracking
Log rotation configured journald + Docker log limits
Unused services disabled ss -tlnp — nothing unexpected
Shared memory hardened /run/shm mounted noexec
Monitoring in place See Monitoring