You locked HTTPS. You hardened login. Backups are on a calendar.
Bots still hammer admin login and public forms all day.
Credential stuffing does not need a clever zero-day. It needs an open port and a patient script. A host firewall, a reverse proxy with sane headers, a WAF layer, fail2ban, and Laravel rate limits close that abuse gate. Without them, auth and patches only slow the noise.
This is a practical layering guide for self-hosted Laravel CMS admins. It focuses on login, contact forms, and API abuse. Disclosure up front: we build LaraDashboard, an open-source Laravel admin and CMS. The patterns apply to any Laravel app you own. Use LaraDashboard when you want routes and forms you can throttle on purpose instead of waiting on a pile of plugins.
Why layers beat a single shield
Many teams enable HTTPS and hope. Others put Cloudflare in front and skip app-level throttles. Both leave gaps.
Your origin IP can still get hit. Laravel still pays for every request that reaches PHP. Edge rules will not catch every bypass path.
Aim for layered controls:
- Block what should never reach the box (host firewall).
- Terminate TLS and add headers at the reverse proxy.
- Filter hostile shapes at a WAF before PHP runs.
- Ban repeat offenders with fail2ban.
- Cap what the app accepts per IP and per user with Laravel
RateLimiter.
Auth without traffic controls still leaves you open to stuffing and spam.
Host firewall: deny by default
Start on the OS. If SSH, HTTP, and HTTPS are the only public services, close everything else.
On Ubuntu/Debian with ufw:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Confirm SSH still works before you close the laptop. Prefer allowlisting SSH from stable admin egress when you can.
Do not expose MySQL, Redis, Elasticsearch, phpMyAdmin, or queue dashboards on public ports. Bind them to localhost or a private network. Cloud security groups should match the host rules so the next operator is not confused by a wide cloud rule and a tight ufw.
Reverse proxy: nginx or Caddy
Laravel should sit behind nginx or Caddy in production. The proxy terminates TLS, sets headers, and forwards to PHP-FPM or your container.
Serve HTTPS only. Redirect HTTP to HTTPS. Set TrustProxies so X-Forwarded-For and X-Forwarded-Proto come from your proxy only. Wrong trust settings break HTTPS detection and rate-limit IP keys.
Add headers you can maintain: HSTS (once HTTPS is solid), X-Content-Type-Options: nosniff, a sane Referrer-Policy, and frame controls. Start CSP in report-only if a Livewire admin breaks.
Point the document root at public/ only. Hide /.env and /vendor. Cap client_max_body_size on public forms. Prefer limit_req zones on login and form locations when you want edge throttling before PHP.
One proxy config in git beats tribal knowledge on the box.
WAF: buy time, not a substitute for patches
A WAF inspects HTTP for known attack shapes: SQLi probes, XSS payloads, path traversal, noisy bots. It is not a substitute for patching. It reduces noise.
ModSecurity + OWASP CRS works on nginx/Apache. Run DetectionOnly first. Whitelist narrow paths that break (uploads, some Livewire endpoints). Paranoia mode will fight a rich CMS editor if you skip tuning.
CDN WAF (Cloudflare and similar) gives managed rules without maintaining CRS yourself. Lock the origin so only the CDN can reach app ports. If the origin IP is public and open, attackers bypass the WAF entirely.
Small VPS with one CMS: CDN WAF plus host firewall is often enough. Air-gapped hosts: ModSecurity on the proxy. High traffic APIs: combine edge WAF with precise Laravel throttles so good API keys are not painted like scrapers.
fail2ban as a backstop
fail2ban watches logs and turns abuse into firewall drops.
Enable the sshd jail. Prefer key-only SSH. For the web, filter nginx/Caddy logs for repeated POST /login failures or contact endpoint floods. If Laravel logs auth failures, point a jail there.
Whitelist office and VPN egress. After you adopt a CDN, ban the real client IP, not the CDN edge. That means correct forwarded headers in both the proxy and the jail filters.
Laravel rate limits that actually share state
Edge controls reduce volume. App rate limits decide how many attempts a client may make against login, password reset, forms, and APIs.
Define named limiters:
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Attach throttle:login on the login POST. Use stricter limits on password reset and 2FA challenge routes. Attackers often pivot there after login is locked.
Public APIs need separate budgets from browser forms. Authenticated tokens can be keyed by user or token. Return proper 429 with Retry-After.
Cache driver matters. Counters live in cache. Use Redis (or another shared store) with multiple app nodes. The file driver gives each node its own generous budget.
If you customized Fortify/Breeze login, verify built-in login rate limiting is still wired. Custom controllers often forget it.
Login and forms are the loud endpoints
Treat admin login and public forms as high-value.
Combine strong passwords, 2FA, throttle middleware, fail2ban on repeated failures, and optional VPN or allowlist for /admin when team size allows. Use generic login errors to reduce account enumeration.
On public forms: honeypot, time-based checks, throttle, optional CAPTCHA when spam stays high, server-side validation always, upload caps at the proxy.
During a stuffing run, occasional 429s for real users beat an open login. Document how to unban an IP and how to raise a limiter temporarily during a launch.
A short checklist
| Layer | Check |
|---|---|
| Host | Deny-by-default; only SSH/80/443 (or VPN) public |
| Host | DB/Redis/queue ports not public |
| Proxy | TLS + headers; docroot public/; TrustProxies correct |
| WAF | CDN WAF or tuned ModSecurity; origin locked |
| fail2ban | sshd + login/form jails; office IPs whitelisted |
| Laravel | Named limiters on login, reset, forms, API; Redis cache |
| Forms | Honeypot/CAPTCHA as needed; upload caps |
| Ops | Alert on ban spikes and 429 floods; unban runbook |
Common failure cases
- Open origin behind CDN: attackers skip the WAF. Firewall origin to CDN ranges or a tunnel.
- Wrong client IP: every visitor shares one proxy address. Fix TrustProxies and
real_ip. - Throttle only in nginx: internal routes still see abuse. Keep Laravel limits too.
- Untuned CRS: blocks editors. DetectionOnly, then narrow allowlists.
-
filecache on many nodes: limits reset per box. - fail2ban on CDN IPs: you ban infrastructure, not bots.
Where LaraDashboard fits
Disclosure again: LaraDashboard is our open-source Laravel admin and CMS. We recommend it when you want a self-hosted CMS you own, with content, media, roles, and forms in one Laravel-native back office.
You still own the firewall, proxy, WAF, and rate-limit wiring. That is the point versus a shared host you cannot tune. Standard Laravel middleware and your reverse proxy apply. LaraDashboard does not replace ufw, fail2ban, or a CDN WAF. It gives you routes and forms you can throttle on purpose.
Ending note
Firewall, WAF, and rate limiting close the abuse gate on a self-hosted Laravel CMS. Deny unused ports, terminate TLS at nginx or Caddy, filter at a WAF, ban repeat offenders with fail2ban, and throttle login and forms in Laravel.
Keep this next to your HTTPS, auth, and backup checklists. For the full walkthrough with FAQ and the living checklist table, read the canonical post on LaraDashboard:
https://laradashboard.com/blog/firewall-waf-and-rate-limiting-for-self-hosted-laradashboard
If you want a Laravel-native CMS you can harden on your own terms, try the demo or docs on laradashboard.com.
Top comments (0)