Appearance
GeoIP & Impossible Travel
GrydAuth uses GeoIP resolution to power impossible travel detection — a Zero Trust control that flags (and, by default, blocks or challenges) logins whose geographic displacement is physically implausible for the elapsed time. This page describes everything a consuming application needs to configure the control correctly and securely.
This control is fail-secure by default. Once GeoIP services are registered, an unavailable database or a critical geographic risk results in a step-up challenge or a block — never a silent allow. Read Security behavior before deploying, and see the Security Features page for the wider Zero Trust context.
How it works
Client IP ──▶ GeoIP lookup ──▶ location + accuracy radius
│
▼
per-device location history + trusted locations
│
▼
impossible-travel / hijack analysis (effective distance, velocity)
│
▼
GeoRiskSignal ──▶ IRiskDecisionPolicy ──▶ Allow | Step-up | Block- The client IP is resolved to a city-level location (latitude/longitude plus an accuracy radius representing GeoIP uncertainty). Private/loopback/link-local and IPv6 ULA addresses are treated as local network and never trigger the control.
- The current location is compared against the user's recorded history for the same device and against the user's trusted locations.
- The service computes the effective distance and the velocity required to cover it, and derives a
GeoRiskSignal(impossible travel, concurrent-session hijack, trusted, uncertainty, or database unavailability). - A single decision policy (
IRiskDecisionPolicy) maps the signal to one action: Allow (auditable), RequireStepUp, or Block.
Prerequisites
The default provider is MaxMind GeoLite2-City, which uses a local .mmdb database.
- Create a MaxMind account and generate a license key (the free GeoLite2 tier is sufficient): https://www.maxmind.com/en/geolite2/signup.
- Provide the license key to the application under the configuration key
GrydAuth:GeoIp:MaxMindLicenseKey, keeping it out of source control. Use whatever configuration source your host already trusts — for local development, .NET user secrets (dotnet user-secrets set "GrydAuth:GeoIp:MaxMindLicenseKey" "<key>"); in other environments, the standard environment-variable formGrydAuth__GeoIp__MaxMindLicenseKey(double underscore) or your secret store. The framework binds this option through normal configuration. - Provision the database in one of two ways:
- Automatic (recommended): set
EnableAutomaticUpdates: true. A background service downloads GeoLite2-City on startup and refreshes it on the configured interval, with SHA256 integrity verification and hot-reload (no restart required). - Manual: download
GeoLite2-City.mmdbyourself and pointDatabasePathat it. Automatic updates can then be disabled.
- Automatic (recommended): set
License key required for automatic updates
When EnableAutomaticUpdates is true for the MaxMind provider, MaxMindLicenseKey is mandatory and the application will fail fast at startup if it is missing.
Dependency injection
Register the GeoIP services in the consuming host. This binds and validates both option sections, wires the MaxMind reader (with hot-reload and integrity verification), and — when enabled — the health check and the background updater:
csharp
// Program.cs / host composition
builder.Services.AddGrydAuthGeoIpServices(builder.Configuration);The risk decision policy (IRiskDecisionPolicy) and the Zero Trust security service that consumes these signals are registered by the Application layer as part of the standard GrydAuth registration — no extra call is needed for them.
Configuration
All keys live under the consolidated GrydAuth configuration section. A minimal appsettings.json for the MaxMind provider:
json
{
"GrydAuth": {
"GeoIp": {
"Provider": "MaxMind",
"DatabasePath": "./Data/GeoIP/GeoLite2-City.mmdb",
"MaxMindAccountId": "",
"MaxMindLicenseKey": "",
"EnableAutomaticUpdates": true,
"RequireChecksumVerification": true,
"UpdateIntervalDays": 30,
"EnableHealthCheck": true
},
"ImpossibleTravel": {
"MaxVelocityKmh": 900.0,
"TimeWindowMinutes": 60,
"MinimumDistanceKm": 10.0,
"EnforcementMode": "Enforce",
"AutoBlockCriticalRisk": true,
"MissingDatabaseBehavior": "FailClosed",
"ConsiderAccuracyRadius": true,
"MaxAccuracyRadiusKm": 200.0,
"EnablePerDeviceHistory": true,
"EvaluateConcurrentSessions": true,
"ConcurrentSessionWindowMinutes": 60,
"TrustedLocationRadiusKm": 50.0,
"AutoLearnTrustedLocations": true,
"MaxTrustedLocationsPerUser": 10,
"MaxDeviceLocationsPerUser": 20,
"NotifySecurityTeam": true
}
}
}Removed legacy key
The database path key is GrydAuth:GeoIp:DatabasePath. The old GrydAuth:GeoIp:MaxMindDatabasePath key is no longer supported — it previously bound to nothing and was silently ignored. If present, the application now fails fast at startup with a message pointing to the correct key. MaxMindAccountId is a string ("" when unused), not a number.
GrydAuth:GeoIp
Provider, database lifecycle, and supply-chain integrity.
| Key | Type | Default | Description |
|---|---|---|---|
Provider | string | MaxMind | Provider: MaxMind (local .mmdb), Azure, AWS, Google, or Custom. |
DatabasePath | string | ./Data/GeoIP/GeoLite2-City.mmdb | Path to the .mmdb database (MaxMind). Required. |
MaxMindLicenseKey | string? | null | MaxMind license key. Required when EnableAutomaticUpdates is true. Supply via user secrets or GrydAuth__GeoIp__MaxMindLicenseKey; keep out of source control. |
MaxMindAccountId | string? | null | MaxMind account ID. Only needed for paid GeoIP2 databases. |
EnableAutomaticUpdates | bool | true | Download/refresh the database via the background updater. |
UpdateIntervalDays | int (0–365) | 30 | Days between automatic updates (0 disables). GeoLite2 updates roughly monthly. |
UpdateHourUtc | int (0–23) | 3 | UTC hour to run scheduled updates. |
RequireChecksumVerification | bool | true | Verify the SHA256 checksum of a downloaded database before installing (supply-chain protection). |
ChecksumUrlTemplate | string | MaxMind .sha256 URL | Template ({0} = license key) for fetching the official checksum. |
DownloadUrlTemplate | string | MaxMind tar.gz URL | Template ({0} = license key) for the database archive. |
DownloadTimeoutSeconds | int (30–3600) | 300 | Timeout for a download operation. |
RetryCount | int (0–10) | 3 | Retry attempts (exponential backoff) for failed downloads. |
KeepDatabaseBackup | bool | true | Keep the previous database version for rollback. |
BackupCount | int (0–10) | 2 | Number of backup versions retained. |
EnableCaching | bool | true | Cache GeoIP lookup results. |
CacheExpirationMinutes | int (1–1440) | 60 | Lookup cache TTL. |
EnableHealthCheck | bool | true | Register the grydauth-geoip health check and availability gauge. |
EnableDetailedLogging | bool | false | Verbose GeoIP logging for troubleshooting. |
Cloud providers use their own keys (AzureMapsSubscriptionKey, AwsRegion / AwsAccessKeyId / AwsSecretAccessKey, GoogleApiKey, CustomProviderUrl); invalid combinations fail fast at startup.
GrydAuth:ImpossibleTravel
Detection thresholds, enforcement, uncertainty handling, and per-device history.
| Key | Type | Default | Description |
|---|---|---|---|
MaxVelocityKmh | double | 900.0 | Maximum realistic velocity (km/h). Above this, travel is impossible. |
TimeWindowMinutes | int | 60 | Time window over which consecutive logins are compared. |
MinimumDistanceKm | double | 10.0 | Distances below this are treated as the same location. |
EnforcementMode | enum | Enforce | Enforce acts on risk (block/step-up); Monitor audits only (opt-in downgrade). |
AutoBlockCriticalRisk | bool | true | Under Enforce, block critical/impossible travel. When false, require step-up instead. |
MissingDatabaseBehavior | enum | FailClosed | FailClosed requires step-up when the database is unavailable; FailOpen skips validation with an auditable warning. |
ConsiderAccuracyRadius | bool | true | Subtract the (capped) GeoIP accuracy radii from the central distance to reduce false positives. Set false for the raw central distance. |
MaxAccuracyRadiusKm | double | 200.0 | Per-reading cap on the accuracy radius, so imprecise readings cannot neutralize detection. |
EnablePerDeviceHistory | bool | true | Compare each login against the last location of the same device. |
EvaluateConcurrentSessions | bool | true | Detect a geographically incompatible active session on another device (possible hijack). |
ConcurrentSessionWindowMinutes | int | 60 | Window within which another device's location counts as concurrent for the hijack check. |
TrustedLocationRadiusKm | double | 50.0 | Radius around a trusted location within which a login is treated as trusted. |
AutoLearnTrustedLocations | bool | true | Promote a plausible, allowed login location to the user's trusted locations. |
MaxTrustedLocationsPerUser | int | 10 | Retention cap on trusted locations per user (privacy/LGPD). |
MaxDeviceLocationsPerUser | int | 20 | Retention cap on per-device location entries per user (privacy/LGPD). |
NotifySecurityTeam | bool | true | Notify the security team on high/critical risk. |
SecurityTeamEmail | string? | null | Recipient for security notifications. |
Security behavior
- Fail-secure enforcement. With
EnforcementMode: Enforce(default), critical / impossible travel is blocked whenAutoBlockCriticalRiskistrue, otherwise it requires step-up. High/medium risk requires step-up.Monitormode downgrades every action to an audited allow — an explicit, opt-in choice. - Fail-closed on missing database. With
MissingDatabaseBehavior: FailClosed(default), an unavailable database routes the request to step-up rather than skipping geographic validation. SetFailOpento skip it with an auditable warning. - Fail-fast configuration. Invalid configuration (for example, automatic updates without a license key, or the removed legacy key) aborts startup with a clear message.
- Supply-chain integrity. Downloaded databases are verified against the official SHA256 checksum before install (
RequireChecksumVerification, defaulttrue). On a mismatch or an unavailable checksum, the install is aborted and the current database is kept; the updater retries on the next window. Disabling this weakens supply-chain protection and is not recommended. - Hot-reload. A successful update swaps the active database atomically, draining in-flight lookups first — updates take effect without a restart, and a corrupt new file leaves the previous database serving traffic.
- Effective distance (uncertainty). When
ConsiderAccuracyRadiusistrue, the decision usesEffectiveDistanceKm = max(0, centralDistance − cappedRadiusPrev − cappedRadiusCurr). This cuts false positives from imprecise readings (CGNAT, VPN, mobile IP) without masking genuinely continental jumps. The response still reports the raw central distance inDistanceKm; readEffectiveDistanceKmfor the value the decision was based on. - Per-device history & trusted locations. Detection is scoped per device (send the device fingerprint on the Zero Trust request for maximum benefit; without one, a sentinel bucket is used). Alternating between legitimate devices no longer produces false impossible travel, while the same device making an impossible jump still blocks. Trusted locations reduce plausible-but-suspicious high/medium risk to an audited allow — they never rescue real impossible travel or a hijack signal.
- Concurrent-session hijack. If another of the user's devices is active in a geographically incompatible location within the concurrency window, the login is escalated as impossible travel (block/step-up per the enforcement settings).
Observability
- Health check
grydauth-geoip(tagsgeoip,grydauth) reports Unhealthy when the database is unavailable. Gated byGeoIp:EnableHealthCheck. - Metric
geoip_database_available— gauge,1when the database is loaded,0when missing. - Tracing tags:
zero_trust.geo_decision(allow|step_up|block), andDistanceKm/EffectiveDistanceKmon the analysis activity. - Reason codes on emitted security events:
impossible_travel,concurrent_session_hijack,trusted_location,geoip_database_unavailable,geo_risk_calculation_failed.
Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
| Startup fails with an options-validation error | EnableAutomaticUpdates: true without MaxMindLicenseKey, or the removed MaxMindDatabasePath key is set | Provide MaxMindLicenseKey (user secrets or GrydAuth__GeoIp__MaxMindLicenseKey) or disable automatic updates; rename the key to DatabasePath. |
| Every login requires step-up; health check is Unhealthy | Database missing/unavailable and MissingDatabaseBehavior: FailClosed | Provision GeoLite2-City.mmdb at DatabasePath (or enable automatic updates). FailOpen is available as an explicit opt-out. |
| Database never updates | Automatic updates disabled, or the checksum could not be fetched/verified | Set EnableAutomaticUpdates: true with a valid license key; check outbound access to MaxMind and the security logs — a failed checksum keeps the current database and retries. |
| Legitimate users blocked as impossible travel (VPN/NAT/mobile) | Imprecise GeoIP readings | Keep ConsiderAccuracyRadius: true; tune MaxAccuracyRadiusKm and TrustedLocationRadiusKm. Ensure the device fingerprint is sent so per-device history applies. |
| Switching devices flags impossible travel | Per-device history disabled or no device fingerprint | Keep EnablePerDeviceHistory: true and send a stable device fingerprint on the Zero Trust request. |
| Update applied but behavior unchanged | Expecting a restart | Not required — the reader hot-reloads after a verified update. Confirm the update succeeded in the logs. |