GeoIP Enrichment
GeoIP enrichment adds where to every event's who: ECS source.geo.* fields (country, city, coordinates) and source.as.organization.name (the network operator). Analysts see 203.0.113.9 (Oslo, NO) in Slack alerts, and can map, filter and alert on geography in the SIEM — impossible-travel detections, "admin login from a country we don't operate in", hosting-provider ASN filtering.
SIEM_GEOIP=true
SIEM_GEOIP_DRIVER=maxmind
Where enrichment happens (and why it's free)#
Lookups run at delivery time on the queue worker — never during the user's request. Results are cached per IP (default 24 h), including misses, and a failing resolver is backed off for 5 minutes. Failure semantics are strict: any resolver error degrades to an un-enriched event. GeoIP can never delay a request or lose an event.
Enriched fields:
{
"source": {
"ip": "203.0.113.9",
"geo": {
"country_iso_code": "NO",
"country_name": "Norway",
"city_name": "Oslo",
"location": { "lat": 59.91, "lon": 10.75 }
},
"as": { "organization": { "name": "AS2119 Telenor" } }
}
}
Private and reserved addresses (10.x, 192.168.x, loopback, ...) are skipped automatically.
Driver: MaxMind (recommended)#
Offline lookups against a local GeoLite2/GeoIP2 City database — microseconds per lookup, no network dependency, no third-party terms on your traffic.
composer require geoip2/geoip2
SIEM_GEOIP_DRIVER=maxmind
SIEM_GEOIP_MMDB=/var/lib/geoip/GeoLite2-City.mmdb
GeoLite2 databases are free with a MaxMind account. Keep the file fresh with their geoipupdate tool on a weekly cron — IP geolocation drifts constantly.
Driver: ipapi (zero dependencies)#
HTTP lookups against an ip-api.com-style JSON endpoint. No package, no database file — at the cost of a network call per uncached IP and the provider's terms of use (ip-api.com's free tier is non-commercial; the URL is configurable to any compatible endpoint, including their paid HTTPS one).
SIEM_GEOIP_DRIVER=ipapi
# optional: custom endpoint, {ip} is substituted
SIEM_GEOIP_URL="https://pro.ip-api.com/json/{ip}?key=...&fields=status,country,countryCode,city,lat,lon,as"
Because results are cached per IP and lookups run on the worker, even the HTTP driver adds no user-facing latency — a scanner probing you 500 times costs exactly one lookup.
Custom resolver#
Bind your own implementation (commercial geo API, internal IP-intelligence service, threat-intel enrichment):
use Inqaba\Gatekeeper\Geo\GeoIpResolver;
use Inqaba\Gatekeeper\Geo\GeoLocation;
$this->app->bind(GeoIpResolver::class, fn () => new class implements GeoIpResolver {
public function resolve(string $ip): ?GeoLocation
{
$data = MyGeoService::lookup($ip);
return new GeoLocation(
countryIso: $data->country,
countryName: $data->countryName,
city: $data->city,
lat: $data->lat,
lon: $data->lon,
organization: $data->asn,
);
}
});
Return null for unknown addresses; throw for infrastructure errors (the enricher catches, logs nothing sensitive, and moves on).
Configuration#
| Key | Env | Default |
|---|---|---|
geoip.enabled |
SIEM_GEOIP |
false |
geoip.driver |
SIEM_GEOIP_DRIVER |
maxmind |
geoip.cache_minutes |
— | 1440 |
geoip.maxmind.database |
SIEM_GEOIP_MMDB |
storage/app/geoip/GeoLite2-City.mmdb |
geoip.ipapi.url |
SIEM_GEOIP_URL |
ip-api.com template |
geoip.ipapi.timeout |
— | 3 |