Security

Firewall, WAF, and Rate Limiting for Self-Hosted LaraDashboard

By Lara Dashboard 5 views
Firewall, WAF, and Rate Limiting for Self-Hosted LaraDashboard
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.

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 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
If 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.
LayerCheckDone
Hostufw/firewalld deny-by-default; only 22/80/443 (or VPN) public
HostDB/Redis/queue ports not public
ProxyTLS + redirect; security headers; docroot public/
ProxyTrustProxies set; real client IP for limits
WAFCDN WAF or ModSecurity tuned; origin locked
fail2bansshd + login/form jails; office IPs whitelisted
LaravelNamed RateLimiters on login, reset, forms, API
LaravelShared cache (Redis) for multi-node limits
FormsHoneypot/CAPTCHA as needed; upload caps
OpsAlert 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.
Admin Panel laravel authorization laravel access control

Try Lara Dashboard for Free

Explore every feature live — no sign-up required.

Launch Live Demo