I run a production server on Hetzner (Ubuntu 24.04) and get hit with thousands of attack attempts daily. After 3 months of hardening, I've blocked 8,000+ IPs from 132 countries with zero successful intrusions.
Here's every step I applied, what actually worked, and the open-source tools I built to make it easier.
1. SSH (biggest single impact)
# /etc/ssh/sshd_config
Port 2222
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
AllowUsers myuser
Moving SSH port sounds like security through obscurity, but it dropped 90% of automated scans overnight. Real attackers scan all ports anyway — this just filters out the lazy bots.
2. Firewall + IP blacklisting
# Create ipset blacklist
ipset create blacklist_set hash:ip hashsize 65536 maxelem 131072
# Add to iptables
iptables -I INPUT -m set --match-set blacklist_set src -j DROP
# Atomic swap for zero-downtime updates
ipset create blacklist_tmp hash:ip hashsize 65536 maxelem 131072
# ... populate blacklist_tmp ...
ipset swap blacklist_tmp blacklist_set
ipset destroy blacklist_tmp
~8,000 IPs blocked and growing. ipset gives you O(1) lookup regardless of list size — regular iptables chains would crawl at this scale.
3. fail2ban (custom jails)
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 1
bantime = -1
action = %(action_)s
ipset-blacklist
-
maxretry=1— sounds aggressive, zero false positives in 3 months with key-only auth -
bantime = -1— permanent ban - 7 jails: SSH + 5 nginx patterns + Suricata integration
- Bans feed into ipset automatically
4. Suricata IDS
# Suricata + fail2ban integration
# /etc/fail2ban/filter.d/suricata.conf
[Definition]
failregex = \[.*\] .* \[Priority: [12]\] .* <HOST>
Running in IDS mode with ~32K rules. The integration chain: Suricata detects → fail2ban bans → ipset blocks. Each layer feeds the next.
5. nginx hardening
# block-exploits.conf — 37+ patterns, silent drop
location ~* (\.env|wp-login|phpMyAdmin|\.git|\.sql|/admin) {
return 444;
}
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
Status 444 = nginx closes the connection silently. No response body, no headers, no information leakage. Bots get nothing.
6. File integrity + Kernel hardening
# AIDE — monitors ~210K files
aide --init
cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Daily cron checks for unauthorized changes
# /etc/sysctl.d/99-hardening.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
Aligned with CIS benchmarks. SYN cookies alone prevent most SYN flood attempts.
7. Real-time monitoring
Telegram bot sends instant alerts for every ban:
fail2ban [sshd]
IP: 45.133.xx.xx
Action: Ban
Time: 2026-03-27 14:32:01
Plus a custom status script — uptime, disk, fail2ban stats, SSL expiry, recent attacks — all in one command.
Results after 3 months
| Metric | Value |
|---|---|
| IPs blocked | 8,000+ |
| Countries | 132 |
| Successful intrusions | 0 |
| False positives (SSH) | 0 |
| Avg server load | Minimal |
The tool I built to verify all this
After setting everything up, I wanted a quick way to check if my headers, SSL, and DNS were actually correct.
Every existing tool was either:
- Enterprise SaaS with a sales call
- A CLI that dumps 200 lines of raw output
- Free but limited to SSL-only
So I built ContrastScan — a free, open-source security scanner. Paste a domain, get an A-F grade covering 11 checks in under 3 seconds.
The core scanner is 2,300 lines of C — raw TCP handshakes for SSL, direct DNS queries via libresolv, HTTP header parsing with libcurl. No frameworks, no runtime bloat.
What it checks
| Module | Points | What It Checks |
|---|---|---|
| Security Headers | 25 | CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy |
| SSL/TLS | 20 | Protocol version, cipher strength, cert validity |
| DNS Security | 15 | SPF, DKIM, DMARC |
| HTTPS Redirect | 8 | HTTP → HTTPS redirect |
| Info Disclosure | 5 | Server/X-Powered-By exposure |
| Cookie Security | 5 | Secure, HttpOnly, SameSite |
| DNSSEC | 5 | DNS response signatures |
| HTTP Methods | 5 | TRACE, DELETE, PUT detection |
| CORS | 5 | Cross-origin misconfig |
| HTML Analysis | 5 | Mixed content, inline scripts, SRI |
| CSP Analysis | 2 | unsafe-inline, unsafe-eval, wildcards |
It also runs passive recon in the background: WHOIS, tech stack detection, WAF fingerprinting, subdomain enumeration, and subdomain takeover detection (checks for dangling CNAMEs across 30 services like GitHub Pages, Heroku, AWS S3, Netlify).
API access
# JSON
curl "https://contrastcyber.com/api/scan?domain=yourdomain.com"
# Text report
curl "https://contrastcyber.com/api/report?domain=yourdomain.com" -o report.txt
For AI agents (MCP)
I also built ContrastAPI — 20 security tools in one API:
claude mcp add --transport http contrastapi https://api.contrastcyber.com/mcp
CVE lookup, domain recon, tech fingerprinting, IP reputation, code security — no key required.
What surprised me
Moving SSH port had more impact than any other single change. Not "real" security, but eliminates 90% of log noise.
Most attacks are dumb bots. 95% hit
wp-login.phpon a server that doesn't run WordPress.maxretry=1 is not aggressive. With key-only SSH, a single failed attempt is always suspicious.
A single grade changes behavior. Raw headers → eyes glaze over. "Grade: D" → they want to fix it immediately.
Stack
| Layer | Tech |
|---|---|
| Scanner | C (libcurl, openssl, libresolv, cJSON) |
| Backend | Python, FastAPI, SQLite |
| API | Python, FastAPI, MCP |
| Frontend | Vanilla HTML/CSS/JS |
| Infra | Hetzner VPS, nginx, systemd, Let's Encrypt |
| Security | fail2ban, Suricata, ipset, AIDE |
No Docker. No Kubernetes. One VPS, two systemd services.
Try it
- Scan your domain: contrastcyber.com
- API: api.contrastcyber.com
- Source: github.com/UPinar/contrastscan
No signup, no API key. Happy to answer questions about any of these steps.
Top comments (0)