Honeypots are usually talked about in the context of web forms: a hidden field that catches bots. Useful, but not what I was after. What I wanted was something more fundamental, detecting someone actively trying to harm a system, and wasting their time doing it.
The honeypot idea is the same: lure with something attractive. But rather than filtering spam, I wanted to identify malicious intent and react accordingly.
Rethinking the honeypot
Most Laravel honeypot packages focus on form bots. That’s a real problem, but it’s a small slice of the threats a web project actually faces.
Three attacker profiles interested me more:
Automated scanners, tools like nuclei or nmap that sweep the internet looking for known attack surfaces. They probe endpoints like /wp-admin, /phpmyadmin, or readme.txt files that reveal vulnerable version numbers. They’re not targeting anyone specifically, they’re harvesting information at scale.
Opportunists, people who find an interesting endpoint and try to exploit it. A visible fake admin panel, and they start trying common credential combos: admin/admin, root/root. They’re looking for an unlocked door.
Targeted attackers, those who know the system and are deliberately trying to compromise it. Using a known username, a password from a data breach, or a combination that shouldn’t have been publicly accessible.
NotTodayHoney grew out of this reading. I wanted to give development teams, small projects and large applications alike, a simple, code-close solution that detects these three levels of intent and reacts accordingly.
What NotTodayHoney does
Full documentation is available at vinksyunit.github.io/NotTodayHoney.
Traps that look like the real thing
The package deploys fake login endpoints, traps, that mimic well-known interfaces:
- WordPress:
/wp-admin/wp-login.phpwith a spoofed version number, REST API discovery endpoints, user listings, and pluginreadme.txtfiles to attract CVE scanners - phpMyAdmin:
/phpmyadminwith session cookies that mimic a real instance, even before authentication - Generic Admin:
/admin/loginwith a configurable title to match whatever attackers are searching for
These endpoints are never reached by legitimate users. Every visit is therefore a signal.
Three detection levels
Collected events generate progressive alerts:
| Level | Trigger | Block duration | Severity |
|---|---|---|---|
| Probing | 3 visits in 24h | 20 minutes | Info |
| Intrusion Attempt | 1 login attempt | 24 hours | Warning |
| Attacking | Known credentials / compromised password | 30 days | Critical |
An IP visiting /wp-admin three times in a day is probably an automated scanner, a light alert is enough. An IP submitting credentials is a more serious threat. An IP using a password from a known data breach deserves a 30-day block and a critical alert.
Wasting the attacker’s time
All trap responses are delayed by a minimum of 1000ms, not to slow down legitimate users (who should never reach these endpoints), but to prevent an attacker from fingerprinting a honeypot by its response time.
On login traps, you can configure a fake_success response: the attacker sees an empty fake dashboard and believes they’ve gained access. They keep exploring, keep wasting time, while every action is logged.
Setup
composer require vinksyunit/not-today-honey
php artisan vendor:publish --tag="not-today-honey-config"
php artisan vendor:publish --tag="not-today-honey-migrations"
php artisan migrate
Requirements: PHP 8.4+ and Laravel 12+.
Block without giving yourself away
Once IPs are marked as threats, the HoneypotBlockMiddleware handles blocking them. The recommended approach is to not apply it globally. If a blocked IP gets a 403 on the homepage, it immediately knows it’s been identified and changes tactics.
The goal is the opposite: let the attacker browse public pages freely, but silently block them on anything that matters.
// bootstrap/app.php — targeted protection only
Route::middleware('nottodayhoney.block')->group(function () {
Route::post('/login', [AuthController::class, 'store']);
Route::post('/register', [RegisterController::class, 'store']);
Route::prefix('/api')->group(function () {
Route::post('/tokens/create', ...);
Route::post('/password/reset', ...);
});
Route::prefix('/admin')->group(function () {
// All sensitive admin routes
});
});
Result: the attacker can browse the site, everything looks normal. The moment they try to reach anything critical, the request is blocked. They don’t know why, they don’t know when it happened, and every new attempt is logged.
One more tool in the Blue Team stack
NotTodayHoney is a detection tool, not a prevention one. It doesn’t block network packets, enforce firewall rules, or fix application vulnerabilities. What it does is surface behavioral signals and alert on them.
That’s its exact role in a Blue Team stack, and it doesn’t claim to be more than that.
Serious defense is layered. NotTodayHoney sits in the application layer, alongside a reverse proxy or WAF at the traffic edge, IP blocking at the network level, strong authentication and a reduced attack surface, injection-focused code reviews, and penetration testing that surfaces what honeypots can’t see.
What NotTodayHoney brings is visibility into real behavior, not simulations. The credentials submitted on a fake /wp-admin are the actual credentials someone tried. That’s information no other tool in the stack collects.
For small projects, it’s often the first piece of operational security monitoring you can deploy without budget or complex infrastructure. For larger ones, it’s an additional signal source that feeds into an existing SIEM.
Easy to install, close to the code, low cost, that’s exactly what I had in mind when I wrote it.