
Before you open a website to the internet: a security audit in ten checks
A website at home rarely gets hacked through the site itself. It gets hacked through what shares its door: an admin console published by habit, a tool that runs as root, a defence that is installed but does not work. We audited one of ours with nothing but reading and measuring, no attack. The site passed. Three things next to it did not, and we closed them the same night.
What you get. A non-destructive audit you can run on your own site in two hours, the command for each check, and the fix for every classic finding. Applied here, it turned up a remote code editor running as root and open to the internet, and a ban tool that had recognised 0 log lines out of 67,079. Both are fixed: admin consoles now answer 403 from outside, and the repaired filter banned its first scanner within minutes.
1. Look like a visitor: the headers
curl -s -o /dev/null -D - https://your-site.example/
Strict-Transport-Security: forces HTTPS for returning visitors. Often missing.Content-Security-Policy: which scripts, styles and images the page may load. The strongest single header against injected scripts.X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy.- No version in
Server(server_tokens off).
Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self';
style-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'
In nginx, an add_header inside a location cancels every add_header of the server block for that location. Our /media/ folder silently lost its security headers that way. Put the headers in a file and include it everywhere.
2. Encryption
for v in tls1 tls1_1 tls1_2 tls1_3; do echo | openssl s_client -connect your-site.example:443 -$v 2>&1 | grep -m1 Protocol; done
Only TLS 1.2 and 1.3 should answer. Check the certificate names too: one of the hosts we looked at served a certificate made for another name, which every browser refuses.
3. DNS
- DNSSEC: are answers signed? A DNS-over-HTTPS query shows the
ADflag. - CAA: which certificate authorities may issue for your domain. Without it, any may.
- SPF and DMARC: without DMARC, anyone can send mail pretending to be your domain.
- A wildcard record (
*.your-site.example) sends every invented name to your house: replace it with explicit records.
curl -s -H 'accept: application/dns-json' "https://cloudflare-dns.com/dns-query?name=your-site.example&type=CAA"
4. The neighbours: everything behind the same door
List every hostname your reverse proxy publishes, and for each ask: who besides me needs this from the internet? Admin consoles (the proxy's own panel, a DNS filter, a hypervisor, a web IDE) never do. Put them behind an access list that only allows your local network and your private network, in one click in Nginx Proxy Manager.
The worst case we met: a web code editor running as root on the machine that hosts the code forge, protected by a password alone. A web terminal must never run as root, and never be on the public internet.
5. Does the ban tool actually ban?
A fail2ban jail can be enabled, running, and blind. Test the filter against your real log:
fail2ban-regex /var/log/npm/proxy-host-1_access.log /etc/fail2ban/filter.d/npm.conf
# Lines: 67079 lines, 0 ignored, 0 matched <- blind
Two traps explained our zero. First, fail2ban removes the date from each line before applying the filter, so a pattern starting with the date never matches. Second, Nginx Proxy Manager writes the client address at the end, not the start. A filter that works on its format:
[Definition]
failregex = \] - (?:401|403|404|429) \d+ - \S+ \S+ \S+ "[^"]*" \[Client <HOST>\]
[npm]
ignoreip = 127.0.0.1/8 YOUR-LAN/24 YOUR-TAILNET-RANGE
The ignoreip line matters: once repaired, the filter matched thousands of lines, and many came from our own machines (a CI runner getting 404s). Without it, the fixed jail would ban your own infrastructure. With Docker, ban in the DOCKER-USER chain (see fail2ban).
6. Read the logs once
Count what reaches you. On the site we audited: 9,901 requests answered 401, 403 or 404 out of about 76,000, and hundreds of probes for /.env and /.git/config, from cloud servers. Those are robots looking for leaked secrets: make sure such paths do not exist in what you publish, and ban on the first probe.
7. The rest of the checklist
- Least privilege: no web service as root; container ports bound to
127.0.0.1behind the proxy. - Host firewall on the proxy machine: 80 and 443 for everyone, the rest for your private network only.
- Look from outside: from a phone on mobile data, scan your public address. Ports that seem open from home may just be your router answering itself.
- Rate limits and short timeouts at the proxy, against password guessing and floods (see resisting DDoS at home).
Traps
Audit without attacking: reading headers, DNS and your own configuration finds most problems. Never scan or test someone else's site without written permission.
Write the audit down, and keep the report private until the fixes are in: it is a map for whoever reads it.
Sources
- MDN, Content-Security-Policy
- RFC 6797, HTTP Strict Transport Security
- RFC 8659, DNS CAA records
- OWASP Secure Headers Project
Found this useful? Tell us what to measure next.