Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Crowdsec Laravel Package

rilo-arbabillah/laravel-crowdsec

Laravel CrowdSec integrates CrowdSec with Laravel apps, offering middleware and helpers to query the CrowdSec Local API and block malicious IPs before they reach your routes. Simplify bot and attack mitigation with configurable, cache-friendly protection.

View on GitHub
Deep Wiki
Context7

Getting Started

This package intercepts requests, scores behaviour, blocks abusive IPs, and runs a WAF-style scenario engine over input. In v1.7.0 the two things you must do first are unrelated to any feature you're buying: define two gates and check your published api.middleware.

1. Install and publish.

composer require vendor/crowdsec
php artisan vendor:publish --tag=crowdsec-config
php artisan vendor:publish --tag=crowdsec-migrations
php artisan migrate

Two new migrations ship in this release (optimize_crowdsec_indexes, widen_whitelisted_ips_expires_at) and there is a new top-level retention config key plus the jndi_injection scenario key. Because both live at the top level, a config you published earlier still picks them up — no republish required. If you have a customised published config, diff it against the package stub rather than assuming you inherited them.

2. Define the gates before you deploy. This is the upgrade blocker. The dashboard and read API now require viewCrowdSec; block/unblock/whitelist require manageCrowdSec. The package defines both to allow only in the local environment — so in staging and production every logged-in user gets a 403 until you define them:

// AppServiceProvider::boot()
Gate::define('viewCrowdSec', fn ($user) => $user->is_admin);
Gate::define('manageCrowdSec', fn ($user) => $user->is_admin || $user->can('manage-security'));

Do this in the same deploy as the upgrade, not after. If you previously relied on "any authenticated user can see the dashboard", that behaviour is intentionally gone and your 403 is not a bug.

3. Add the throttle to your published API middleware. Published configs keep their own api.middleware, so the new throttle:60,1 default will not reach you. Add it by hand:

'api' => [
    'middleware' => ['api', 'auth', 'throttle:60,1'],
],

First use case: point your login and 404 handling at the package's trackers, then load the dashboard in local to confirm scenarios fire, and finally run php artisan crowdsec:block 203.0.113.10 --reason="manual test" --duration=60 to verify the block path end to end. php artisan crowdsec:unblock 203.0.113.10 reverses it.

Where to look first: the scenarios block of the config (jndi_injection is the new critical entry), then retention, then the routes file — every API route is now named crowdsec.api.*, which is what the dashboard whitelist panel uses.

Implementation Patterns

Manual moderation from your own admin panel. Reach for the new commands rather than writing rows yourself — they go through the same service layer as automatic blocks:

Artisan::call('crowdsec:block', ['ip' => $ip, '--reason' => 'abuse report', '--duration' => 1440]);
Artisan::call('crowdsec:unblock', ['ip' => $ip]);

Because upsertBlock() now refuses to shorten an active block, a manual 7-day block survives a subsequent honeypot hit that would have set 4 hours. You can hand out long blocks without them being silently downgraded.

Escalation with a sane ceiling. Blocks are driven by block_count; the fix in this release means automatic blocking now halves a threat score that is at or above threat_score_threshold instead of leaving it primed, so an IP is not re-blocked on the first request after expiry. Let retention.block_count_reset_days (default 30, CROWDSEC_BLOCK_COUNT_RESET_DAYS) decay the counter for shared/NAT IPs via crowdsec:cleanup --old-behaviors. For carrier-grade NAT ranges you may want this lower than 30 — otherwise the whole range sits at the escalation cap.

Scenario-specific block durations. WAF blocks now honour block_duration per scenario, taking the longest among the blocking threats, and falling back to the severity defaults. If you add a custom scenario for, say, credential stuffing, set block_duration explicitly — it is no longer guessed from the reason string.

Trusting jwt_privilege_escalation in monitor mode. This scenario now defaults to monitor instead of enforce, so bearer tokens with role=admin no longer get legitimate admins blocked. jwt_payload_* threats inherit their base scenario's mode and exclusions. If you want enforcement, opt in deliberately and make sure your own admin tokens are excluded — do not flip it back to enforce blindly on the strength of an old config.

