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/05/14"
  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 Audit Logs",
 71    "Data Source: Google Workspace User log events",
 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]
 78timestamp_override = "event.ingested"
 79type = "esql"
 80
 81query = '''
 82// successful Google Workspace logins with country + region populated.
 83from logs-google_workspace.login-*
 84| where event.dataset == "google_workspace.login"
 85    and event.action == "login_success"
 86    and event.outcome == "success"
 87    and user.email is not null
 88    and source.geo.location is not null
 89    and source.geo.country_name is not null
 90    and source.geo.region_name is not null
 91| eval Esql.source_geo_lat = st_y(source.geo.location),
 92       Esql.source_geo_lon = st_x(source.geo.location)
 93
 94// collapse each (user, country, region) into one centroid + the actual lat/lon
 95// of the first and last event in that region. FIRST/LAST lock coords to the
 96// timestamp ordering so we can later build the honest event pair.
 97| stats
 98    Esql.region_centroid_lat = avg(Esql.source_geo_lat),
 99    Esql.region_centroid_lon = avg(Esql.source_geo_lon),
100    Esql.region_first_lat    = first(Esql.source_geo_lat, @timestamp),
101    Esql.region_first_lon    = first(Esql.source_geo_lon, @timestamp),
102    Esql.region_last_lat     = last(Esql.source_geo_lat,  @timestamp),
103    Esql.region_last_lon     = last(Esql.source_geo_lon,  @timestamp),
104    Esql.region_first_seen   = min(@timestamp),
105    Esql.region_last_seen    = max(@timestamp),
106    Esql.region_event_count  = count(*),
107    Esql.region_city_values   = values(source.geo.city_name),
108    Esql.region_asn_values    = values(source.`as`.organization.name),
109    Esql.region_ip_values     = values(source.ip)
110  by user.email,
111     source.geo.country_name,
112     source.geo.region_name
113
114// roll up to the user. two parallel measurements:
115// bbox: corners over region centroids.
116// honest: real coords at the user's actual first and last events (nested FIRST/LAST).
117| stats
118    Esql.min_lat = min(Esql.region_centroid_lat),
119    Esql.max_lat = max(Esql.region_centroid_lat),
120    Esql.min_lon = min(Esql.region_centroid_lon),
121    Esql.max_lon = max(Esql.region_centroid_lon),
122    Esql.honest_first_lat = first(Esql.region_first_lat, Esql.region_first_seen),
123    Esql.honest_first_lon = first(Esql.region_first_lon, Esql.region_first_seen),
124    Esql.honest_last_lat  = last(Esql.region_last_lat,   Esql.region_last_seen),
125    Esql.honest_last_lon  = last(Esql.region_last_lon,   Esql.region_last_seen),
126    Esql.timestamp_first_seen = min(Esql.region_first_seen),
127    Esql.timestamp_last_seen  = max(Esql.region_first_seen), // first arrival in last region > tighter bbox window
128    Esql.honest_last_time     = max(Esql.region_last_seen),  // user's actual last event > honest window
129    Esql.region_count  = count_distinct(source.geo.region_name),
130    Esql.country_count = count_distinct(source.geo.country_name),
131    Esql.event_count   = sum(Esql.region_event_count),
132    Esql.source_geo_country_name_values     = values(source.geo.country_name),
133    Esql.source_geo_region_name_values      = values(source.geo.region_name),
134    Esql.source_geo_city_name_values        = values(Esql.region_city_values),
135    Esql.source_as_organization_name_values = values(Esql.region_asn_values),
136    Esql.source_ip_values                   = values(Esql.region_ip_values)
137  by user.email
138
139// need at least 2 regions to have anything to compare. cap at 5 because regions
140// are finer-grained than countries (a traveling user can hit 3-4 in 90m via
141// carrier hub bouncing) > bbox drift stays bounded below this.
142| where Esql.region_count >= 2 and Esql.region_count <= 5
143
144// bbox path (primary trigger): corners over region centroids.
145| eval Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.min_lon), " ", to_string(Esql.min_lat), ")")),
146       Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.max_lon), " ", to_string(Esql.max_lat), ")"))
147| eval Esql.distance_km    = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
148       Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
149       Esql.travel_kmh     = case(Esql.window_minutes > 0,
150                                  round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null)
151
152// honest pair (triage signal): real coords at the user's actual first and last
153// events, time locked to those same two events.
154| eval Esql.honest_p1 = to_geopoint(concat("POINT(", to_string(Esql.honest_first_lon), " ", to_string(Esql.honest_first_lat), ")")),
155       Esql.honest_p2 = to_geopoint(concat("POINT(", to_string(Esql.honest_last_lon),  " ", to_string(Esql.honest_last_lat),  ")"))
156| eval Esql.honest_distance_km     = round(st_distance(Esql.honest_p1, Esql.honest_p2) / 1000.0, 0),
157       Esql.honest_window_minutes  = date_diff("minute", Esql.timestamp_first_seen, Esql.honest_last_time),
158       Esql.honest_travel_kmh      = case(Esql.honest_window_minutes > 0,
159                                          round(Esql.honest_distance_km * 60.0 / Esql.honest_window_minutes, 0), null)
160
161// 500 km separation + faster than a commercial airliner. bbox is the trigger
162// honest fields are kept purely as triage signal.
163| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
164
165| keep user.email,
166    Esql.source_geo_country_name_values,
167    Esql.source_geo_region_name_values,
168    Esql.source_geo_city_name_values,
169    Esql.source_as_organization_name_values,
170    Esql.source_ip_values,
171    Esql.country_count,
172    Esql.region_count,
173    Esql.event_count,
174    Esql.timestamp_first_seen,
175    Esql.timestamp_last_seen,
176    Esql.window_minutes,
177    Esql.distance_km,
178    Esql.travel_kmh,
179    Esql.honest_distance_km,
180    Esql.honest_travel_kmh,
181    Esql.honest_window_minutes
182'''
183
184
185[[rule.threat]]
186framework = "MITRE ATT&CK"
187[[rule.threat.technique]]
188id = "T1078"
189name = "Valid Accounts"
190reference = "https://attack.mitre.org/techniques/T1078/"
191[[rule.threat.technique.subtechnique]]
192id = "T1078.004"
193name = "Cloud Accounts"
194reference = "https://attack.mitre.org/techniques/T1078/004/"
195
196
197
198[rule.threat.tactic]
199id = "TA0001"
200name = "Initial Access"
201reference = "https://attack.mitre.org/tactics/TA0001/"
202[[rule.threat]]
203framework = "MITRE ATT&CK"
204[[rule.threat.technique]]
205id = "T1528"
206name = "Steal Application Access Token"
207reference = "https://attack.mitre.org/techniques/T1528/"
208
209[[rule.threat.technique]]
210id = "T1557"
211name = "Adversary-in-the-Middle"
212reference = "https://attack.mitre.org/techniques/T1557/"
213
214
215[rule.threat.tactic]
216id = "TA0006"
217name = "Credential Access"
218reference = "https://attack.mitre.org/tactics/TA0006/"
219
220[rule.alert_suppression]
221group_by = ["user.email"]
222missing_fields_strategy = "suppress"
223
224[rule.investigation_fields]
225field_names = ["user.email"]
226
227[rule.alert_suppression.duration]
228unit = "m"
229value = 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