Gatekeeper inqaba-security/gatekeeper
v1.x

Honeypots

Honeypots turn attacker behaviour into your highest-confidence telemetry. No legitimate user ever requests /wp-login.php on a Laravel app or fills a form field that isn't visible — so every trigger is signal, never noise.

Route honeypots#

Decoy endpoints registered automatically from honeypots.routes.paths. A hit:

  1. emits a high-severity honeypot_route_hit event (MITRE T1595.003, Wordlist Scanning),
  2. optionally auto-blocks the source IP (default: 24 hours), and
  3. returns an innocuous 404 so the attacker learns nothing.
'routes' => [
    'enabled' => true,
    'paths' => [
        '.env',
        '.git/config',
        'wp-login.php',
        'wp-admin',
        'xmlrpc.php',
        'phpmyadmin',
        'backup.sql',
        // ...
    ],
    'response_status' => 404,
    'tarpit_ms' => 0,
    'auto_block' => true,
    'block_minutes' => 1440,
],

The default list covers high-signal scanner targets: WordPress paths, exposed dotfiles, database dumps, RCE probe paths. Add decoys that fit your stack — and make sure a decoy never shadows a real route.

Parameterized honeypots#

Decoys can take route parameters, which is how you trap ID-guessing scripts on endpoints that look like real resources:

'paths' => [
    'internal/photos/{id}',
    ['path' => 'api/v1/export/{file}', 'methods' => ['GET'], 'where' => ['file' => '.*\.sql']],
],
  • String form — any URI, parameters allowed.
  • Array form — adds methods (restrict HTTP verbs) and where (regex constraints per parameter).

The probed values land in the event context, so the SOC sees exactly what the attacker tried:

{
    "laravel": {
        "context": {
            "honeypot_pattern": "internal/photos/{id}",
            "route_params": { "id": "42" },
            "auto_blocked": true
        }
    }
}

The tarpit#

tarpit_ms delays honeypot responses by a random interval (half to full value). Against mass scanners this wastes attacker time at nearly zero cost to you — but note the sleep occupies a PHP worker, so keep values modest (e.g. 2000) or leave it off on small worker pools.

CSRF and honeypot routes#

Honeypot routes are deliberately registered without the web middleware group. If they had it, Laravel's CSRF layer would reject POST probes with a 419 before the hit was ever logged — and the whole point is to log it. Honeypots need no session state, so nothing is lost.

Form honeypot#

Two invisible traps for bots on public forms, packaged as a Blade component plus middleware.

Usage#

<form method="POST" action="{{ route('register') }}">
    @csrf
    <x-siem::honeypot />
    ...
</form>
Route::post('/register', [RegisteredUserController::class, 'store'])
    ->middleware('siem.honeypot');

Your controller doesn't change — the honeypot fields are just extra input it ignores.

How it catches bots#

The component renders an off-screen text input and an encrypted timestamp:

  • Hidden field — invisible to humans (aria-hidden, off-screen, tabindex="-1"), but bots parsing the HTML fill everything. Non-empty ⇒ hidden_field_filled.
  • Timer — the encrypted render time is compared to the submit time. Faster than min_seconds (default 2) ⇒ submitted_too_fast. A forged or corrupted timestamp ⇒ timer_tampered.

Each trigger emits a honeypot_form_triggered event with the reason, IP and user agent.

Reject vs silent mode#

respond Behaviour
reject (default) 422 response, like a validation failure
silent Fake success (200 / redirect back) — the bot believes it worked and keeps wasting time instead of adapting

Why CSRF doesn't make this redundant#

CSRF verifies that the client loaded your form first — not that a human filled it. Modern spam bots GET the page, extract the CSRF token with all other fields, and POST it back; that's a valid CSRF flow. Those bots are exactly what the honeypot catches. And a CSRF 419 is silent, while a honeypot catch is classified SIEM telemetry that can feed your blocklist. The layers stack: CSRF kills blind POSTs → honeypot kills token-aware scripts → abuse detection catches whatever persists.

Real-world notes#

  • Deploy on unauthenticated forms that write data or send email: registration, contact, newsletter, comments, quote requests.
  • Full-page caching freezes the timestamp — humans aren't false-positived (older is fine) but the min-time trap weakens on cached pages. The hidden field is unaffected.
  • SPAs / JSON clients — the component is Blade-only; render the two fields yourself using the configured names, or rely on the hidden field alone.
  • Change the field name per app (SIEM_HONEYPOT_FIELD=fax_number) — anything that sounds like a real field bots would want to fill.