D
Dev SOPKnowledge Base
Search
← All topics

VPS Server Hardening: UFW, SSH, Fail2ban, and Nginx Security

VPS security hardening checklist — SSH key authentication, UFW firewall rules, Fail2ban setup, nginx security headers, automatic security updates, non-root deployment user, and Tailscale for private access.

linuxvpssecurityufwsshnginxfail2banhardening
Agent trigger phrases: VPS hardening · server security · UFW firewall · SSH hardening · Fail2ban · nginx security · server setup · Linux security

Overview

Minimum security baseline for any internet-facing VPS. Run this checklist on every new server before exposing any services.

1. Create Non-Root User

# Create deploy user
adduser deploy
usermod -aG sudo deploy

# Switch to new user
su - deploy

Never run production apps as root.

2. SSH Key Authentication

# On local machine: generate key if needed
ssh-keygen -t ed25519 -C "deploy@server" -f ~/.ssh/server_deploy

# Copy public key to server
ssh-copy-id -i ~/.ssh/server_deploy.pub deploy@server-ip

# Verify login works before disabling password auth
ssh -i ~/.ssh/server_deploy deploy@server-ip

Then lock down SSH:

sudo nano /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
LoginGraceTime 30
Protocol 2
sudo systemctl restart sshd

3. UFW Firewall

# Install and configure
sudo apt install ufw -y

# Default deny all incoming
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (do this FIRST before enabling)
sudo ufw allow ssh

# Allow web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Allow specific app ports (only what's needed)
# sudo ufw allow 3000/tcp  # only if running app directly (no nginx reverse proxy)

# Enable
sudo ufw enable

# Verify
sudo ufw status verbose

4. Tailscale (Private Network)

For services that shouldn't be public (databases, admin panels, OpenClaw gateway):

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --authkey=tskey-auth-xxx

# Allow only through Tailscale interface
sudo ufw allow in on tailscale0

# Verify
tailscale status
tailscale ip

5. Fail2ban

sudo apt install fail2ban -y

# Configure
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5

[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 3
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check status
sudo fail2ban-client status sshd

6. Automatic Security Updates

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Mail "your@email.com";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Automatic-Reboot "false";  # manual reboots for servers

7. Nginx Security Headers

server {
    listen 443 ssl;
    server_name example.com;

    # SSL
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Hide server version
    server_tokens off;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req zone=api burst=20 nodelay;

    location /api/ {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

8. Node.js App as Systemd Service

# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js App
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/myapp
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production
EnvironmentFile=/home/deploy/myapp/.env

# Security
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/home/deploy/myapp/logs

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
sudo journalctl -u myapp -f  # follow logs

Hardening Checklist

[ ] Non-root user created
[ ] SSH key authentication enabled
[ ] Password authentication disabled
[ ] Root login disabled
[ ] UFW enabled with deny-by-default
[ ] Only required ports open (22, 80, 443)
[ ] Fail2ban protecting SSH
[ ] Automatic security updates enabled
[ ] Nginx security headers set
[ ] server_tokens off (hide nginx version)
[ ] SSL/TLS configured (Let's Encrypt via Certbot)
[ ] App runs as non-root user
[ ] App env file has restricted permissions (chmod 600 .env)
[ ] Database not exposed publicly (bind to 127.0.0.1)
[ ] Tailscale for private admin access (if applicable)