Google Workspace Impossible Travel Login

Detects successful Google Workspace sign-ins for the same user from two geographically separated locations within a 90-minute window, where the implied travel speed between the two points exceeds what is physically possible (>=800 km/h, faster than modern commercial airliners) and the geographic separation is at least 500 km. This pattern indicates either VPN/proxy use or an adversary signing in to a compromised account from a different location than the legitimate user.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2026/05/14"
  3integration = ["google_workspace"]
  4maturity = "production"
  5min_stack_comments = "ES|QL FIRST and LAST aggregation functions are GA in 9.4."
  6min_stack_version = "9.4.0"
  7updated_date = "2026/09/18"
  8
  9[rule]
 10author = ["Elastic"]
 11description = """
 12Detects successful Google Workspace sign-ins for the same user from two geographically separated locations within a
 1390-minute window, where the implied travel speed between the two points exceeds what is physically possible (>=800 km/h,
 14faster than modern commercial airliners) and the geographic separation is at least 500 km. This pattern indicates either
 15VPN/proxy use or an adversary signing in to a compromised account from a different location than the legitimate user.
 16"""
 17false_positives = [
 18    """
 19    Users on VPN or proxy egress that geo-resolves through a region distant from the user's physical location. Mobile
 20    clients on cellular carrier networks that peer through regional hubs may geo-resolve to a different region than the
 21    user's physical location.
 22    """,
 23]
 24from = "now-180m"
 25interval = "30m"
 26language = "esql"
 27license = "Elastic License v2"
 28name = "Google Workspace Impossible Travel Login"
 29note = """## Triage and analysis
 30
 31### Investigating Google Workspace Impossible Travel Login
 32
 33Google Workspace is accessible globally; legitimate users authenticate from one location at a time. Two successful sign-ins for the same user separated by a distance and time delta implying travel faster than a commercial airliner cannot be the same human being physically moving, and indicate either a VPN/proxy egress mismatch or a compromised account being accessed from a separate location by an adversary.
 34
 35### Possible investigation steps
 36
 37- Identify the user (`user.email`) and the geographic separation observed: `Esql.distance_km`, `Esql.travel_kmh`, `Esql.window_minutes` (bbox path over region centroids), and the set of distinct countries, regions, and cities (`Esql.source_geo_country_name_values`, `Esql.source_geo_region_name_values`, `Esql.source_geo_city_name_values`).
 38- Cross-check `Esql.honest_distance_km`, `Esql.honest_travel_kmh`, `Esql.honest_window_minutes` these measure the real great-circle distance between the user's actual first and last sign-in events with timestamps locked to those same events. When the honest distance is small but the bbox distance is large, the user appeared in an outlier region in the middle of the window (A->B->A pattern -- typical AiTM kit replay). When both agree, it's a clean two-region case.
 39- Pull all `google_workspace.login` events for the user across the alert window. Sort by `@timestamp` and inspect each `source.ip`, `source.as.organization.name`, `source.geo.country_name`, and `user_agent.original` (when present).
 40- Determine which sign-ins are consistent with the user's baseline (corporate VPN egress, home ISP, mobile carrier) and which are not.
 41- For each non-baseline sign-in: check the ASN. Hosting-provider ASNs (Clouvider, Host Telecom, Alibaba, cheap-VPS providers) for interactive sign-ins are high-fidelity suspicious because legitimate end users do not typically egress through those networks.
 42- Cross-reference `logs-google_workspace.token` for `event.action: authorize` events from the same `user.email` around the same time. An OAuth grant minted from a non-baseline ASN immediately after a non-baseline sign-in is the AiTM kit signature.
 43- Check `logs-google_workspace.user_accounts` for `2sv_enroll`, recovery email/phone additions, or other state changes that an attacker would make to establish persistence.
 44- Confirm with the user whether the sign-ins are theirs (VPN, travel) or unexpected.
 45
 46### False positive analysis
 47
 48- Users on VPN or proxy infrastructure egressing through a distant region: validate against the user's known VPN ranges and consider excluding by ASN.
 49- Mobile carriers that geo-resolve outside the user's home country (cellular providers often peer through regional hubs): validate by user-agent (mobile UA fingerprint) and source ASN (carrier networks).
 50
 51### Response and remediation
 52
 53- If the pattern is unexpected, suspend the user immediately, then revoke OAuth tokens (`DELETE /admin/directory/v1/users/<upn>/tokens/<clientId>`), reset password, and clear recovery email/phone.
 54- Investigate any `google_workspace.token: authorize` events fired around the same window for tokens minted to the adversary.
 55- Review `google_workspace.device` for any `DEVICE_REGISTER_UNREGISTER_EVENT` with `account_state: REGISTERED` near the same window: kit-side device registrations are a persistence vector that survives password rotation if the underlying OAuth tokens were not revoked.
 56- Cross-check `logs-gcp.audit-*` if the tenant exposes any GCP resources to the user: look for `authenticationInfo.principalEmail` matching the user from a non-baseline `callerIp`.
 57"""
 58references = [
 59    "https://www.elastic.co/security-labs/google-workspace-attack-surface-part-one",
 60    "https://www.elastic.co/security-labs/google-workspace-attack-surface-part-two",
 61    "https://security.googlecloudcommunity.com/community-blog-42/detecting-impossible-travel-with-google-secops-part-1-3892",
 62]
 63risk_score = 73
 64rule_id = "aff74d85-5bfa-4ff1-ace2-4e3995a37cfa"
 65severity = "high"
 66tags = [
 67    "Domain: Cloud",
 68    "Domain: Identity",
 69    "Data Source: Google Workspace",
 70    "Data Source: Google Workspace User Log Events",
 71    "Data Source: Google Workspace Audit Logs",
 72    "Use Case: Threat Detection",
 73    "Use Case: Identity and Access Audit",
 74    "Tactic: Initial Access",
 75    "Tactic: Credential Access",
 76    "Resources: Investigation Guide",
 77    "Noise: Medium",
 78    "Performance: Fast",
 79    "Profile: Recommended",
 80    "Threat: Impossible Travel",
 81    "Rule Type: ES|QL",
 82    "Platform: Google Workspace",
 83    "Domain: SaaS",
 84]
 85timestamp_override = "event.ingested"
 86type = "esql"
 87
 88query = '''
 89// successful Google Workspace logins with country + region populated.
 90from logs-google_workspace.login-*
 91| where event.dataset == "google_workspace.login"
 92    and event.action == "login_success"
 93    and event.outcome == "success"
 94    and user.email is not null
 95    and source.geo.location is not null
 96    and source.geo.country_name is not null
 97    and source.geo.region_name is not null
 98| eval Esql.source_geo_lat = st_y(source.geo.location),
 99       Esql.source_geo_lon = st_x(source.geo.location)
100
101// collapse each (user, country, region) into one centroid + the actual lat/lon
102// of the first and last event in that region. FIRST/LAST lock coords to the
103// timestamp ordering so we can later build the honest event pair.
104| stats
105    Esql.region_centroid_lat = avg(Esql.source_geo_lat),
106    Esql.region_centroid_lon = avg(Esql.source_geo_lon),
107    Esql.region_first_lat    = first(Esql.source_geo_lat, @timestamp),
108    Esql.region_first_lon    = first(Esql.source_geo_lon, @timestamp),
109    Esql.region_last_lat     = last(Esql.source_geo_lat,  @timestamp),
110    Esql.region_last_lon     = last(Esql.source_geo_lon,  @timestamp),
111    Esql.region_first_seen   = min(@timestamp),
112    Esql.region_last_seen    = max(@timestamp),
113    Esql.region_event_count  = count(*),
114    Esql.region_city_values   = values(source.geo.city_name),
115    Esql.region_asn_values    = values(source.`as`.organization.name),
116    Esql.region_ip_values     = values(source.ip)
117  by user.email,
118     source.geo.country_name,
119     source.geo.region_name
120
121// roll up to the user. two parallel measurements:
122// bbox: corners over region centroids.
123// honest: real coords at the user's actual first and last events (nested FIRST/LAST).
124| stats
125    Esql.min_lat = min(Esql.region_centroid_lat),
126    Esql.max_lat = max(Esql.region_centroid_lat),
127    Esql.min_lon = min(Esql.region_centroid_lon),
128    Esql.max_lon = max(Esql.region_centroid_lon),
129    Esql.honest_first_lat = first(Esql.region_first_lat, Esql.region_first_seen),
130    Esql.honest_first_lon = first(Esql.region_first_lon, Esql.region_first_seen),
131    Esql.honest_last_lat  = last(Esql.region_last_lat,   Esql.region_last_seen),
132    Esql.honest_last_lon  = last(Esql.region_last_lon,   Esql.region_last_seen),
133    Esql.timestamp_first_seen = min(Esql.region_first_seen),
134    Esql.timestamp_last_seen  = max(Esql.region_first_seen), // first arrival in last region > tighter bbox window
135    Esql.honest_last_time     = max(Esql.region_last_seen),  // user's actual last event > honest window
136    Esql.region_count  = count_distinct(source.geo.region_name),
137    Esql.country_count = count_distinct(source.geo.country_name),
138    Esql.event_count   = sum(Esql.region_event_count),
139    Esql.source_geo_country_name_values     = values(source.geo.country_name),
140    Esql.source_geo_region_name_values      = values(source.geo.region_name),
141    Esql.source_geo_city_name_values        = values(Esql.region_city_values),
142    Esql.source_as_organization_name_values = values(Esql.region_asn_values),
143    Esql.source_ip_values                   = values(Esql.region_ip_values)
144  by user.email
145
146// need at least 2 regions to have anything to compare. cap at 5 because regions
147// are finer-grained than countries (a traveling user can hit 3-4 in 90m via
148// carrier hub bouncing) > bbox drift stays bounded below this.
149| where Esql.region_count >= 2 and Esql.region_count <= 5
150
151// bbox path (primary trigger): corners over region centroids.
152| eval Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.min_lon), " ", to_string(Esql.min_lat), ")")),
153       Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.max_lon), " ", to_string(Esql.max_lat), ")"))
154| eval Esql.distance_km    = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
155       Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
156       Esql.travel_kmh     = case(Esql.window_minutes > 0,
157                                  round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null)
158
159// honest pair (triage signal): real coords at the user's actual first and last
160// events, time locked to those same two events.
161| eval Esql.honest_p1 = to_geopoint(concat("POINT(", to_string(Esql.honest_first_lon), " ", to_string(Esql.honest_first_lat), ")")),
162       Esql.honest_p2 = to_geopoint(concat("POINT(", to_string(Esql.honest_last_lon),  " ", to_string(Esql.honest_last_lat),  ")"))
163| eval Esql.honest_distance_km     = round(st_distance(Esql.honest_p1, Esql.honest_p2) / 1000.0, 0),
164       Esql.honest_window_minutes  = date_diff("minute", Esql.timestamp_first_seen, Esql.honest_last_time),
165       Esql.honest_travel_kmh      = case(Esql.honest_window_minutes > 0,
166                                          round(Esql.honest_distance_km * 60.0 / Esql.honest_window_minutes, 0), null)
167
168// 500 km separation + faster than a commercial airliner. bbox is the trigger
169// honest fields are kept purely as triage signal.
170| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
171
172| keep user.email,
173    Esql.source_geo_country_name_values,
174    Esql.source_geo_region_name_values,
175    Esql.source_geo_city_name_values,
176    Esql.source_as_organization_name_values,
177    Esql.source_ip_values,
178    Esql.country_count,
179    Esql.region_count,
180    Esql.event_count,
181    Esql.timestamp_first_seen,
182    Esql.timestamp_last_seen,
183    Esql.window_minutes,
184    Esql.distance_km,
185    Esql.travel_kmh,
186    Esql.honest_distance_km,
187    Esql.honest_travel_kmh,
188    Esql.honest_window_minutes
189'''
190
191
192[[rule.threat]]
193framework = "MITRE ATT&CK"
194[[rule.threat.technique]]
195id = "T1078"
196name = "Valid Accounts"
197reference = "https://attack.mitre.org/techniques/T1078/"
198[[rule.threat.technique.subtechnique]]
199id = "T1078.004"
200name = "Cloud Accounts"
201reference = "https://attack.mitre.org/techniques/T1078/004/"
202
203
204
205[rule.threat.tactic]
206id = "TA0001"
207name = "Initial Access"
208reference = "https://attack.mitre.org/tactics/TA0001/"
209[[rule.threat]]
210framework = "MITRE ATT&CK"
211[[rule.threat.technique]]
212id = "T1528"
213name = "Steal Application Access Token"
214reference = "https://attack.mitre.org/techniques/T1528/"
215
216[[rule.threat.technique]]
217id = "T1557"
218name = "Adversary-in-the-Middle"
219reference = "https://attack.mitre.org/techniques/T1557/"
220
221
222[rule.threat.tactic]
223id = "TA0006"
224name = "Credential Access"
225reference = "https://attack.mitre.org/tactics/TA0006/"
226
227[rule.alert_suppression]
228group_by = ["user.email"]
229missing_fields_strategy = "suppress"
230
231[rule.investigation_fields]
232field_names = ["user.email"]
233
234[rule.alert_suppression.duration]
235unit = "m"
236value = 180

