Gatekeeper inqaba-security/gatekeeper
v1.x

Egress Monitoring

Gatekeeper's other detectors watch inbound traffic. Egress monitoring watches the outbound HTTP calls your application makes — so the SOC can see when an external API is being abused: a runaway loop hammering a paid endpoint, a compromised dependency or SSRF exfiltrating data to an unknown host, or a partner API suddenly erroring.

It works by subscribing to Laravel's HTTP client events (RequestSending, ResponseReceived, ConnectionFailed) — no middleware, no code changes. Enable it and every Http:: call is tracked per destination host.

SIEM_EGRESS=true

All detections are alert-only. Exceeding a limit or tripping a spike emits a SIEM event — the outgoing call always proceeds. Egress monitoring never blocks your application's own traffic.

What it detects#

Detection Event Severity Catches
New destination egress_new_destination medium First-ever call to a host — SSRF, data exfiltration, a dependency phoning home to C2 (MITRE T1071)
Volume spike egress_spike_detected high Current call volume to a host far above its rolling baseline — runaway loop, abuse of a paid API (T1496)
Hard limit egress_limit_exceeded high A configured per-host request cap was crossed within the window (T1496)
Error spike egress_error_spike medium A surge of 4xx/5xx/connection failures to a host — outage, credential stuffing against a partner, broken integration (T1499)

New destination#

The highest-security-value signal, and the cheapest. The first time your app ever calls a host, Gatekeeper records it and emits egress_new_destination. Every subsequent call to that host is silent (the host is remembered for ttl_minutes, default 30 days).

Why it matters: a normal app talks to a stable, small set of external hosts (your payment provider, mail service, a few APIs). A call to a host you've never seen is exactly the signature of SSRF, a supply-chain compromise, or exfiltration — things no inbound monitoring can catch.

{
    "event": { "action": "egress_new_destination", "severity": 5 },
    "destination": { "domain": "evil-c2.example", "address": "evil-c2.example" },
    "threat": { "technique": { "id": ["T1071"], "name": ["Application Layer Protocol"] } },
    "url": { "full": "https://evil-c2.example/beacon" }
}

Volume spike#

Per host, Gatekeeper counts calls in fixed buckets (bucket_seconds, default 60) and keeps a rolling history of previous buckets as a baseline. When the current bucket exceeds multiplier × the baseline average (and clears an absolute min_count floor so low volume never trips), it fires egress_spike_detected.

This is the "external API being abused" alarm: a bug retrying in a tight loop, a queue worker gone wild, or an attacker using your server to hammer a third party all show up as a sudden surge above your normal rate.

# fire when a host's per-minute volume exceeds 5x its baseline, min 20 calls
SIEM_EGRESS_SPIKE=true

Hard limits (alert-only)#

Set an explicit ceiling per host. When calls exceed max within the window, egress_limit_exceeded fires — but the call still goes through.

// config/siem.php
'egress' => [
    'limits' => [
        'window_minutes' => 1,
        'default' => 0,              // 0 = no default cap
        'targets' => [
            'api.stripe.com' => 1000,   // alert if we exceed 1000 calls/min
            'api.openai.com' => 500,
        ],
    ],
],

Use this as a cost-control tripwire on paid APIs: you'll get a high-severity alert the moment usage runs away, long before the invoice arrives.

Error spike#

A surge of failed responses (4xx/5xx) or connection failures to one host trips egress_error_spike. Signals a partner outage, an expired credential (a flood of 401s), or someone using your integration to brute-force a downstream service.

The feedback-loop guard#

Gatekeeper's own sinks make outbound HTTP calls — Sentinel, Slack and Teams webhooks, and the ipapi GeoIP driver. Left unchecked, shipping an egress event would itself generate egress events, endlessly.

Gatekeeper prevents this automatically: egress recording is suppressed while the package delivers an event or performs a GeoIP lookup, so its own traffic is never monitored. You can add further exemptions (internal services, health checks) via ignore_hosts:

SIEM_EGRESS_IGNORE=internal-api.svc,metrics.internal

Limitation: Http client only#

Egress monitoring sees calls made through Laravel's HTTP client (the Http facade, and anything built on it). Calls made with raw Guzzle, cURL, or a vendor SDK that ships its own HTTP transport are not seen. Most first-party integration code uses the Http client; for a critical SDK that doesn't, point it at a Guzzle handler that uses Laravel's client, or wrap its calls.

Configuration#

Key Env Default
egress.enabled SIEM_EGRESS false
egress.ignore_hosts SIEM_EGRESS_IGNORE []
egress.bucket_seconds 60
egress.new_destination.enabled SIEM_EGRESS_NEW_DEST true
egress.new_destination.ttl_minutes 43200 (30 days)
egress.spike.enabled SIEM_EGRESS_SPIKE true
egress.spike.history_buckets 30
egress.spike.multiplier 5
egress.spike.min_count 20
egress.limits.window_minutes 1
egress.limits.default SIEM_EGRESS_LIMIT 0 (no cap)
egress.limits.targets [] (host → max map)
egress.errors.enabled true
egress.errors.window_minutes 1
egress.errors.threshold SIEM_EGRESS_ERROR_THRESHOLD 25

Privacy#

Outgoing URLs frequently carry API keys in the query string, so Gatekeeper strips the query string before shipping — events record scheme, host and path only. Request headers and bodies are never captured.

Operational notes#

  • Detections are deduplicated one-event-per-host-per-window, so a sustained condition produces one alert, not thousands.
  • State lives in the cache (per-host counters and baselines) — no migrations. Use a shared store (Redis) in multi-server setups so counts are cluster-wide, and note that a cache flush resets baselines and the "known hosts" set.
  • Because it rides on the queue like every other event, spike/limit alerting never adds latency to the outbound call itself.