Add audit event for a PAT used from an unseen IP address
What does this MR do and why?
When a personal access token is used from an IP address not present in its
last_used_ips history, a personal_access_token_used_from_unseen_ip audit
event is recorded. This gives security teams visibility into PAT usage from
new locations.
- Reuses the existing IP-novelty detection in
PersonalAccessTokens::LastUsedService(which maintains a rolling window of the last 5 IPs per token) - Fires the event only on subsequent uses — the first use of a token (when
last_used_atisnil) does not emit an event, as there is no baseline - Extracts a shared
ip_already_recorded?memoized helper to avoid duplicate DB queries betweenunseen_ip?andlast_used_ip_needs_update? - The audit event message includes the unseen IP address for actionability
- Available in CE;
authentication_event: trueevents are persisted to the database and written toaudit_json.login all tiers - Also covers Group Access Tokens and Project Access Tokens since they
are
PersonalAccessTokenrecords (owned byproject_botusers) and flow through the sameLastUsedService - Gated behind the
audit_event_pat_unseen_ipfeature flag (default disabled) to allow gradual rollout — can be enabled per-user, per-group, or by percentage
Feature flag
- Name:
audit_event_pat_unseen_ip - Type:
development - Default: disabled
- Scoped to: user (supports per-user, percentage-of-actors rollout)
To enable for a specific user:
Feature.enable(:audit_event_pat_unseen_ip, User.find_by_username('username'))To enable for all users:
Feature.enable(:audit_event_pat_unseen_ip)Implementation notes
unseen_ip?is evaluated before thewithout_sticky_writesblock so the DB query reads the pre-insert state oflast_used_ipslog_audit_event_for_unseen_ipis called after thewithout_sticky_writesblock so theAuditEventwrite is not suppressedorganization:is passed from@personal_access_token.organization(always present), avoiding theNOT NULLconstraint onauthentication_events.organization_id
Local GDK tests & Audit event examples
Establishing baseline IP over REST API
curl -H "PRIVATE-TOKEN: $TOKEN" http://gdk.test:3000/api/v4/personal_access_tokens/self{
"id": 25,
"name": "another-token",
"last_used_at": "2026-04-29T01:48:27.956Z",
"last_used_ips": [
"::1"
]
}Triggering unseen IP via X-Forwarded-For
curl -H "PRIVATE-TOKEN: $TOKEN" -H "X-Forwarded-For: 172.16.0.99" http://gdk.test:3000/api/v4/personal_access_tokens/self{
"id": 25,
"name": "another-token",
"last_used_at": "2026-04-29T01:48:27.956Z",
"last_used_ips": [
"::1",
"172.16.0.99"
]
}Audit log entry (audit_json.log)
{
"severity": "INFO",
"time": "2026-04-29T01:48:28.149Z",
"correlation_id": "01KQBERPCZ76G2ZEPE3D22Z06R",
"meta.caller_id": "GET /api/:version/personal_access_tokens/self",
"meta.remote_ip": "172.16.0.99",
"meta.feature_category": "system_access",
"id": 698,
"author_id": 1,
"entity_id": 1,
"entity_type": "User",
"details": {
"pat_id": 25,
"pat_name": "another-token",
"event_name": "personal_access_token_used_from_unseen_ip",
"author_name": "Administrator",
"author_class": "User",
"target_id": 1,
"target_type": "User",
"target_details": "Administrator",
"custom_message": "Personal access token was used from a previously unseen IP address: 172.16.0.99",
"ip_address": "172.16.0.99",
"entity_path": "root"
},
"ip_address": "172.16.0.99",
"author_name": "Administrator",
"entity_path": "root",
"target_details": "Administrator",
"created_at": "2026-04-29T01:48:28.106Z",
"target_type": "User",
"target_id": 1,
"event_name": "personal_access_token_used_from_unseen_ip",
"custom_message": "Personal access token was used from a previously unseen IP address: 172.16.0.99"
}How to validate locally
-
Enable the feature flag:
Feature.enable(:audit_event_pat_unseen_ip) -
Create a PAT and use it once to establish a baseline IP:
curl -H "PRIVATE-TOKEN: $TOKEN" http://gdk.test:3000/api/v4/personal_access_tokens/self -
Use the PAT from a different IP:
curl -H "PRIVATE-TOKEN: $TOKEN" -H "X-Forwarded-For: 172.16.0.99" http://gdk.test:3000/api/v4/personal_access_tokens/self -
Check audit log:
grep unseen_ip log/audit_json.log
References
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
- Tests added (27 examples, 0 failures)
- Documentation updated (
audit_event_types.md) - Audit event type YAML added
-
introduced_by_mrfield updated with this MR's URL - GDK end-to-end validation (curl + audit_json.log)
- Feature flag added (
audit_event_pat_unseen_ip, default disabled)
Edited by Neil McDonald