{"slug":"server-hardening-vps","title":"VPS Server Hardening: UFW, SSH, Fail2ban, and Nginx Security","tags":["linux","vps","security","ufw","ssh","nginx","fail2ban","hardening"],"agent_summary":"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.","trigger_phrases":["VPS hardening","server security","UFW firewall","SSH hardening","Fail2ban","nginx security","server setup","Linux security"],"runnable":false,"markdown":"\n## Overview\n\nMinimum security baseline for any internet-facing VPS. Run this checklist on every new server before exposing any services.\n\n## 1. Create Non-Root User\n\n```bash\n# Create deploy user\nadduser deploy\nusermod -aG sudo deploy\n\n# Switch to new user\nsu - deploy\n```\n\nNever run production apps as root.\n\n## 2. SSH Key Authentication\n\n```bash\n# On local machine: generate key if needed\nssh-keygen -t ed25519 -C \"deploy@server\" -f ~/.ssh/server_deploy\n\n# Copy public key to server\nssh-copy-id -i ~/.ssh/server_deploy.pub deploy@server-ip\n\n# Verify login works before disabling password auth\nssh -i ~/.ssh/server_deploy deploy@server-ip\n```\n\nThen lock down SSH:\n\n```bash\nsudo nano /etc/ssh/sshd_config\n```\n\n```\nPermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes\nAuthorizedKeysFile .ssh/authorized_keys\nMaxAuthTries 3\nLoginGraceTime 30\nProtocol 2\n```\n\n```bash\nsudo systemctl restart sshd\n```\n\n## 3. UFW Firewall\n\n```bash\n# Install and configure\nsudo apt install ufw -y\n\n# Default deny all incoming\nsudo ufw default deny incoming\nsudo ufw default allow outgoing\n\n# Allow SSH (do this FIRST before enabling)\nsudo ufw allow ssh\n\n# Allow web traffic\nsudo ufw allow 80/tcp\nsudo ufw allow 443/tcp\n\n# Allow specific app ports (only what's needed)\n# sudo ufw allow 3000/tcp  # only if running app directly (no nginx reverse proxy)\n\n# Enable\nsudo ufw enable\n\n# Verify\nsudo ufw status verbose\n```\n\n## 4. Tailscale (Private Network)\n\nFor services that shouldn't be public (databases, admin panels, OpenClaw gateway):\n\n```bash\ncurl -fsSL https://tailscale.com/install.sh | sh\nsudo tailscale up --authkey=tskey-auth-xxx\n\n# Allow only through Tailscale interface\nsudo ufw allow in on tailscale0\n\n# Verify\ntailscale status\ntailscale ip\n```\n\n## 5. Fail2ban\n\n```bash\nsudo apt install fail2ban -y\n\n# Configure\nsudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local\nsudo nano /etc/fail2ban/jail.local\n```\n\n```ini\n[DEFAULT]\nbantime = 1h\nfindtime = 10m\nmaxretry = 5\n\n[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\nmaxretry = 3\n```\n\n```bash\nsudo systemctl enable fail2ban\nsudo systemctl start fail2ban\n\n# Check status\nsudo fail2ban-client status sshd\n```\n\n## 6. Automatic Security Updates\n\n```bash\nsudo apt install unattended-upgrades -y\nsudo dpkg-reconfigure --priority=low unattended-upgrades\n```\n\n```ini\n# /etc/apt/apt.conf.d/50unattended-upgrades\nUnattended-Upgrade::Allowed-Origins {\n    \"${distro_id}:${distro_codename}-security\";\n};\nUnattended-Upgrade::Mail \"your@email.com\";\nUnattended-Upgrade::Remove-Unused-Kernel-Packages \"true\";\nUnattended-Upgrade::Automatic-Reboot \"false\";  # manual reboots for servers\n```\n\n## 7. Nginx Security Headers\n\n```nginx\nserver {\n    listen 443 ssl;\n    server_name example.com;\n\n    # SSL\n    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;\n    ssl_protocols TLSv1.2 TLSv1.3;\n    ssl_prefer_server_ciphers off;\n    ssl_session_timeout 1d;\n    ssl_session_cache shared:SSL:50m;\n\n    # Security headers\n    add_header X-Frame-Options \"SAMEORIGIN\" always;\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header X-XSS-Protection \"1; mode=block\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n    add_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';\" always;\n    add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always;\n    add_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n\n    # Hide server version\n    server_tokens off;\n\n    # Rate limiting\n    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;\n    limit_req zone=api burst=20 nodelay;\n\n    location /api/ {\n        proxy_pass http://localhost:3000;\n        proxy_http_version 1.1;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}\n\n# Redirect HTTP to HTTPS\nserver {\n    listen 80;\n    server_name example.com;\n    return 301 https://$server_name$request_uri;\n}\n```\n\n## 8. Node.js App as Systemd Service\n\n```ini\n# /etc/systemd/system/myapp.service\n[Unit]\nDescription=My Node.js App\nAfter=network.target\n\n[Service]\nType=simple\nUser=deploy\nWorkingDirectory=/home/deploy/myapp\nExecStart=/usr/bin/node dist/server.js\nRestart=on-failure\nRestartSec=10\nEnvironment=NODE_ENV=production\nEnvironmentFile=/home/deploy/myapp/.env\n\n# Security\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nReadWritePaths=/home/deploy/myapp/logs\n\n[Install]\nWantedBy=multi-user.target\n```\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable myapp\nsudo systemctl start myapp\nsudo journalctl -u myapp -f  # follow logs\n```\n\n## Hardening Checklist\n\n```\n[ ] Non-root user created\n[ ] SSH key authentication enabled\n[ ] Password authentication disabled\n[ ] Root login disabled\n[ ] UFW enabled with deny-by-default\n[ ] Only required ports open (22, 80, 443)\n[ ] Fail2ban protecting SSH\n[ ] Automatic security updates enabled\n[ ] Nginx security headers set\n[ ] server_tokens off (hide nginx version)\n[ ] SSL/TLS configured (Let's Encrypt via Certbot)\n[ ] App runs as non-root user\n[ ] App env file has restricted permissions (chmod 600 .env)\n[ ] Database not exposed publicly (bind to 127.0.0.1)\n[ ] Tailscale for private admin access (if applicable)\n```\n","html":"<h2>Overview</h2>\n<p>Minimum security baseline for any internet-facing VPS. Run this checklist on every new server before exposing any services.</p>\n<h2>1. Create Non-Root User</h2>\n<pre><code class=\"language-bash\"># Create deploy user\nadduser deploy\nusermod -aG sudo deploy\n\n# Switch to new user\nsu - deploy\n</code></pre>\n<p>Never run production apps as root.</p>\n<h2>2. SSH Key Authentication</h2>\n<pre><code class=\"language-bash\"># On local machine: generate key if needed\nssh-keygen -t ed25519 -C \"deploy@server\" -f ~/.ssh/server_deploy\n\n# Copy public key to server\nssh-copy-id -i ~/.ssh/server_deploy.pub deploy@server-ip\n\n# Verify login works before disabling password auth\nssh -i ~/.ssh/server_deploy deploy@server-ip\n</code></pre>\n<p>Then lock down SSH:</p>\n<pre><code class=\"language-bash\">sudo nano /etc/ssh/sshd_config\n</code></pre>\n<pre><code>PermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes\nAuthorizedKeysFile .ssh/authorized_keys\nMaxAuthTries 3\nLoginGraceTime 30\nProtocol 2\n</code></pre>\n<pre><code class=\"language-bash\">sudo systemctl restart sshd\n</code></pre>\n<h2>3. UFW Firewall</h2>\n<pre><code class=\"language-bash\"># Install and configure\nsudo apt install ufw -y\n\n# Default deny all incoming\nsudo ufw default deny incoming\nsudo ufw default allow outgoing\n\n# Allow SSH (do this FIRST before enabling)\nsudo ufw allow ssh\n\n# Allow web traffic\nsudo ufw allow 80/tcp\nsudo ufw allow 443/tcp\n\n# Allow specific app ports (only what's needed)\n# sudo ufw allow 3000/tcp  # only if running app directly (no nginx reverse proxy)\n\n# Enable\nsudo ufw enable\n\n# Verify\nsudo ufw status verbose\n</code></pre>\n<h2>4. Tailscale (Private Network)</h2>\n<p>For services that shouldn't be public (databases, admin panels, OpenClaw gateway):</p>\n<pre><code class=\"language-bash\">curl -fsSL https://tailscale.com/install.sh | sh\nsudo tailscale up --authkey=tskey-auth-xxx\n\n# Allow only through Tailscale interface\nsudo ufw allow in on tailscale0\n\n# Verify\ntailscale status\ntailscale ip\n</code></pre>\n<h2>5. Fail2ban</h2>\n<pre><code class=\"language-bash\">sudo apt install fail2ban -y\n\n# Configure\nsudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local\nsudo nano /etc/fail2ban/jail.local\n</code></pre>\n<pre><code class=\"language-ini\">[DEFAULT]\nbantime = 1h\nfindtime = 10m\nmaxretry = 5\n\n[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\nmaxretry = 3\n</code></pre>\n<pre><code class=\"language-bash\">sudo systemctl enable fail2ban\nsudo systemctl start fail2ban\n\n# Check status\nsudo fail2ban-client status sshd\n</code></pre>\n<h2>6. Automatic Security Updates</h2>\n<pre><code class=\"language-bash\">sudo apt install unattended-upgrades -y\nsudo dpkg-reconfigure --priority=low unattended-upgrades\n</code></pre>\n<pre><code class=\"language-ini\"># /etc/apt/apt.conf.d/50unattended-upgrades\nUnattended-Upgrade::Allowed-Origins {\n    \"${distro_id}:${distro_codename}-security\";\n};\nUnattended-Upgrade::Mail \"your@email.com\";\nUnattended-Upgrade::Remove-Unused-Kernel-Packages \"true\";\nUnattended-Upgrade::Automatic-Reboot \"false\";  # manual reboots for servers\n</code></pre>\n<h2>7. Nginx Security Headers</h2>\n<pre><code class=\"language-nginx\">server {\n    listen 443 ssl;\n    server_name example.com;\n\n    # SSL\n    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;\n    ssl_protocols TLSv1.2 TLSv1.3;\n    ssl_prefer_server_ciphers off;\n    ssl_session_timeout 1d;\n    ssl_session_cache shared:SSL:50m;\n\n    # Security headers\n    add_header X-Frame-Options \"SAMEORIGIN\" always;\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header X-XSS-Protection \"1; mode=block\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n    add_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';\" always;\n    add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always;\n    add_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n\n    # Hide server version\n    server_tokens off;\n\n    # Rate limiting\n    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;\n    limit_req zone=api burst=20 nodelay;\n\n    location /api/ {\n        proxy_pass http://localhost:3000;\n        proxy_http_version 1.1;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}\n\n# Redirect HTTP to HTTPS\nserver {\n    listen 80;\n    server_name example.com;\n    return 301 https://$server_name$request_uri;\n}\n</code></pre>\n<h2>8. Node.js App as Systemd Service</h2>\n<pre><code class=\"language-ini\"># /etc/systemd/system/myapp.service\n[Unit]\nDescription=My Node.js App\nAfter=network.target\n\n[Service]\nType=simple\nUser=deploy\nWorkingDirectory=/home/deploy/myapp\nExecStart=/usr/bin/node dist/server.js\nRestart=on-failure\nRestartSec=10\nEnvironment=NODE_ENV=production\nEnvironmentFile=/home/deploy/myapp/.env\n\n# Security\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nReadWritePaths=/home/deploy/myapp/logs\n\n[Install]\nWantedBy=multi-user.target\n</code></pre>\n<pre><code class=\"language-bash\">sudo systemctl daemon-reload\nsudo systemctl enable myapp\nsudo systemctl start myapp\nsudo journalctl -u myapp -f  # follow logs\n</code></pre>\n<h2>Hardening Checklist</h2>\n<pre><code>[ ] Non-root user created\n[ ] SSH key authentication enabled\n[ ] Password authentication disabled\n[ ] Root login disabled\n[ ] UFW enabled with deny-by-default\n[ ] Only required ports open (22, 80, 443)\n[ ] Fail2ban protecting SSH\n[ ] Automatic security updates enabled\n[ ] Nginx security headers set\n[ ] server_tokens off (hide nginx version)\n[ ] SSL/TLS configured (Let's Encrypt via Certbot)\n[ ] App runs as non-root user\n[ ] App env file has restricted permissions (chmod 600 .env)\n[ ] Database not exposed publicly (bind to 127.0.0.1)\n[ ] Tailscale for private admin access (if applicable)\n</code></pre>\n"}