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)