← Back to Blog
· 30 min read ·

Real-world Linux Server Hardening: 14 layers I apply in production with automation code

Advanced Linux hardening guide based on real infrastructure: SSH hardening + fail2ban + UFW + kernel sysctl + auditd + systemd sandboxing + nginx auto-fix + Redis bind + MongoDB auth + Postfix secure relay + automated backup with Google Drive + orchestrated restore via Go + Conky monitoring — all with production scripts and configs.

#security#cyber-security#linux#hardening#devops#defensive#nginx#systemd#automation
Share

This isn’t a generic DigitalOcean checklist. Here I document the 14 hardening layers I apply to every infrastructure I build — from $5 VPSes to multi-service clusters. Each item comes with the actual code I use, explained.

If you have a server exposed to the internet, it’s already being scanned. Automated bots test SSH, open ports, and default configs 24/7. The question isn’t if you’ll be attacked — it’s when.

Legal Disclaimer: All content in this article is published exclusively for educational and defensive security purposes. Configurations have been anonymized. Use only on your own infrastructure or with explicit authorization.

1. SSH Hardening — Attack surface #1

SSH is the most exploited vector. OpenSSH’s default config is insecure by design — it allows root, passwords, forwarding, and weak ciphers.

# /etc/ssh/sshd_config
Port 2222                          # Non-standard port kills 99% of bots
PermitRootLogin no                 # NEVER root via SSH
PasswordAuthentication no          # Ed25519 keys only
PubkeyAuthentication yes
MaxAuthTries 3
MaxSessions 2
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deploy                  # Explicit whitelist

# Modern ciphers — discard everything pre-2020
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

# Disable everything unnecessary
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no

Generate Ed25519 key with 100 KDF rounds

ssh-keygen -t ed25519 -a 100 -C "deploy@prod-$(date +%Y)"
ssh-copy-id -p 2222 deploy@your-server

# NEVER disconnect current session before testing
ssh -p 2222 deploy@your-server  # new terminal window!

Real-time login alerts

Every SSH connection triggers a notification:

cat << 'SCRIPT' | sudo tee /usr/local/bin/login_alert.sh
#!/bin/bash
BODY="Login: $PAM_USER from $PAM_RHOST on $(hostname) at $(date)"
echo "$BODY" | mail -s "SSH Alert: $(hostname)" admin@yourdomain.com

