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.