Auditing local user accounts confirms who can authenticate on a system and highlights unexpected privilege changes during security reviews or incident response. Extra logins, unusual shells, or additional UID 0 entries are common red flags worth investigating.
Local account identity data is stored in /etc/passwd and /etc/group, while password hashes and lock state are stored in /etc/shadow. Administrative access is commonly granted through the sudo or wheel groups, or via explicit rules in /etc/sudoers and /etc/sudoers.d.
Regular users typically fall within the UID_MIN to UID_MAX range from /etc/login.defs, but the exact values depend on distribution and policy. Reading /etc/shadow and sudoers files requires root access, and copying those files into tickets or chat logs can leak password hashes or privilege mappings.
Related: How to investigate a Linux intrusion
Related: How to list logged-in users in Linux
$ awk '/^UID_MIN|^UID_MAX/ {print}' /etc/login.defs
UID_MIN 1000
UID_MAX 60000
$ awk -F: '$3 >= 1000 && $3 <= 60000 {printf "%s:%s:%s:%s\n", $1, $3, $6, $7}' /etc/passwd
ubuntu:1000:/home/ubuntu:/bin/bash
user:1001:/home/user:/bin/bash
backupuser:1002:/home/backupuser:/bin/bash
audituser:1003:/home/audituser:/bin/bash
Replace 1000 and 60000 with the values from the previous step when they differ.
$ sudo ls -ld /home/* drwxr-x--- 2 audituser audituser 4096 Jan 13 00:44 /home/audituser drwxr-x--- 2 backupuser backupuser 4096 Jan 13 00:43 /home/backupuser drwxr-x--- 2 ubuntu ubuntu 4096 Oct 13 14:12 /home/ubuntu drwxr-x--- 2 user user 4096 Jan 13 00:45 /home/user
World-writable home directories can allow other local users to drop scripts, keys, or config files for persistence.
$ awk -F: '($3 < 1000 || $3 > 60000) && ($7 !~ /(nologin|false)$/) {printf "%s:%s:%s:%s\n", $1, $3, $6, $7}' /etc/passwd
root:0:/root:/bin/bash
sync:4:/bin:/bin/sync
Service accounts typically use /usr/sbin/nologin or /bin/false to block interactive logins.
$ awk -F: '($3 == 0) {printf "%s:%s:%s\n", $1, $6, $7}' /etc/passwd
root:/root:/bin/bash
Any account other than root with UID 0 has full root privileges and should be treated as a high-severity finding.
$ getent group sudo sudo:x:27:ubuntu,user
No output commonly indicates the sudo group is not used on this system.
$ getent group wheel
wheel is commonly used on RHEL and CentOS.
$ sudo grep --recursive --line-number --no-messages --extended-regexp '^[[:space:]]*[^#[:space:]]' /etc/sudoers /etc/sudoers.d 2>/dev/null /etc/sudoers:9:Defaults env_reset /etc/sudoers:10:Defaults mail_badpass /etc/sudoers:11:Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" /etc/sudoers:15:Defaults use_pty /etc/sudoers:47:root ALL=(ALL:ALL) ALL /etc/sudoers:50:%admin ALL=(ALL) ALL /etc/sudoers:53:%sudo ALL=(ALL:ALL) ALL /etc/sudoers:57:@includedir /etc/sudoers.d /etc/sudoers.d/90-user-nopasswd:1:user ALL=(ALL) NOPASSWD: ALL
Unexpected user entries or NOPASSWD rules can grant passwordless administrative access.
$ sudo awk -F: '($2 == "") {print $1}' /etc/shadow
No output indicates no empty password fields were found.
An empty password field can allow passwordless authentication depending on PAM configuration and login method.
$ sudo passwd --status --all | head root L 2025-10-13 0 99999 7 -1 daemon L 2025-10-13 0 99999 7 -1 bin L 2025-10-13 0 99999 7 -1 sys L 2025-10-13 0 99999 7 -1 sync L 2025-10-13 0 99999 7 -1 games L 2025-10-13 0 99999 7 -1 man L 2025-10-13 0 99999 7 -1 lp L 2025-10-13 0 99999 7 -1 mail L 2025-10-13 0 99999 7 -1 news L 2025-10-13 0 99999 7 -1
Common status codes include P (password set), L (locked), and NP (no password).
$ sudo chage --list --iso8601 user Last password change : 2026-01-13 Password expires : never Password inactive : never Account expires : never Minimum number of days between password change : 0 Maximum number of days between password change : 99999 Number of days of warning before password expires : 7
Replace user with the target account name.
$ lastlog -u user Username Port From Latest user **Never logged in**
Accounts showing Never logged in can still be valid, but unexpected entries are a useful triage signal.