ahmedmerza / watchtower
Active blocking and cross-server coordination at the edge of your Laravel app — IPs (with bots and exploit paths in v1.0).
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/cache: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- ahmedmerza/logscope: >=1.6.1
- larastan/larastan: ^2.0|^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^1.0|^2.0
Suggests
- ahmedmerza/logscope: If installed, Watchtower adds a Block-IP button to the LogScope log detail panel and mounts its management routes under LogScope's prefix with LogScope's authorization. Requires >=1.6.1 — earlier versions include the pre-rename `logscope-guard::` partial, which this package no longer registers, so the button never renders. Watchtower is fully functional without it.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-24 08:43:27 UTC
README
IP blocking for Laravel apps, shared across every environment you run. Block a bad actor in one place and the rest see it within minutes. It runs on any Laravel cache driver, with no Cloudflare, no WAF and no infrastructure changes.
watchtower:simulate — see what a rule would have blocked last week, before you arm it. Read-only.
Why Watchtower
- Fast where it matters. Every request is checked against your cache before sessions, auth or routing run. The database is never queried per request.
- One block, every environment. Production, staging and the rest share one blocklist over signed requests.
- Automatic blocking that starts as a dry run. Detectors and log rules report what they would block until you arm them, and
watchtower:simulatebacktests a rule against your log history first. - Careful with real people. It won't auto-block an address that many signed-in users share, and you can block an address from your login routes only instead of the whole app.
- Nothing to build or host. A management page with no JavaScript and a JSON API. LogScope is optional: with it, log-based rules and
watchtower:simulatecan read your log history.
Quick Start
1. Install
composer require ahmedmerza/watchtower php artisan watchtower:install
This publishes the config and runs the migration.
2. Protect yourself first. Add your own IP to the never-block list before you block anything:
WATCHTOWER_ENABLED=true WATCHTOWER_NEVER_BLOCK_IPS=127.0.0.1,::1,your.own.ip
Nothing can block an address on this list. On IPv6, list your network (2001:db8:1:2::/64), not one address: blocking an address blocks its whole /64, but a never-block address protects only itself.
3. Block something. Open /watchtower and use the block form. In the local environment the page is open to everyone.
4. Decide who gets in, before you deploy. Outside local, the page and the API refuse everyone until you define the viewWatchtower Gate, the same model as Horizon and Pulse:
// app/Providers/AppServiceProvider.php use Illuminate\Support\Facades\Gate; public function boot(): void { Gate::define('viewWatchtower', fn ($user) => in_array($user->email, [ 'admin@example.com', ])); }
With LogScope installed, the page moves to /logscope/watchtower and LogScope's own authorization applies instead of the Gate. LogScope 1.6.1–2.1.x also show a Block IP button on each log entry; 2.2.0 removed it.
How It Works
Admin blocks an IP from the management page or the API (staging)
│
├─► DB row created + cache rebuilt → staging protected immediately
│
└─► Queued job pushes block to master env
│
└─► Every other env pulls from master via watchtower:sync (every 5 min)
└─► Cache rebuilt → all environments protected
A global middleware checks each request against the cache right after TrustProxies. A block can be one IP or a CIDR range. Blocked requests get a plain 403.
Features
| Feature | What it does | Docs |
|---|---|---|
| Management page | List, filter, block and unblock by hand. No build step, no JavaScript | Management Page |
| JSON API | Block, unblock, check and list blocks from your own scripts | Management API |
| Ranges and IPv6 | Block CIDR ranges, and a whole IPv6 /64 from one address |
Configuration |
| Cross-environment sync | A master/satellite setup that shares blocks between environments | Sync |
| Attack-tool filter | Rejects sqlmap, Nikto, WPScan, masscan and zgrab. On by default | User-Agents |
| Auto-block | Real-time detectors (failed logins, lockouts, scanner paths, 404 bursts) and rules over your logs | Auto-Block |
| Backtesting | watchtower:simulate replays a rule over past logs before you arm it |
Backtesting |
| Shared-IP guard | Holds back an auto-block on an address several signed-in users share | Shared IPs |
| Escalating durations | Longer blocks for addresses that come back | Escalation |
| Scoped blocks | Block an address from some routes, such as login, instead of the whole app | Scoped Blocks |
| Webhook | Posts every block to a URL, e.g. for Slack or n8n | Configuration |
All docs: docs/ · Configuration · Artisan commands · Security notes
Requirements
- PHP 8.2, 8.3 or 8.4
- Laravel 12 or 13 (Laravel 13 needs PHP 8.3+). Laravel 11 isn't supported: it is past security support, and a current Composer refuses to install it.
- Any Laravel cache store. Redis is recommended for production.
- Optional: ahmedmerza/logscope >= 1.6.1, for log-based auto-block rules and backtesting. The detectors work without it. LogScope 1.6.1–2.1.x also show a Block IP button on each log entry.
Upgrading
Run php artisan migrate after every upgrade, then read Upgrading. Two changes are easy to miss:
- ⚠️ Since v0.4.0, auto-block defaults to
warn: it reports instead of blocking. SetWATCHTOWER_AUTO_BLOCK_MODE=blockif you relied on it to block. - ⚠️ Since v0.6.0,
GET /api/blocksis paginated, sodatais one page, not the whole list.
Status
Heading to v1.0. Blocking, sync, the management page and API, and the auto-block engine are in place and tested. Still to come: pushing blocks out to Cloudflare or an nginx deny file, and an importer for antonioribeiro/firewall users. See the roadmap.
Security
- Configure trusted proxies, or every request looks like it comes from your load balancer.
- A cache outage fails open: requests are let through, and the failure is logged about once a minute.
- Sync requests are HMAC-signed. The shared secret can block any IP on every environment, so treat it as a credential.
Details: Security Notes.
Contributing
Contributions are welcome. Please open an issue or submit a pull request on GitHub.
License
MIT License. See LICENSE for details.
