Simple VPS hardening

home

ยท Tacoma

It’s becoming increasingly popular to reduce abstractions in your stack. A lot of people are ditching all-in-one cloud providers for a single VPS, and others are starting full stack projects with vanilla PHP and self-hosting everything down to the db.

I personally enjoy this push, but think it could be risky since few places show the full process of what configuring a new VPS really looks like. Here’s a write up on hardening to help ensure the new machine is at least a bit safer.

Initial setup

VPS provider console

  • Purchase your desired machine
  • Set up SSH key authentication (avoid password login)
    • Use a strong, non-memorable passphrase
    • Store the key securely and privately
  • Configure firewall rules
    • Allow 443/tcp and 80/tcp from any IPv4/IPv6
    • Allow 22/tcp from your IP only

Local machine setup

  • Place the provider-supplied SSH key in ~/.ssh with a configured name
  • Test login: ssh root@<vps ip>
  • Use -i ~/.ssh/key-location if the passphrase prompt doesn’t appear

Server configuration

First login as root

apt update && apt upgrade
adduser <your username>
usermod -aG sudo <your username>
su - <your username>
sudo apt update
exit 
exit # close ssh connection

Local machine: new user SSH setup

Generate a new key pair for this user rather than reusing the root key.

# generate new key pair
ssh-keygen -t ed25519 -C "[email protected]"

# copy to clipboard
pbcopy < ~/.ssh/id_ed25519.pub

# reconnect to server
ssh <your username>@<vps ip>

Server: configure SSH

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys  # paste clipboard contents
chmod 600 ~/.ssh/authorized_keys
exit

SSH hardening

sudo nano /etc/ssh/sshd_config

# Set:
# - PermitRootLogin no
# - PasswordAuthentication no
# - Port <custom port 1024-65535>

sudo systemctl restart ssh

Automatic security updates

sudo apt install unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades
sudo unattended-upgrades --dry-run --debug
sudo systemctl enable unattended-upgrades
sudo systemctl start unattended-upgrades

Fail2ban

sudo apt install fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

# Configure [sshd] section:
# - enabled = true
# - port = <custom ssh port>
# - logpath = systemd-journal
# - backend = systemd

sudo usermod -aG systemd-journal fail2ban
sudo systemctl restart fail2ban

UFW firewall

sudo apt install ufw -y

sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow <custom ssh port>/tcp
sudo ufw allow 443/tcp
sudo ufw allow 80/tcp
sudo ufw limit <custom ssh port>/tcp comment 'rate limit'

Web server setup: Caddy + Cloudflare

Go installation

wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc

Caddy installation

Building Caddy with the Cloudflare DNS module lets you obtain SSL certificates through DNS challenges without needing ports 80/443 open during issuance.

wget https://github.com/caddyserver/xcaddy/releases/download/v0.4.2/xcaddy_0.4.2_linux_amd64.deb
sudo dpkg -i xcaddy_0.4.2_linux_amd64.deb
sudo apt-get install -f

# build Caddy with Cloudflare module
xcaddy build --with github.com/caddy-dns/cloudflare

# install to system
sudo mv caddy /usr/bin/caddy
sudo chown root:root /usr/bin/caddy 
sudo chmod 755 /usr/bin/caddy
sudo caddy reload

Caddy configuration

Set global DNS challenges in Caddyfile:

tls {
    dns cloudflare {.env.CF_TOKEN}
}

Create a dedicated caddy user:

sudo useradd -r -d /var/lib/caddy -s /usr/sbin/nologin caddy
sudo chown -R caddy:caddy /etc/caddy
sudo chown -R caddy:caddy /var/lib/caddy

Update the systemd service:

sudo systemctl edit caddy.service

# Add:

[Service]
User=caddy
Group=caddy
EnvironmentFile=/etc/caddy/caddy.env

Set permissions:

sudo setcap cap_net_bind_service=+ep $(which caddy)
sudo systemctl daemon-reload
sudo systemctl restart caddy

sudo groupadd webcontent
sudo usermod -a -G webcontent <your username>
sudo usermod -a -G webcontent caddy
sudo chgrp webcontent /home/<your username>
sudo chmod g+x /home/<your username>
sudo systemctl restart caddy

Cloudflare IP restriction

This script restricts ports 80 and 443 to only Cloudflare’s IP ranges.

#!/bin/bash

log() {
  echo "[INFO] $1"
}

deny_other_ips() {
  log "denying 80 and 443 access..."
  ufw deny to any port 80 comment "deny all access to port 80"
  ufw deny to any port 443 comment "deny all access to port 443"
}

allow_cloudflare_ips() {
  local ipv4_ips=$(curl -s https://www.cloudflare.com/ips-v4)
  local ipv6_ips=$(curl -s https://www.cloudflare.com/ips-v6)
  log "configuring ufw rules for cloudflare only on 80 and 443..."
  
  for ip in $ipv4_ips; do
    ufw allow proto tcp from "$ip" to any port 80,443 comment "cloudflare ip $ip"
  done
  
  for ip in $ipv6_ips; do
    ufw allow proto tcp from "$ip" to any port 80,443 comment "cloudflare ip $ip"
  done
}

enable_ufw() {
  log "enabling ufw..."
  if ! ufw --force enable; then
    log "failed to enable ufw."
    exit 1
  fi
  log "ufw enabled."
}

main() {
  deny_other_ips
  allow_cloudflare_ips
  enable_ufw
}

main

Kernel hardening

#!/bin/bash

sudo tee /etc/sysctl.d/99-security.conf > /dev/null <<EOT
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
fs.file-max = 65535
net.ipv4.ip_local_port_range = 2000 65000
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.core.netdev_max_backlog = 65536
EOT

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

Comments

Loading comments...