A structured guide to setting up and maintaining a Debian VPS—secure, robust, and production-ready with monitoring.
-
Basic Server Configuration
- System initialization
- User management
- Essential utilities
-
Security Hardening
- SSH hardening
- Firewall configuration
- Intrusion prevention
- Rootkit protection
-
Web Server Setup
- Nginx/Apache configurations
- SSL/TLS implementation
- Node.js + PM2
- Multi-site hosting
-
Monitoring
- System metrics
- Security monitoring
- Performance tracking
- Alert configuration
-
Server Maintenance
- System auditing
- Security checks
- Performance optimization
- Log management
-
Data Backups
- Encrypted backups
- Automated routines
- Verification processes
- Restoration procedures
-
Bootstraps
- Configuration templates
- Hardened defaults
- Quick-start scripts
Basic Linux configurations to use in all VPS instances.
$ apt update && apt upgrade
$ hostnamectl set-hostname "hostname"
$ echo "hostname" > /etc/hostname
$ hostname -F /etc/hostname
$ dpkg-reconfigure tzdata
$ date
$ adduser "username"
$ apt install sudo
$ usermod -a -G sudo "username"
A modern, easy-going, and powerful shell.
$ sudo apt install fish
$ sudo chsh -s /usr/bin/fish
$ sudo shutdown -r now
Basic hardening configurations to use in all VPS instances.
On the server:
$ mkdir -p ~/.ssh/ && sudo chmod -R 700 ~/.ssh/
$ sudo chmod 700 -R ~/.ssh && chmod 600 ~/.ssh/authorized_keys
On local machine:
$ scp ~/.ssh/id_rsa.pub username@vpsipaddress:~/.ssh/authorized_keys
File: /etc/ssh/sshd_config
# Authentication reforce
PermitRootLogin no
PasswordAuthentication no
# Listen on only on IPv4
AddressFamily inet
Restart SSH
$ sudo systemctl restart sshd
Simple intrusion prevention software.
$ apt install fail2ban
Configure:
$ cp /etc/fail2ban/fail2ban.conf /etc/fail2ban/fail2ban.local
$ cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Lets configure jail.local with our optimized settings:
$ sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# Base settings with progressive banning
bantime = 24h # Initial ban duration
findtime = 15m # Detection window
maxretry = 2 # Failures allowed before ban
# Progressive ban system for repeat offenders
bantime.increment = true
bantime.multipliers = 1 3 6 12 24 48
bantime.maxtime = 4w
bantime.factor = 1
# Basic security settings
ignoreip = 127.0.0.1/8 ::1
backend = auto
usedns = warn
banaction = iptables-multiport
protocol = tcp
chain = INPUT
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 2
findtime = 10m
bantime = 48h
[nginx-badhosts]
enabled = true
port = http,https
filter = nginx-badhosts
logpath = /var/log/nginx/error.log
maxretry = 3
findtime = 15m
bantime = 24h
Then create a custom filter for nginx SSL issues:
$ sudo nano /etc/fail2ban/filter.d/nginx-badhosts.conf
[Definition]
failregex = SSL_do_handshake\(\) failed .* client: <HOST>
ignoreregex =
Adding a verification section:
$ sudo systemctl restart fail2ban
$ sudo systemctl status fail2ban
$ sudo fail2ban-client status
$ sudo fail2ban-client status sshd
$ sudo fail2ban-client status nginx-badhosts
Test the nginx filter against logs:
$ sudo fail2ban-regex /var/log/nginx/error.log /etc/fail2ban/filter.d/nginx-badhosts.conf
Monitor fail2ban activity:
$ sudo tail -f /var/log/fail2ban.log
Check currently banned IPs:
$ sudo fail2ban-client get sshd banned
$ sudo fail2ban-client get nginx-badhosts banned
Modern, collaborative security engine that works alongside fail2ban.
# Add Crowdsec repository
$ curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
# Install Crowdsec with nginx collection
$ sudo apt install crowdsec
$ sudo apt install crowdsec-nginx-bouncer
# Initial setup
$ sudo cscli hub update
$ sudo cscli collections install crowdsecurity/nginx
$ sudo cscli collections install crowdsecurity/linux
$ sudo cscli collections install crowdsecurity/http-cve
Create parsers configuration:
$ sudo nano /etc/crowdsec/acquis.yaml
filenames:
- /var/log/nginx/access.log
- /var/log/nginx/error.log
labels:
type: nginx
Configure Nginx Bouncer:
$ sudo nano /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
api_key: # Will be automatically populated
ban_time: 1h
ban_mode: http-403
include_scenarios:
- crowdsecurity/http-probing
- crowdsecurity/http-bad-user-agent
- crowdsecurity/nginx-http-bad-user-agent
- crowdsecurity/http-crawl-non_statics
- crowdsecurity/http-path-traversal-probing
Configure Nginx for Crowdsec:
$ sudo nano /etc/nginx/conf.d/crowdsec_nginx.conf
load_module /usr/lib/nginx/modules/ngx_http_crowdsec_module.so;
http {
crowdsec_instance {
path /var/run/crowdsec/crowdsec-nginx-bouncer.sock;
}
# ... rest of your http configuration
}
Start Services
# Restart services to apply changes
$ sudo systemctl restart crowdsec
$ sudo systemctl restart nginx
$ sudo systemctl enable crowdsec
# Verify services
$ sudo systemctl status crowdsec
$ sudo cscli hub list
Basic Commands
# Check Crowdsec status
$ sudo cscli metrics
# List current bans
$ sudo cscli decisions list
# List active scenarios
$ sudo cscli scenarios list
# List bouncers
$ sudo cscli bouncers list
# Check real-time logs
$ sudo journalctl -f -u crowdsec
# Add IP to whitelist
$ sudo cscli decisions add --ip X.X.X.X --duration 168h --type whitelist
# Remove IP from bans
$ sudo cscli decisions delete --ip X.X.X.X
Maintenance: daily tasks
# Update hub collections
$ sudo cscli hub update
# Check for outdated scenarios
$ sudo cscli scenarios list -o
# Review metrics
$ sudo cscli metrics
# Review current decisions
$ sudo cscli decisions list
# Check bouncer status
$ sudo cscli bouncers list
Maintenance: weekly checks
# Full database cleanup
$ sudo cscli database cleanup
# Update all collections
$ sudo cscli collections upgrade
# Check configuration validity
$ sudo crowdsec -c /etc/crowdsec/config.yaml -t
Crowdsec can work alongside fail2ban. Configure fail2ban to respect Crowdsec's decisions:
$ sudo nano /etc/fail2ban/jail.local
```
Add to [DEFAULT] section:
````console
ignoreip = 127.0.0.1/8 ::1 # Add your IPs and Crowdsec's IP range if used
```
### Rootkit protection
Rkhunter and chkrootkit:
```console
$ sudo apt install rkhunter
$ sudo rkhunter --propupd
$ sudo rkhunter --check
$ sudo apt install chkrootkit
Always iptables: Minimal configuration; Easygoing; Effective.
$ sudo iptables -L
$ sudo apt install iptables-persistent
$ sudo nano /etc/iptables/rules.v4
iptables file [ download here ]:
*filter
# Allow all outgoing, but drop incoming and forwarding packets by default
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
# Custom per-protocol chains
:UDP - [0:0]
:TCP - [0:0]
:ICMP - [0:0]
# Acceptable UDP traffic
# Acceptable TCP traffic
-A TCP -p tcp --dport 80 -j ACCEPT
-A TCP -p tcp --dport 443 -j ACCEPT
-A TCP -p tcp --dport 22 -j ACCEPT
# Acceptable ICMP traffic
# Boilerplate acceptance policy
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
-A INPUT -i lo -j ACCEPT
# Drop invalid packets
-A INPUT -m conntrack --ctstate INVALID -j DROP
# Pass traffic to protocol-specific chains
## Only allow new connections (established and related should already be handled)
## For TCP, additionally only allow new SYN packets since that is the only valid
## method for establishing a new TCP connection
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --syn -m conntrack --ctstate NEW -j TCP
-A INPUT -p icmp -m conntrack --ctstate NEW -j ICMP
# Reject anything that's fallen through to this point
## Try to be protocol-specific w/ rejection message
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
# Commit the changes
COMMIT
*raw
:PREROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT
*security
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
COMMIT
$ sudo iptables-restore -t /etc/iptables/rules.v4
$ sudo nano /etc/iptables/rules.v6
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
COMMIT
*raw
:PREROUTING DROP [0:0]
:OUTPUT DROP [0:0]
COMMIT
*nat
:PREROUTING DROP [0:0]
:INPUT DROP [0:0]
:OUTPUT DROP [0:0]
:POSTROUTING DROP [0:0]
COMMIT
*security
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
COMMIT
*mangle
:PREROUTING DROP [0:0]
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
:POSTROUTING DROP [0:0]
COMMIT
$ sudo ip6tables-restore -t /etc/iptables/rules.v6
$ sudo service netfilter-persistent reload
$ sudo iptables -S
$ sudo ip6tables -S
Lynis to perform full system audits.
$ sudo apt install lynis
To monitor active SSH connections.
$ sudo apt install whowatch
$ sudo ss -atpu
$ sudo apt purge "package_name"
Multiple configurations for regular web server needs.
1: Secure web traffic with Let's Encrypt's Certbot
2: Nginx with Server Blocks to host multiple websites
3: Apache with Virtual Host to host multiple websites
4: NodeJS and PM2 to host multiple Nuxt instances
$ sudo apt install certbot
Request a certificate for Apache:
$ sudo certbot --apache -d domain.com -d www.domain.com
Request a certificate for Nginx:
$ sudo certbot --nginx -d domain.com -d www.domain.com
Certificate automated renewals:
$ sudo certbot renew --dry-run
$ sudo apt install nginx
$ sudo chmod -R 755 /var/www
Nginx folder structure.
$ sudo mkdir -p /var/www/your_domain/html
$ sudo chown -R $USER:$USER /var/www/your_domain/html
$ sudo chmod -R 755 /var/www/your_domain
$ sudo chmod -R 755 /var/www/your_domain
sample index.html
$ nano /var/www/your_domain/html/index.html
your_domain config default config file
$ sudo nano /etc/nginx/sites-available/your_domain
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
server {
root /var/www/website.net/html;
index index.html index.htm index.nginx-debian.html;
server_name website.net www.website.net;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
#add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), fullscreen=(self), payment=()";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests;" always;
# Hide Nginx version number
server_tokens off;
# 404 page
error_page 404 /404.html;
# SSL Session Caching
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
gzip_disable "MSIE [1-6]\.";
location / {
try_files $uri $uri/ =404;
# Rate limiting
limit_req zone=one burst=5;
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/website.net/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/website.net/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.website.net) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = website.net) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name website.net www.website.net;
return 404; # managed by Certbot
}
enable server block
$ sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
safeguard for hash bucket memory
$ sudo nano /etc/nginx/nginx.conf
...
http {
...
server_names_hash_bucket_size 64;
...
}
...
nginx verification
$ sudo nginx -t
$ sudo systemctl restart nginx
$ sudo apt install apache2
$ sudo chmod -R 755 /var/www
Enable the ssl module
$ sudo a2enmod ssl
Enable the proxy module
$ sudo a2enmod proxy
Enable the http2 module
$ sudo a2enmod http2
Restart Apache.
$ sudo systemctl restart apache2
Apache folder structure.
$ mkdir -p /var/www/domain.com/public_html/
$ touch /var/www/domain.com/public_html/index.html
Apache config for http:
$ touch /etc/apache2/sites-available/domain.com.conf
<VirtualHost *:80>
ServerAdmin webmaster@domain.com
ServerName domain.com
ServerAlias www.domain.com
DocumentRoot /var/www/domain.com/public_html/
ErrorLog /var/www/domain.com/logs/error.log
CustomLog /var/www/domain.com/logs/access.log combined
Redirect permanent / https://domain.com/
</VirtualHost>
Apache config for https:
$ touch /etc/apache2/sites-available/domain.com-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerAdmin webmaster@domain.com
ServerName domain.com
ServerAlias www.domain.com
DocumentRoot /var/www/domain.com/public_html/
ErrorLog /var/www/domain.com/logs/error.log
CustomLog /var/www/domain.com/logs/access.log combined
SSLCertificateFile /etc/letsencrypt/live/domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/domain.com/privkey.pem
SSLCACertificateFile /etc/letsencrypt/live/domain.com/cert.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
Map the domain.
$ sudo a2ensite domain.com.conf
Restart the Apache service to register the changes.
$ sudo systemctl restart apache2
$ sudo apt install nodejs
$ sudo apt install npm
$ sudo npm install pm2 -g
$ sudo pm2 list
$ sudo pm2 monit
Configuring Apache to Reserve Proxy to PM2:
<VirtualHost *:80>
ServerAdmin webmaster@domain.com
ServerName domain.com
ServerAlias www.domain.com
DocumentRoot /var/www/domain.com/public_html
ErrorLog /var/www/domain.com/logs/error.log
CustomLog /var/www/domain.com/logs/access.log combined
ProxyPreserveHost On
ProxyPass / http://localhost:3000/
ProxyPassReverse / http://localhost:3000/
Redirect permanent / https://domain.com/
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerAdmin webmaster@domain.com
ServerName domain.com
ServerAlias www.domain.com
DocumentRoot /var/www/domain.com/public_html
ErrorLog /var/www/domain.com/logs/error.log
CustomLog /var/www/domain.com/logs/access.log combined
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
SSLCertificateFile /etc/letsencrypt/live/domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/domain.com/privkey.pem
SSLCACertificateFile /etc/letsencrypt/live/domain.com/cert.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
Essential monitoring setup for all VPS instances.
Real-time performance and health monitoring system. Provides immediate insight into web server performance, system resources, and potential security issues.
Install required dependencies:
$ sudo apt install wget curl
Install Netdata using the automatic installer
$ wget -O /tmp/netdata-kickstart.sh https://my-netdata.io/kickstart.sh
$ sudo sh /tmp/netdata-kickstart.sh --no-updates --stable-channel --disable-telemetry
Verify the installation
$ sudo systemctl status netdata
Restrict Netdata to localhost only:
$ sudo nano /etc/netdata/netdata.conf
[global]
# Reduce retention to save resources (default is 3600)
history = 1800
[web]
# Only allow connections from localhost
bind to = localhost
allow connections from = localhost
mode = static-threaded
Create a custom system configuration:
$ sudo nano /etc/netdata/go.d.local/web_log.conf
jobs:
- name: nginx
path: /var/log/nginx/access.log
- name: apache
path: /var/log/apache2/access.log
Create custom alerts for web hosting:
$ sudo nano /etc/netdata/health.d/web_alerts.conf
# High CPU Usage Alert
alarm: cpu_usage
on: system.cpu
lookup: average -3m unaligned of user,system,softirq,irq,guest
units: %
every: 1m
warn: $this > 60
crit: $this > 80
info: CPU utilization exceeded threshold
# RAM Usage Alert
alarm: ram_usage
on: system.ram
lookup: average -1m percentage of used
units: %
every: 1m
warn: $this > 70
crit: $this > 85
info: RAM utilization exceeded threshold
# Disk Space Alert
alarm: disk_space_usage
on: disk.space
lookup: average -1m percentage of used
units: %
every: 1m
warn: $this > 80
crit: $this > 90
info: Disk space utilization exceeded threshold
# Web Server Response Time
alarm: web_response_time
on: web_log.response_time
lookup: average -3m
units: ms
every: 1m
warn: $this > 500
crit: $this > 1000
info: Web server response time is too high
Restart Netdata
$ sudo systemctl restart netdata
From our local machine:
# Create SSH tunnel to access Netdata dashboard
$ ssh -L 19999:localhost:19999 username@server_ip
# Access dashboard by opening it in the browser:
# http://localhost:19999
Semi-regular healthy maintenance tasks.
$ sudo lynis show options
$ sudo lynis audit system
$ sudo rkhunter -C
$ sudo rkhunter > ~/audits/rkhunter-audit-results.txt
$ sudo chkrootkit > ~/audits/chkrootkit-audit-results.txt
$ sudo whowatch
Simple processes to backup (encrypted versions) all-things VPS data:
Regular compressed backups:
# Create a directory for encrypted backups if it doesn't exist
$ mkdir -p ~/encrypted_backups
# Backup and encrypt home directory
$ tar czvf - ~/var/home | gpg --symmetric --cipher-algo AES256 -o ~/encrypted_backups/vps-backup-home.tar.gz.gpg
# Backup and encrypt web directory
$ tar czvf - ~/var/www | gpg --symmetric --cipher-algo AES256 -o ~/encrypted_backups/vps-backup-web.tar.gz.gpg
# Backup and encrypt logs
$ tar czvf - ~/var/log | gpg --symmetric --cipher-algo AES256 -o ~/encrypted_backups/vps-backup-logs.tar.gz.gpg
# Backup and encrypt Apache configuration
$ tar czvf - ~/etc/apache2 | gpg --symmetric --cipher-algo AES256 -o ~/encrypted_backups/vps-backup-apache.tar.gz.gpg
# Backup and encrypt Nginx configuration
$ tar czvf - ~/etc/nginx | gpg --symmetric --cipher-algo AES256 -o ~/encrypted_backups/vps-backup-nginx.tar.gz.gpg
# Backup and encrypt Let's Encrypt certificates
$ tar czvf - ~/etc/letsencrypt | gpg --symmetric --cipher-algo AES256 -o ~/encrypted_backups/vps-backup-letsencrypt-certificates.tar.gz.gpg
# Backup and encrypt security audits
$ tar czvf - ~/var/home/audits | gpg --symmetric --cipher-algo AES256 -o ~/encrypted_backups/vps-backup-security-audits.tar.gz.gpg
Then, use rsync to copy the encrypted backups from our VPS to our local machine:
$ rsync -ahvz username@vpsipaddress:/path/to/encrypted_backups/ /path/to/local/encrypted_backups/
Then, on our local machine, decrypt and extract a specific backup:
$ gpg -d /path/to/local/encrypted_backups/vps-backup-home.tar.gz.gpg | tar xzvf -
A bunch of opinionated and hardened files for multiple software configs: