You locked HTTPS, hardened login, and set backup drills. Bots still hammer admin login and public forms all day.
A host firewall, a reverse proxy with TLS headers, a WAF layer, fail2ban, and Laravel rate limits close that abuse gate. Without them, auth and patches only slow the noise.
This guide covers ufw or firewalld, nginx or Caddy basics, WAF options, fail2ban jails, and Laravel
RateLimiter plus throttle middleware. It focuses on login, contact forms, and API abuse on a self-hosted Laravel CMS.It pairs with How to Secure a Self-Hosted Laravel CMS, Harden a Laravel CMS for Production, Auth, 2FA, and Session Security, and Backups, Updates, and Patching. For product context, see what Lara Dashboard is.
Why firewall, WAF, and rate limits matter
Self-hosted means your public IP is on the map. Credential stuffing, form spam, and scrapers do not need a clever zero-day. They need an open port and a patient script.
A host firewall cuts unused ports. A reverse proxy terminates TLS and adds security headers. A WAF filters common web attack patterns before PHP runs. Rate limits and fail2ban slow repeat offenders on login and forms.
Many teams only enable HTTPS and hope. Others put Cloudflare in front and skip app-level throttles. Both leave gaps: origin IPs still get hit, and Laravel still pays the cost of every request that reaches the app.
Aim for layered controls. Block what never should reach the box. Filter what looks hostile at the edge. Cap what the app will accept per IP and per user.
Host firewall: ufw and firewalld basics
Start on the server OS. If SSH, HTTP, and HTTPS are the only public services, close everything else.
ufw (Ubuntu and Debian)
Default deny incoming. Allow SSH from your admin network when you can. Allow 80 and 443 for the site. Enable the firewall and confirm you can still SSH before you close the laptop.
Example baseline:
sudo ufw default deny incomingsudo ufw default allow outgoingsudo ufw allow OpenSSHsudo ufw allow 80/tcpsudo ufw allow 443/tcpsudo ufw enablesudo ufw status verboseIf MySQL or Redis run on the same host, do not open their ports to the world. Bind them to localhost or a private network. Database ports on 0.0.0.0 are a common self-hosted miss.
firewalld (RHEL, Alma, Rocky, Fedora)
Use the public zone for the WAN interface. Add services
http, https, and ssh. Reload and verify with firewall-cmd --list-all.Prefer rich rules or source IP allowlists for SSH when your team has stable egress IPs. Password SSH from the whole internet invites brute force even with fail2ban.
What not to open
Do not expose phpMyAdmin, Redis, Elasticsearch, or queue dashboards on public ports. Put them behind VPN, SSH tunnel, or private network only. Your CMS admin already needs strong auth; do not add a second public admin surface.
Cloud security groups (AWS, DigitalOcean firewall, Hetzner) should match the host rules. A wide cloud rule with a tight ufw still confuses the next operator. Keep both aligned and documented.
Reverse proxy: nginx and Caddy
Laravel should sit behind nginx or Caddy in production. The proxy terminates TLS, sets headers, and forwards to PHP-FPM or your app container.
TLS termination
Serve HTTPS only. Redirect HTTP to HTTPS at the proxy. Use Let's Encrypt or your provider certs. Prefer modern TLS (1.2+) and a sane cipher suite. HSTS belongs here once you know HTTPS works end to end.
Set
TrustProxies in Laravel so X-Forwarded-For, X-Forwarded-Proto, and related headers are trusted from your proxy only. Wrong trust settings break HTTPS detection and rate limit IP keys.Security headers
Add headers at the proxy or in Laravel middleware. Common set:
Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, X-Frame-Options or CSP frame-ancestors, and a Content-Security-Policy you can actually maintain.Start CSP in report-only if the admin UI breaks. Tighten once you know which CDNs and inline needs you have. Do not copy a strict CSP from a static site onto a Livewire admin without testing.
nginx notes
Limit request body size for public forms (
client_max_body_size). Hide server tokens. Prefer limit_req zones for login and form locations when you want edge throttling before PHP. Forward real client IP with real_ip if you sit behind another CDN.Do not pass requests to
/vendor, /.env, or other app paths that should never be public. Point the document root at public/ only.Caddy notes
Caddy makes TLS automatic for many VPS setups. Keep a short Caddyfile that reverse-proxies to your PHP-FPM or container. Add header directives for HSTS and nosniff the same way you would in nginx.
Whatever you pick, one proxy config in git beats tribal knowledge on the box. Document reload commands and cert renewal checks.
WAF options for a self-hosted Laravel CMS
A web application firewall inspects HTTP traffic for known attack shapes: SQLi probes, XSS payloads, path traversal, and some bots. It is not a substitute for patching. It buys time and reduces noise.
ModSecurity (self-hosted)
ModSecurity with the OWASP Core Rule Set (CRS) runs with nginx or Apache. It catches many common probes. Tune carefully: CRS in paranoia mode can block legitimate CMS admin actions and rich editors.
Run in DetectionOnly first. Review false positives. Whitelist paths that break (uploads, certain Livewire endpoints) with narrow exceptions. Log enough to debug without filling the disk.
Cloudflare and other WAF SaaS
Cloudflare, Fastly, AWS WAF, and similar products sit in front of your origin. You get managed rules, bot scores, and geo controls without maintaining CRS yourself.
Lock down origin so only the CDN can reach your app ports (authenticated origin pulls, firewall allowlists, or tunnel). If the origin IP is public and open, attackers bypass the WAF.
SaaS WAF still fails closed if you misconfigure DNS or leave an alternate hostname pointing at the origin. Inventory every hostname.
When to choose which
Small VPS with one CMS: CDN WAF plus host firewall is often enough. Regulated or air-gapped hosts: ModSecurity on the reverse proxy. High traffic with API clients: combine edge WAF with precise Laravel throttles so legitimate API keys are not painted with the same brush as scrapers.
fail2ban for SSH, login, and forms
fail2ban watches logs and bans IPs that match abuse patterns. Pair it with your firewall so bans become drop rules.
SSH jail
Enable the sshd jail. Tune
maxretry and bantime. Prefer key-only SSH and a non-default port or allowlist when practical. fail2ban is a backstop, not your only SSH control.Web login and form jails
Ship or write filters for nginx or Caddy access/error logs that catch repeated
POST /login 401/422 responses, or repeated hits on contact endpoints. Ban after a threshold within a find time.If Laravel logs auth failures to a file or syslog, point a jail at that log. Keep filters specific so a marketing spike on a public page does not ban half your readers.
Operational care
Monitor ban lists. Whitelist your office or VPN egress. Recidive jails help against slow scanners. After CDN adoption, ban the real client IP, not the CDN edge. That means correct forwarded headers and filters that read them.
Laravel rate limiting: RateLimiter and throttle
Edge controls reduce volume. App rate limits decide how many attempts a client may make against login, password reset, forms, and APIs. Laravel gives you
RateLimiter and the throttle middleware.Define named limiters
In
AppServiceProvider (or a dedicated provider), define named limiters with RateLimiter::for. Key by IP for guests. Key by user id for authenticated API calls. Return Limit::perMinute(n) with clear names like login, contact, and api.Example shape:
RateLimiter::for('login', function (Request $request) { return Limit::perMinute(5)->by($request->ip());});Use stricter limits on password reset and 2FA challenge routes. Attackers often pivot there after login is locked.
Attach throttle middleware
On routes:
->middleware('throttle:login') for the login POST. Use throttle:6,1 style only when a quick inline limit is enough. Named limiters stay easier to tune and test.For form endpoints, throttle by IP and optionally by email or phone field hash so one botnet IP rotating targets still slows down. Do not log raw PII in rate limit keys.
API and Sanctum
Public API routes need separate budgets from browser form posts. Authenticated tokens can have higher limits keyed by token or user. Return proper 429 responses with
Retry-After so good clients back off.If you use Laravel's built-in login rate limiting helpers in Fortify or Breeze scaffolding, verify they are still wired after you customize auth. Custom login controllers often forget to call them.
Cache driver matters
Rate limit counters live in the cache. Use Redis (or another shared store) in production with multiple app nodes. The
file or array driver will not share limits across servers. That leaves each node with its own generous budget.Bot abuse on admin login and public forms
Admin login and contact or newsletter forms are the loudest targets on a CMS. Treat them as high-value endpoints.
Admin login
Combine: strong passwords, 2FA (see the auth post), throttle middleware, fail2ban on repeated failures, and optional allowlist or VPN for
/admin or /login when your team size allows it.Avoid revealing whether an email exists on failed login. Generic errors reduce account enumeration. Log failures for jails and alerts without dumping passwords.
Public forms
Add honeypot fields, time-based checks, and throttle. CAPTCHA (Turnstile, hCaptcha, reCAPTCHA) helps when spam volume stays high after throttles. Validate and sanitize server-side no matter what the browser did.
Rate limit by IP and by destination inbox. Cap uploads on public forms. Reject unexpected content types early at the proxy when you can.
What good looks like under load
During a stuffing run, legitimate users may see occasional 429s. That is better than open login. Watch metrics: 429 rate, ban count, form submission success, and false positive tickets from editors.
Document how to unban an IP and how to raise a limiter temporarily during a launch. Panic edits without a runbook create new holes.
A practical layering checklist
Use this as a living checklist. Assign an owner. Tick boxes in the runbook.
| Layer | Check | Done |
|---|---|---|
| Host | ufw/firewalld deny-by-default; only 22/80/443 (or VPN) public | ☐ |
| Host | DB/Redis/queue ports not public | ☐ |
| Proxy | TLS + redirect; security headers; docroot public/ | ☐ |
| Proxy | TrustProxies set; real client IP for limits | ☐ |
| WAF | CDN WAF or ModSecurity tuned; origin locked | ☐ |
| fail2ban | sshd + login/form jails; office IPs whitelisted | ☐ |
| Laravel | Named RateLimiters on login, reset, forms, API | ☐ |
| Laravel | Shared cache (Redis) for multi-node limits | ☐ |
| Forms | Honeypot/CAPTCHA as needed; upload caps | ☐ |
| Ops | Alert on ban spikes and 429 floods; unban runbook | ☐ |
Print it next to the HTTPS, auth, and backup checklists from the earlier security posts.
Common failure cases
Open origin behind CDN. Attackers hit the IP directly and skip WAF rules. Firewall the origin to CDN ranges or a tunnel.
Wrong client IP. Every visitor shares one proxy address. Rate limits ban the CDN or never trip. Fix TrustProxies and real_ip.
Throttle only in nginx, nothing in Laravel. App servers behind a mesh still see abuse on internal routes. Keep both.
CRS blocking editors. Untuned ModSecurity fights your CMS. DetectionOnly, then narrow allowlists.
file cache on multiple nodes. Limits reset per box. Use Redis.
fail2ban on CDN IPs. You ban infrastructure, not bots. Parse forwarded headers.
Where LaraDashboard fits
Disclosure: 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. You are not waiting on a dozen unrelated plugins to ship their own half-working flood controls.
LaraDashboard does not replace ufw, fail2ban, or a CDN WAF. It gives you routes and forms you can throttle on purpose. Pair this with the auth and session guide and the production hardening checklist.
FAQ
Do I need both a WAF and Laravel throttle middleware?
Yes if you can. WAF cuts obvious probes early. Laravel throttles protect login and forms with app-aware keys even when traffic is "valid" HTTP.
Is Cloudflare enough without ufw?
No. Lock the host firewall and origin access. CDN misconfig or a leaked IP should not expose SSH, Redis, or the app port to the world.
ufw or firewalld?
Use the tool your distro supports well. The policy matters more than the brand: deny incoming by default, allow only what you need.
Where should I put rate limits: nginx or Laravel?
Both when practical. nginx
limit_req sheds load before PHP. Laravel named limiters encode login and API rules with the right keys.Will fail2ban ban real users?
It can, especially behind a misconfigured proxy. Whitelist known offices, fix client IP parsing, and keep bantime reasonable with clear unban steps.
What about bot abuse on contact forms?
Throttle, honeypot, optional CAPTCHA, and server-side validation. Cap body size at the proxy. Alert when submission rates jump.
Does LaraDashboard change this stack?
You run a Laravel app you control. Firewall, WAF, and RateLimiter patterns are normal Laravel ops. You still must configure them on your host and routes.
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.
Auth and backups without traffic controls still leave you open to stuffing and spam. Keep this checklist next to your security runbook.
If you want a Laravel-native CMS you can harden on your own terms, try the LaraDashboard demo or read the docs on the site. Pair this guide with the self-hosted security pillar, production hardening, auth, and backups posts. Keep the next deploy behind a clear network and rate-limit bar.