Triage and analysis

Investigating Google Workspace Impossible Travel Login

Google Workspace is accessible globally; legitimate users authenticate from one location at a time. Two successful sign-ins for the same user separated by a distance and time delta implying travel faster than a commercial airliner cannot be the same human being physically moving, and indicate either a VPN/proxy egress mismatch or a compromised account being accessed from a separate location by an adversary.

Possible investigation steps

  • Identify the user (user.email) and the geographic separation observed: Esql.distance_km, Esql.travel_kmh, Esql.window_minutes (bbox path over region centroids), and the set of distinct countries, regions, and cities (Esql.source_geo_country_name_values, Esql.source_geo_region_name_values, Esql.source_geo_city_name_values).
  • Cross-check Esql.honest_distance_km, Esql.honest_travel_kmh, Esql.honest_window_minutes these measure the real great-circle distance between the user's actual first and last sign-in events with timestamps locked to those same events. When the honest distance is small but the bbox distance is large, the user appeared in an outlier region in the middle of the window (A->B->A pattern -- typical AiTM kit replay). When both agree, it's a clean two-region case.
  • Pull all google_workspace.login events for the user across the alert window. Sort by @timestamp and inspect each source.ip, source.as.organization.name, source.geo.country_name, and user_agent.original (when present).
  • Determine which sign-ins are consistent with the user's baseline (corporate VPN egress, home ISP, mobile carrier) and which are not.
  • For each non-baseline sign-in: check the ASN. Hosting-provider ASNs (Clouvider, Host Telecom, Alibaba, cheap-VPS providers) for interactive sign-ins are high-fidelity suspicious because legitimate end users do not typically egress through those networks.
  • Cross-reference logs-google_workspace.token for event.action: authorize events from the same user.email around the same time. An OAuth grant minted from a non-baseline ASN immediately after a non-baseline sign-in is the AiTM kit signature.
  • Check logs-google_workspace.user_accounts for 2sv_enroll, recovery email/phone additions, or other state changes that an attacker would make to establish persistence.
  • Confirm with the user whether the sign-ins are theirs (VPN, travel) or unexpected.

False positive analysis

  • Users on VPN or proxy infrastructure egressing through a distant region: validate against the user's known VPN ranges and consider excluding by ASN.
  • Mobile carriers that geo-resolve outside the user's home country (cellular providers often peer through regional hubs): validate by user-agent (mobile UA fingerprint) and source ASN (carrier networks).

Response and remediation

  • If the pattern is unexpected, suspend the user immediately, then revoke OAuth tokens (DELETE /admin/directory/v1/users/<upn>/tokens/<clientId>), reset password, and clear recovery email/phone.
  • Investigate any google_workspace.token: authorize events fired around the same window for tokens minted to the adversary.
  • Review google_workspace.device for any DEVICE_REGISTER_UNREGISTER_EVENT with account_state: REGISTERED near the same window: kit-side device registrations are a persistence vector that survives password rotation if the underlying OAuth tokens were not revoked.
  • Cross-check logs-gcp.audit-* if the tenant exposes any GCP resources to the user: look for authenticationInfo.principalEmail matching the user from a non-baseline callerIp.

References

Related rules

to-top