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_minutesthese 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.loginevents for the user across the alert window. Sort by@timestampand inspect eachsource.ip,source.as.organization.name,source.geo.country_name, anduser_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.tokenforevent.action: authorizeevents from the sameuser.emailaround 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_accountsfor2sv_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: authorizeevents fired around the same window for tokens minted to the adversary. - Review
google_workspace.devicefor anyDEVICE_REGISTER_UNREGISTER_EVENTwithaccount_state: REGISTEREDnear 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 forauthenticationInfo.principalEmailmatching the user from a non-baselinecallerIp.
References
Related rules
- Google Workspace Device Registration Burst for Single User
- Google Workspace User Login with Unusual ASN
- Google Workspace User Sign-in from Atypical Device Type
- Microsoft Entra ID Impossible Travel Sign-in
- Entra ID Illicit Consent Grant via Registered Application