JNDI coverage as a log4shell tripwire. jndi_injection is critical and inspects every input, including headers and User-Agent, matching ${jndi:…} and obfuscated forms. Combined with the new header inspection (non-standard headers on header_injection and jndi_injection), a scanner probing X-Api-Version: ${jndi:ldap://…} will now get blocked. Expect this to catch vulnerability scanners that previously sailed through.

Exporting for a SIEM or an incident. Use the filters rather than pulling everything:

php artisan crowdsec:export --format=csv --event-type=jndi_injection --from=2026-10-01 --output=/tmp/jndi.csv
php artisan crowdsec:export --ip=203.0.113.10 --from="2026-10-01 09:00:00" --to="2026-10-01 17:00:00"

A plain date still covers the whole day; a date-time is used verbatim. Unknown --format, --severity, or an unwritable --output now fail loudly — wrap the call in a scheduled job that alerts on non-zero exit, because it will no longer silently emit JSON.

Read APIs and dashboards. GET /api/crowdsec/check/{ip} returns behaviour counters only (request_count, error_404_count, login_attempts, threat_score, block_count, last_activity) — no full row. If your own tooling consumed the old shape, it now needs the named routes or the database for anything else. Build links with route('crowdsec.api.whitelist') style names rather than hardcoding paths.

Metrics polling. The metrics endpoint caches output for metrics.cache_seconds (default 15, 0 disables). If you scrape it per-request in a health check, raise the value; if you need live numbers in tests, set it to 0.

Scheduling. Cleanup tasks now run withoutOverlapping() and onOneServer(). Laravel 10 apps scheduling in app/Console/Kernel.php should add the audit-log task explicitly, since audit cleanup no longer piggybacks on --expired:

$schedule->command('crowdsec:cleanup --expired')->daily();
$schedule->command('crowdsec:cleanup --audit-logs')->daily();

Gotchas and Tips

The 403 after upgrade is the gate, not a bug. Outside local, both gates deny by default. Search your logs for authorization failures immediately after deploying; if you run a multi-tenant app, scope manageCrowdSec to tenant admins rather than a global is_admin, and remember the read gate too — "can view events" and "can block" are deliberately separate.

Your published api.middleware predates the rate limit. Without throttle:60,1 (or your own limiter) the API is unthrottled. This is the one security fix in the release that will not reach you automatically.

Whitelist CIDR semantics changed. Different address families never match now, so 2001:db8::/32 no longer swallows 32.1.13.184. IPv4-mapped IPv6 (::ffff:10.0.0.1) matches IPv4 entries, and IPv6 exact matches ignore notation. If you were getting accidental coverage from a cross-family match, you will start seeing blocks where you previously saw none — audit your whitelist as part of the upgrade.

Block expiry is now monotonic upward. An active block ending later (or never) keeps its expiry. To shorten a block you must crowdsec:unblock and re-block, because upsertBlock() will not overwrite expires_at with an earlier value. This is the intended fix, but it will surprise any script that assumed "block again with a shorter duration" worked.

Whitelist cache invalidation was broken on the first change. invalidateAllBlockedCaches() incremented a missing version key, which read as the implicit current version, so cached blocks survived the first whitelist edit. Fixed, but if you have long-running workers with their own caches keyed on block state, clear those yourself — the package cannot see them.

HoneypotTrap now fails open. It skips whitelisted IPs and, on failure, increments a counter, logs, and dispatches CrowdSecMiddlewareFailed instead of throwing. Listen for that event — a silent fail-open is exactly what you want for availability and exactly what you do not want to be blind to in production.

Input scanning limits. Payloads are scanned in overlapping 8 KB chunks up to 64 KB, so padding past 8 KB no longer hides a payload — but 64 KB is still the ceiling. Deeply nested bodies flatten 32 levels deep and then anything deeper is inspected as a single JSON string, so a payload split across nesting levels is seen, just not structurally.

Chunked uploads without Content-Length are now measured by reading the body. If you proxy large streaming uploads through a CrowdSec-guarded route, be aware the size check has real cost on those requests.

PCRE limits are set once per WAF pass and restored afterwards rather than four ini_set() calls per pattern. Your own code that reads pcre.backtrack_limit mid-request could observe the lowered value inside the middleware's window; don't tune PCRE settings from a middleware that runs after CrowdSec.

pcre evasion hardening is a moving target. The WAF now decodes %uXXXX, applies a third URL-decode, HTML-entity-decodes URL-decoded input, strips NUL bytes and SQL inline comments, and NFKC-normalises fullwidth characters (only when intl is installed). Verify ext-intl on your production image — without it, that last evasion class is unaddressed and nothing will tell you.

MySQL/MariaDB only: whitelisted_ips.expires_at is now DATETIME. Pre-2038 limits are gone, so crowdsec:whitelist add --expires=2099-12-31 works. The widening migration is a no-op on other drivers; on MySQL it rewrites the column, so plan for it on a large table.

Cleanup now deletes, not flags. crowdsec:cleanup --expired removes inactive blocked_ips rows older than retention.blocked_ips_days (default 30), in batches of 1,000. Anything reporting on historical blocks must read security_events, not blocked_ips. Same for audit logs — they were being wrongly deleted by any selector run and now have their own --audit-logs task, so check your audit retention is actually being enforced after upgrade.

Retention defaults land at 30 days for four new keys. If you have a compliance requirement longer than that, set CROWDSEC_EVENTS_RETENTION_DAYS etc. explicitly before the first nightly cleanup runs — otherwise the first --expired after upgrade silently prunes to 30 days.

Indexes. The new security_events (event_type, severity) composite backs the metrics grouping and drops the now-redundant security_events.event_type and blocked_ips.ip. If you built custom queries hinting at those indexes, re-check the plans.

Extending. The natural extension points are custom scenarios in the config (each with its own mode, severity, block_duration, and exclusions) and the CrowdSecMiddlewareFailed event. trackRequest() now accepts $additionalThreatScore, which is the supported way to fold your own signals into the behaviour score rather than writing to ip_behaviors yourself — the request path takes a single lock on a clean request (5 queries on SQLite versus 7 before), and bypassing it reintroduces the contention this release removed.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names