# Webhook (Discord/Slack/Telegram)
curl -s -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{\"content\": \"$BODY\"}" >/dev/null 2>&1 &
SCRIPT

sudo chmod +x /usr/local/bin/login_alert.sh
echo "session optional pam_exec.so /usr/local/bin/login_alert.sh" \
    | sudo tee -a /etc/pam.d/sshd

2. Firewall (UFW) — Deny all, allow explicit

sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing

sudo ufw allow 2222/tcp comment "SSH"
sudo ufw allow 80/tcp   comment "HTTP"
sudo ufw allow 443/tcp  comment "HTTPS"
sudo ufw limit 2222/tcp   # Kernel-level brute force protection

sudo ufw --force enable
sudo ufw status verbose

Advanced iptables — conntrack + rate limiting

sudo iptables -A INPUT -p tcp --dport 2222 -m conntrack --ctstate NEW \
    -m recent --set --name SSH
sudo iptables -A INPUT -p tcp --dport 2222 -m conntrack --ctstate NEW \
    -m recent --update --seconds 60 --hitcount 6 --name SSH -j DROP

# Drop invalid packets
sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# Persist rules
sudo iptables-save | sudo tee /etc/iptables/rules.v4

3. Fail2Ban — Smart banning with UFW integration

sudo apt install fail2ban -y

cat << 'EOF' | sudo tee /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = ufw
ignoreip = 127.0.0.1/8

[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400

[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5

[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 120
bantime = 7200

[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 4
findtime = 60
bantime = 86400
EOF

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

4. Kernel Hardening — Production sysctl

Every parameter here has a reason. Not copy-paste — defense measures against specific attack classes:

cat << 'EOF' | sudo tee /etc/sysctl.d/99-hardening.conf
# Anti-Spoofing
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Source Routing (MUST disable)
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0

# ICMP Redirects (MitM vector)
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

# SYN Flood Protection
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2

# Disable IPv6 if unused
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1

# Log impossible packets
net.ipv4.conf.all.log_martians = 1

# Maximum ASLR
kernel.randomize_va_space = 2

# Restrict dmesg and kernel pointers
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2

# Disable Magic SysRq
kernel.sysrq = 0

# Hardlinks/Symlinks protection
fs.protected_hardlinks = 1
fs.protected_symlinks = 1

# Performance (web server bonus)
net.core.somaxconn = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
EOF

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

5. Systemd Service Hardening — Real sandboxing

Every service I run in production uses systemd sandboxing. Restart=always isn’t enough — you need to limit what the process can do:

# /etc/systemd/system/myapp.service
[Unit]
Description=Production API Service
After=network.target mongod.service redis-server.service
Requires=mongod.service redis-server.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/opt/myapp/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
Restart=always
RestartSec=5

# === HARDENING (what 90% of people skip) ===
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectControlGroups=true
ProtectKernelModules=true

# Limits
LimitNOFILE=65535
LimitNPROC=4096

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

Multi-service orchestration check

for svc in myapp-api myapp-worker myapp-beat myapp-portal nginx mongod redis-server; do
    STATUS=$(systemctl is-active $svc)
    [ "$STATUS" = "active" ] && echo "  ✓ $svc: ACTIVE" || echo "  ✗ $svc: $STATUS"
done

6. Redis Hardening — Bind localhost + LRU

Redis should never be exposed:

REDIS_CONF="/etc/redis/redis.conf"
sudo sed -i 's/^bind .*/bind 127.0.0.1 ::1/' "$REDIS_CONF"
sudo sed -i 's/^daemonize no/daemonize yes/' "$REDIS_CONF"
echo "maxmemory 256mb" | sudo tee -a "$REDIS_CONF"
echo "maxmemory-policy allkeys-lru" | sudo tee -a "$REDIS_CONF"

# Disable dangerous commands
echo 'rename-command FLUSHDB ""' | sudo tee -a "$REDIS_CONF"
echo 'rename-command FLUSHALL ""' | sudo tee -a "$REDIS_CONF"
echo 'rename-command CONFIG ""' | sudo tee -a "$REDIS_CONF"
echo 'rename-command DEBUG ""' | sudo tee -a "$REDIS_CONF"

sudo systemctl restart redis-server

7. MongoDB Hardening — Auth + Bind + Network

cat << 'EOF' | sudo tee /etc/mongod.conf
storage:
  dbPath: /var/lib/mongodb
  journal:
    enabled: true
systemLog:
  destination: file
  logAppend: true
  path: /var/log/mongodb/mongod.log
net:
  port: 27017
  bindIp: 127.0.0.1          # NEVER 0.0.0.0
security:
  authorization: enabled      # Mandatory auth
EOF

sudo systemctl restart mongod

8. Auditing with auditd — Who did what

sudo apt install auditd -y

cat << 'EOF' | sudo tee /etc/audit/rules.d/hardening.rules
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/crontab -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands
-w /sbin/insmod -p x -k kernel_modules
-w /sbin/modprobe -p x -k kernel_modules
-w /etc/hosts -p wa -k network
-w /etc/nginx/ -p wa -k nginx_config
-a always,exit -F arch=b64 -S execve -F path=/usr/bin/systemctl -k systemd
EOF

sudo systemctl restart auditd

9. Postfix Secure Relay — SASL + mandatory TLS

# /etc/postfix/main.cf
smtpd_banner = $myhostname ESMTP
smtp_tls_security_level = encrypt
smtp_tls_wrappermode = yes
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
relayhost = [smtp.provider.com]:465
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
echo "[smtp.provider.com]:465 user@domain.com:password" | sudo tee /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfix

10. Automated Setup — One-shot provisioning

#!/bin/bash
set -euo pipefail

apt-get update && apt-get install -y \
    curl wget git build-essential certbot python3-certbot-nginx \
    logrotate fail2ban auditd ufw

# Node.js 22 LTS, MongoDB 8.0, Redis, Nginx
# Python venv with auto-generated secrets
SECRET_KEY=$(openssl rand -hex 32)
JWT_SECRET=$(openssl rand -hex 32)

# Deploy systemd services and enable
systemctl daemon-reload
for svc in myapp-api myapp-worker myapp-beat; do
    systemctl enable --now "$svc"
done

11. Automated Backup with Google Drive

Daily backup at 3 AM with 30-day retention:

echo "0 3 * * * /opt/myapp/backup.sh --quiet >> /var/log/backup.log 2>&1" \
    | sudo crontab -

# What the script does:
# 1. mongodump → /tmp/backup/mongodb/
# 2. redis-cli BGSAVE → copy RDB
# 3. Copy configs: .env, nginx.conf, systemd units
# 4. rsync source code (--exclude __pycache__, node_modules, .git)
# 5. Generate metadata JSON with versions and timestamps
# 6. tar.gz with timestamp
# 7. Upload to Google Drive via API
# 8. Remove local backups > 30 days

Selective restore

./restore.sh --from /backups/backup-2026-03-08.tar.gz
./restore.sh --skip-code --skip-redis   # database and configs only
./restore.sh --skip-mongo --skip-config  # code only

12. Logrotate — Keep disk clean

cat << 'EOF' | sudo tee /etc/logrotate.d/myapp
/var/log/myapp*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 root root
}
EOF

13. Active Monitoring

#!/bin/bash
# /usr/local/bin/health_check.sh — runs via cron every 5min
FAILED=""

for svc in nginx mongod redis-server myapp-api; do
    if ! systemctl is-active --quiet "$svc"; then
        FAILED+="$svc "
        systemctl restart "$svc"
    fi
done

for PORT in 80 443 8000 27017 6379; do
    ss -tlnp | grep -q ":$PORT " || FAILED+="port:$PORT "
done

CODE=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8000/api/health")
[ "$CODE" != "200" ] && FAILED+="http:health($CODE) "
redis-cli ping | grep -q PONG || FAILED+="redis-ping "

[ -n "$FAILED" ] && echo "FAIL: $FAILED" | mail -s "ALERT: $(hostname)" admin@yourdomain.com

14. Safe cleanup before reprovisioning

#!/bin/bash
set -euo pipefail

for svc in myapp-api myapp-worker myapp-beat mongod redis-server nginx; do
    systemctl stop "$svc" 2>/dev/null || true
    systemctl disable "$svc" 2>/dev/null || true
done

# Preserve Let's Encrypt certs (avoid rate limits)
mkdir -p /tmp/letsencrypt-saved
cp -a /etc/letsencrypt/live /tmp/letsencrypt-saved/ 2>/dev/null || true
cp -a /etc/letsencrypt/archive /tmp/letsencrypt-saved/ 2>/dev/null || true

apt-get purge -y nginx* mongodb* redis* || true
apt-get autoremove -y
rm -rf /var/lib/mongodb /var/log/mongodb /var/lib/redis /var/log/redis
rm -f /etc/systemd/system/myapp-*.service
systemctl daemon-reload

Final checklist — 14 layers

#LayerStatus
1SSH: custom port, root off, Ed25519 only, modern ciphers
2UFW: deny all, allow explicit, limit SSH
3Fail2Ban: SSH 24h ban, nginx filters, UFW integration
4Kernel: sysctl anti-spoof, SYN flood, ASLR, kptr_restrict
5Systemd: sandbox with NoNewPrivileges, ProtectSystem, PrivateTmp
6Redis: bind 127.0.0.1, LRU, dangerous commands disabled
7MongoDB: auth enabled, bind localhost
8Auditd: identity, SSH config, root commands, kernel modules
9Postfix: SASL + mandatory TLS, restricted relay
10Automated setup: one-shot script, generated secrets
11Backup: mongodump, Redis RDB, configs, Google Drive, 30d retention
12Logrotate: daily rotation, compression, 14 days
13Monitoring: health check, disk/mem alerts, auto-restart
14Cleanup: preserve certs, full purge, reprovisioning ready

Conclusion

Security is infrastructure as code. Each layer here is a script that can be versioned, tested, and replicated. The goal isn’t perfection — it’s reducing the attack surface to an acceptable minimum and having visibility into what happens on your server.

The difference between a “configured” server and a hardened server is in the details: systemd sandboxing, Redis with renamed commands, auditd monitoring every execve as root, backups that actually work when you need them.

This article documents defensive security practices applied to real infrastructure. All configurations have been anonymized. Use these techniques only on your own servers or with authorization.


Rafael Cavalcanti da Silva — Fullstack Developer & Security Specialist rafaelroot.com