doc/user/profile/active_sessions.md
{{< details >}}
{{< /details >}}
GitLab lists all devices that have logged into your account. You can review the sessions, and revoke any you don't recognize.
To list all active sessions:
GitLab allows users to have up to 100 active sessions at once. If the number of active sessions exceeds 100, the oldest ones are deleted.
To revoke an active session:
[!note] When any session is revoked all Remember me tokens for all devices are revoked. For details about Remember me, see cookies used for sign-in.
You can also revoke user sessions through the Rails console. You can use this to revoke multiple sessions at the same time.
To revoke all sessions for all users:
Optional. List all active sessions with the following command:
# Show all users with active sessions
puts "=== Currently Logged In Users ==="
User.find_each do |user|
sessions = ActiveSession.list(user)
if sessions.any?
puts "\n#{user.username} (#{user.name}):"
sessions.each do |session|
puts " - IP: #{session.ip_address}, Browser: #{session.browser}, Last active: #{session.updated_at}"
end
end
end
Revoke all sessions with the following command:
User.find_each do |user|
ActiveSession.destroy_all_but_current(user, nil)
end
Optional. Confirm all sessions have been revoked by running the "List all active sessions" command again.
Save the following script to your GitLab instance. For example, scripts/session_revocation/revoke_group_sessions.rb.
# frozen_string_literal: true
#
# Revoke all active sessions for members of a group, including:
# - Direct and inherited members of the top-level group
# - Direct members of all subgroups
# - Members invited via group shares at the top-level and subgroup level
#
# Usage (Rails console):
# DRY_RUN = true
# GROUP_IDENTIFIER = 'your-group-path' # or numeric ID
# load 'scripts/session_revocation/revoke_group_sessions.rb'
DRY_RUN = true unless defined?(DRY_RUN)
# Replace `your-group-path` with your group ID or the full path to your group
GROUP_IDENTIFIER = 'your-group-path' unless defined?(GROUP_IDENTIFIER)
# ---------------------------------------------------------------
def find_group(identifier)
# Try finding by full path first (handles numeric group names)
group = Group.find_by_full_path(identifier.to_s)
return group if group
# Fallback to ID lookup if path not found and identifier is numeric
if identifier.is_a?(Integer) || identifier.to_s.match?(/\A\d+\z/)
Group.find_by(id: identifier)
end
end
def collect_member_user_ids(group)
user_ids = Set.new
# Direct and inherited members of the top-level group
user_ids.merge(group.members_with_parents.pluck(:user_id))
# Members invited via group shares into the top-level group
group.shared_with_group_links.each do |link|
user_ids.merge(link.shared_with_group.members_with_parents.pluck(:user_id))
end
# Traverse all subgroups
group.descendants.find_each do |subgroup|
# Direct members of each subgroup
user_ids.merge(subgroup.members.pluck(:user_id))
# Members invited via group shares into each subgroup
subgroup.shared_with_group_links.each do |link|
user_ids.merge(link.shared_with_group.members_with_parents.pluck(:user_id))
end
end
user_ids.to_a
end
def revoke_sessions_for_group(group, dry_run:)
member_user_ids = collect_member_user_ids(group)
puts "Found #{member_user_ids.count} unique members in group '#{group.full_path}' (including subgroups and group shares)"
# Only process active, non-bot human users to avoid unnecessary Redis lookups
users = User.active.human.id_in(member_user_ids)
revoked_sessions = 0
affected_users = []
skipped_users = []
users.find_each do |user|
sessions = ActiveSession.list(user)
if sessions.empty?
skipped_users << user.username
next
end
session_ids = sessions.map(&:session_private_id).compact
if session_ids.empty?
puts " [WARN] User #{user.username} has sessions but all session_private_ids are nil, skipping."
skipped_users << user.username
next
end
unless dry_run
Gitlab::Redis::Sessions.with do |redis|
ActiveSession.destroy_sessions(redis, user, session_ids)
end
# Emit audit event for security traceability
Gitlab::AppLogger.info(
message: "Sessions revoked via admin script",
user_id: user.id,
username: user.username,
session_count: session_ids.size,
group: group.full_path,
performed_at: Time.current.iso8601
)
end
revoked_sessions += session_ids.size
affected_users << user.username
end
[revoked_sessions, affected_users, skipped_users]
end
# ---------------------------------------------------------------
group = find_group(GROUP_IDENTIFIER)
if group.nil?
puts "ERROR: Group '#{GROUP_IDENTIFIER}' not found. Aborting."
end
puts "=== Session Revocation #{DRY_RUN ? '(DRY RUN)' : '(LIVE)'} ==="
puts "Group: #{group.full_path} (ID: #{group.id})"
puts
revoked_sessions, affected_users, skipped_users = revoke_sessions_for_group(group, dry_run: DRY_RUN)
prefix = DRY_RUN ? "[DRY RUN] Would revoke" : "Revoked"
puts "#{prefix} #{revoked_sessions} sessions for #{affected_users.size} users"
puts "Users affected: #{affected_users.sort.join(', ')}" if affected_users.any?
puts "Users skipped (no active sessions): #{skipped_users.size}" if skipped_users.any?
if DRY_RUN && revoked_sessions.positive?
puts "\nTo actually revoke sessions, set DRY_RUN = false and run again."
end
Run the following command to target the group. Replace your-group-path with your group ID or the full path to your group:
GROUP_IDENTIFIER = 'your-group-path'
Run the following command to list all active sessions in the group:
DRY_RUN = true
load 'scripts/session_revocation/revoke_group_sessions.rb'
Run the following command to revoke all sessions in the group:
DRY_RUN = false
load 'scripts/session_revocation/revoke_group_sessions.rb'
Run the following command to verify all sessions are closed. The output should list 0 active sessions:
DRY_RUN = true
load 'scripts/session_revocation/revoke_group_sessions.rb'
To revoke all sessions for a specific user:
Find the user with the following commands:
By username:
user = User.find_by_username 'exampleuser'
By user ID:
user = User.find(123)
By email address:
user = User.find_by(email: '[email protected]')
Optional. List all active sessions for the user with the following command:
ActiveSession.list(user)
Revoke all sessions with the following command:
ActiveSession.list(user).each { |session| ActiveSession.destroy_session(user, session.session_private_id) }
Verify all sessions are closed with the following command:
# If all sessions are closed, returns an empty array.
ActiveSession.list(user)