Linux is one of the most widely deployed operating systems in enterprise environments, cloud infrastructure, and critical systems worldwide. Its reputation for stability and security is well earned, but that reputation does not come automatically. It requires deliberate, ongoing attention to how users are created, what permissions they hold, and how those permissions evolve over time.

User permission auditing is the process of systematically reviewing who has access to what on a Linux system, confirming that access aligns with business need and least-privilege principles, and identifying configurations that introduce unnecessary risk. Many breaches in Linux environments are not the result of novel exploits. They are the result of misconfigured permissions, forgotten accounts, overly permissive sudo rules, or files with world-writable attributes sitting in sensitive directories.

This article walks through a comprehensive methodology for auditing Linux user permissions, covering the tools available, the specific checks to run, what to look for, and how to document and remediate what you find.

Understanding the Linux Permission Model

Before running any audit commands, it helps to have a solid mental model of how Linux handles permissions.

Every file and directory in Linux has three permission sets: owner, group, and others. Each set can carry read (r), write (w), and execute (x) permissions. These are expressed numerically in octal notation, where 7 means full permissions (rwx), 6 means read and write (rw-), and 5 means read and execute (r-x).

Beyond the standard permission triad, Linux uses three special permission bits that are critical to understand during an audit.

The setuid (SUID) bit, when set on an executable, causes it to run with the privileges of the file's owner rather than the user executing it. If a root-owned binary has SUID set, any user who runs it effectively runs it as root. This is by design for utilities like passwd, but SUID binaries that are writable or poorly coded create significant attack surface.

The setgid (SGID) bit, when applied to a file, causes it to run with the group's privileges. When applied to a directory, files created inside inherit the group of the directory rather than the creator's primary group. This is useful for collaborative directories but can create unexpected permission inheritance.

The sticky bit, when set on a directory, prevents users from deleting or renaming files they do not own, even if the directory is world-writable. The /tmp directory is the classic example.

One additional layer that standard ls -la output does not reveal is POSIX Access Control Lists (ACLs). ACLs extend the basic permission model, allowing fine-grained access grants to individual users or groups beyond the owner/group/others triad. A file can appear to have locked-down permissions while an ACL silently grants write access to an unexpected account. Any thorough audit must check for ACLs on sensitive paths.

Note

Understanding these mechanisms is essential for interpreting what you find during an audit. Each special bit and ACL entry changes the security posture of a file or directory in ways that standard permission output alone does not convey. The presence of a + symbol at the end of a permission string in ls -la output indicates an ACL is present.

Step 1: Enumerate All User Accounts

The first step in any Linux user audit is simply knowing who exists on the system. The /etc/passwd file holds this information, with one entry per account. Each line contains the username, password placeholder, UID, GID, comment field, home directory, and default shell.

$ cat /etc/passwd

To isolate human-interactive accounts from system accounts, filter by UID. On many modern Linux distributions, UIDs below 1000 are reserved for system accounts, while UIDs 1000 and above represent human users. However, this threshold varies by distribution -- on some older Red Hat-based systems the threshold is 500 -- so check /etc/login.defs for the UID_MIN value to be certain.

terminal
# List human user accounts (UID >= 1000)
$ awk -F: '$3 >= 1000 {print $1, $3, $7}' /etc/passwd # Cross-reference with UID_MIN from login.defs
$ grep UID_MIN /etc/login.defs

Pay attention to the shell field (the seventh column). Accounts with /bin/bash, /bin/sh, or any valid shell can log in interactively. Service accounts should typically have /sbin/nologin or /bin/false to prevent interactive login. Finding a service account with a real shell is a red flag unless it is explicitly documented.

Also look for accounts with UID 0. There should be exactly one: root. Any additional account with UID 0 has full root access regardless of the username.

terminal
# Find all accounts with UID 0 (root-level access)
$ awk -F: '$3 == 0 {print $1}' /etc/passwd
Critical Finding

Any account with UID 0 other than root is an immediate critical finding. This grants full superuser access regardless of the account name and may indicate a backdoor or a misconfigured provisioning script.

Beyond who exists on the system, audit when accounts last logged in. Stale accounts that have not been used in months or years are a significant risk -- they represent dormant credentials that may have been forgotten during offboarding or left over from contractor engagements.

terminal
# Show last login time for all accounts
$ lastlog # Show recent login history
$ last -n 50 # Show failed login attempts
$ lastb -n 50

Accounts showing "Never logged in" alongside an active shell and no documented business purpose should be investigated and disabled. On systems with last data missing or truncated, check /var/log/auth.log (Debian-based) or /var/log/secure (Red Hat-based) for authentication history.

Step 2: Identify Privileged Groups and Membership

Group membership is how Linux grants access to shared resources and elevated permissions. The /etc/group file lists all groups, their GIDs, and their members.

Several groups warrant immediate scrutiny. The sudo group on Debian-based systems, and the wheel group on Red Hat-based systems, grant members the ability to run commands as root through sudo. Every member of these groups should be explicitly authorized.

terminal
# Check privileged group membership
$ getent group sudo
$ getent group wheel

The adm group grants read access to system log files. The shadow group grants read access to /etc/shadow, which contains hashed passwords. The disk group grants direct access to block devices, which can be used to bypass filesystem permissions entirely. The lxd and docker groups are also high privilege -- membership in either is effectively equivalent to root because containers can be used to mount the host filesystem. Membership in any of these groups should be treated as a high-privilege finding.

Look for users who belong to an unusually large number of groups, as this may indicate accumulated permissions over time that were never reviewed.

terminal
# List all group memberships for human users
$ for user in $(awk -F: '$3 >= 1000 {print $1}' /etc/passwd); do groups=$(id "$user" | grep -oP '(?<=groups=).*') echo "$user: $groups" done

Step 3: Audit the sudo Configuration

The sudoers file at /etc/sudoers, along with any files in /etc/sudoers.d/, controls who can run what commands as which users. Misconfigurations here are among the commonly exploited privilege escalation paths.

Warning

Never edit the sudoers file directly. Use visudo to review and modify it. Syntax errors in this file can lock you out of sudo access entirely. The same applies to files in /etc/sudoers.d/ -- use visudo -f /etc/sudoers.d/filename for those.

terminal
# Review sudoers configuration
$ sudo cat /etc/sudoers
$ sudo ls -la /etc/sudoers.d/
$ sudo cat /etc/sudoers.d/*

Several patterns indicate security concerns. A rule granting ALL=(ALL) ALL or ALL=(ALL:ALL) ALL gives the user unrestricted root access. This may be appropriate for systems administrators, but it should be documented and minimized.

The NOPASSWD tag is particularly dangerous. A rule like username ALL=(ALL) NOPASSWD: ALL allows the user to become root without entering a password, which means any process running as that user -- including malware -- can silently escalate to root.

Wildcards in command specifications are easy to overlook but frequently exploitable. A rule allowing sudo /usr/bin/vi /etc/config* sounds restrictive but permits opening /etc/sudoers if the glob matches, and allows shell escapes from within vi regardless of the target file. Similarly, sudo /usr/bin/find /var/log * becomes dangerous once you know that find supports -exec.

Look for entries that allow access to shells, interpreters, text editors, file management tools, or scripting languages. GTFOBins (gtfobins.github.io) maintains a comprehensive reference of binaries that can be leveraged to escape restrictions even when granted seemingly limited sudo access.

Modern sudo audit additions (high-signal checks)

In addition to visually reviewing the sudoers files, validate the effective sudo permissions that matter at runtime. This catches surprises introduced by group-based rules, include ordering, and defaults.

terminal
# Validate sudoers syntax (safe read-only check)
$ sudo visudo -c # Show effective sudo privileges for a user (what they can actually run)
$ sudo -l -U username # Show sudo version and compile-time options (useful for incident response)
$ sudo -V

Pay particular attention to risky Defaults settings and patterns in /etc/sudoers and /etc/sudoers.d/* such as disabling authentication (!authenticate), keeping dangerous environment variables (env_keep), allowing arbitrary editors, or allowing SETENV. Also consider enabling or verifying Defaults use_pty and I/O logging (log_input/log_output) where appropriate, to improve attribution and deter abuse.

Step 4: Check Password Policy and Account Security

The /etc/shadow file stores password hashes and aging information. Review it for accounts with no password, accounts that have never had their passwords changed, and accounts with passwords that have not expired in a very long time.

$ sudo awk -F: '($2=="" || $2=="!" || $2=="*") {print $1 ":" $2}' /etc/shadow
Operational Note

Avoid copying or exporting full /etc/shadow contents into tickets or reports. For most audits, you only need to identify conditions such as empty password fields, locked accounts, and password aging status—not capture the hashes themselves. If you must preserve evidence, restrict it to minimal indicators (username + condition) and store it in an approved, access-controlled location.

The second field in each entry is the password hash. An entry of ! or * means the account is locked and cannot authenticate with a password. A blank field means no password is required, which is a critical finding. An entry beginning with ! followed by a hash (e.g., !$6$...) indicates a locked account that previously had a password set -- the account is disabled but the hash is preserved.

Password aging is configured in /etc/login.defs and per-account via chage. To review aging settings for a specific user:

terminal
# Review password aging for a specific user
$ sudo chage -l username # Scan all human users for never-expiring passwords
$ for user in $(awk -F: '$3 >= 1000 {print $1}' /etc/passwd); do exp=$(sudo chage -l "$user" 2>/dev/null | grep "Password expires" | cut -d: -f2) echo "$user: Password expires$exp" done # Check system-wide password policy defaults
$ grep -E "PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_WARN_AGE" /etc/login.defs

Look for accounts where the maximum password age is set to -1 (never expires), the account has no expiration date, and the password was last changed an unreasonably long time ago. In regulated environments, password aging policies must align with compliance requirements.

Also check the system's default umask setting in /etc/profile, /etc/bashrc, and /etc/login.defs. The umask determines the default permissions of newly created files. A umask of 022 creates files as 644 and directories as 755. A more restrictive umask of 027 creates files as 640 and directories as 750, preventing other users from reading newly created files by default.

Step 5: Find SUID and SGID Binaries

Finding all files with SUID or SGID set is a critical step in any Linux permission audit. The following command searches the entire filesystem, skipping mounted filesystems like /proc and /sys:

terminal
# Find all SUID and SGID binaries
$ find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -ls 2>/dev/null

Compare the output against a known-good baseline. On a minimal, freshly installed system, expected SUID binaries typically include passwd, su, sudo, ping, mount, umount, and newgrp. Any SUID binary that is not part of the standard OS package set, or that has been modified recently, deserves investigation. On systems using Polkit (PolicyKit), pkexec is another expected SUID binary -- but it has a history of critical vulnerabilities (CVE-2021-4034 is a prominent example) and its presence should prompt a version check.

Check whether any SUID binaries are writable by non-root users:

terminal
# Find writable SUID binaries -- critical finding
$ find / -xdev -perm -4000 -writable 2>/dev/null
Critical Finding

A writable SUID binary is an immediate critical finding. An attacker who can replace or modify the binary can execute arbitrary code as root. Treat any result here as requiring immediate remediation before proceeding with the rest of the audit.

Step 5b: Audit Linux File Capabilities (Modern Privilege Boundary)

Modern Linux systems increasingly rely on capabilities to grant specific privileged operations without giving a binary full root privileges. Capabilities are stored as extended attributes (xattrs) on executables. From an attacker’s perspective, a risky capability on an interpreter or utility can be as valuable as an SUID bit, and it often receives less scrutiny during audits.

Enumerate capabilities across the filesystem and review any result that is not expected for your baseline:

terminal
# List file capabilities (recursive); review any unexpected entries
$ getcap -r / 2>/dev/null # Focus on high-risk capability sets often associated with escalation
$ getcap -r / 2>/dev/null | grep -E "cap_(dac_read_search|dac_override|sys_admin|sys_ptrace|sys_module|setuid|setgid)\+"
What to look for

Investigate capabilities applied to general-purpose binaries (shells, interpreters, file viewers/editors, backup tools) and any capability set that effectively bypasses discretionary access controls (for example, cap_dac_override / cap_dac_read_search) or enables powerful kernel/admin actions (for example, cap_sys_admin, cap_sys_ptrace, cap_sys_module). Capabilities are a legitimate design, but unexpected assignments should be treated as a high-severity finding until proven safe.

As with SUID/SGID, compare capability outputs against a known-good baseline for each OS image or server role, and re-check after patching or package changes.

Step 6: Identify World-Writable Files and Directories

World-writable files can be modified by any user on the system. In many cases this is unintended and represents a risk.

terminal
# Find world-writable files (excluding /proc and /sys)
$ find / -xdev -perm -0002 -type f -ls 2>/dev/null # Find world-writable directories
$ find / -xdev -perm -0002 -type d -ls 2>/dev/null # Find files with no owner (orphaned files)
$ find / -xdev -nouser -ls 2>/dev/null # Find files with no group
$ find / -xdev -nogroup -ls 2>/dev/null

World-writable directories without the sticky bit are particularly dangerous because any user can delete or rename files in them, even files they did not create. The sticky bit prevents this, which is why /tmp has it set.

Pay special attention to world-writable files in system directories like /etc, /usr, or /var. These are almost never intentional and represent direct attack paths.

Orphaned files -- those with no valid owner or group -- are also worth investigating. They often result from user accounts being deleted without cleaning up owned files, and they can be claimed or exploited depending on how the system handles unowned resource access. They are also a useful forensic indicator: an orphaned file with a suspicious name or location may be a remnant of attacker activity.

Step 6b: Check Access Control Lists

Standard permission bits do not capture the full picture on systems with ACLs enabled (which includes the vast majority of modern Linux installations using ext4, XFS, or Btrfs). ACLs can grant or deny access to specific users and groups beyond what ls -la shows. A file listed as -rw-r--r-- may have an ACL giving write access to a particular user or group without that being visible in the permission bits.

Use getfacl to inspect ACLs on sensitive paths:

terminal
# Check ACLs on critical system files
$ getfacl /etc/passwd /etc/shadow /etc/sudoers # Recursively check ACLs on a directory
$ getfacl -R /etc/ 2>/dev/null | grep -A2 "^# file" # Find files with non-default ACLs in a path
$ find /etc /home /var /usr -xdev -exec getfacl --skip-base {} \; 2>/dev/null | grep -v "^$"
Warning

The presence of ACL entries on files in /etc or other system directories is unusual and should be investigated. Legitimate ACL use is most common in application data directories and collaborative workspaces. ACLs on configuration files or credential stores are a red flag.

Step 7: Review Home Directory Permissions

User home directories should be readable and writable only by their owners in environments where privacy is expected. A common misconfiguration is home directories set to 755, meaning any user on the system can read the contents of another user's home directory.

$ ls -la /home/

Acceptable permission settings for home directories are typically 700 (only the owner can read, write, or execute) or 750 (the owner and their primary group). Settings of 755 or 777 expose private files to other users and are almost never justified on multi-user systems.

Also check for sensitive files within home directories that have overly broad permissions, such as SSH private keys, shell history files, or configuration files containing credentials.

terminal
# Search for sensitive files in home directories
$ find /home -name "*.key" -o -name "id_rsa" -o -name "id_ed25519" -o -name ".env" 2>/dev/null # Find overly permissive home directories
$ find /home -maxdepth 1 -type d -perm /o+rx -ls 2>/dev/null # Find SSH private keys readable by group or others
$ find /home /root -name "id_rsa" -o -name "id_ed25519" -o -name "id_ecdsa" 2>/dev/null | xargs ls -la 2>/dev/null

SSH private key files should always be mode 600. If they are 644 or wider, the key is readable by other users and should be treated as compromised.

Step 8: Audit SSH Configuration and Authorized Keys

Modern config note: on many current distributions, SSH server configuration can be split across drop-in directories (for example, /etc/ssh/sshd_config.d/*.conf) and may use Include directives. Also remember that Match blocks can change settings for specific users, groups, or source IP ranges. For a trustworthy audit, review the effective configuration that sshd will apply, not just the base file.

terminal
# Show effective SSHD configuration (after Includes and Match logic)
$ sudo sshd -T # If sshd is not in PATH or uses a non-default config file:
$ sudo /usr/sbin/sshd -T -f /etc/ssh/sshd_config # List drop-in config fragments when present
$ ls -la /etc/ssh/sshd_config.d/ 2>/dev/null

When investigating authentication history on systemd-based systems, also remember that authentication events may be in the journal rather than traditional flat files if the host is configured without rsyslog. For example: journalctl -u ssh or journalctl _COMM=sshd.

SSH is the primary remote access mechanism on Linux systems, and its configuration deserves dedicated scrutiny.

Review /etc/ssh/sshd_config for settings that weaken security. PermitRootLogin yes allows direct root login over SSH, which bypasses the audit trail that sudo provides. PermitRootLogin prohibit-password is a common middle-ground that allows key-based root access but blocks password authentication -- review whether this is appropriate for your environment. PasswordAuthentication yes exposes the system to brute-force attacks. PermitEmptyPasswords yes is a critical finding that should never exist in production.

terminal
# Review key SSH security settings
$ sudo grep -E "PermitRootLogin|PasswordAuthentication|PermitEmptyPasswords|X11Forwarding|AllowUsers|AllowGroups|MaxAuthTries" /etc/ssh/sshd_config

Authorized keys files, typically located at ~/.ssh/authorized_keys for each user, define what SSH keys can authenticate as that user. Audit these files to confirm every key is recognized and authorized. On systems using a centralized authorized keys location (configured via AuthorizedKeysFile in sshd_config), check that path as well.

terminal
# Audit all authorized_keys files on the system
$ find /home /root -name "authorized_keys" 2>/dev/null | while read f; do echo "=== $f ===" ls -la "$f" cat "$f" done
Warning

An unknown or undocumented SSH key in an authorized_keys file may indicate a backdoor left by an attacker or a former employee. Every key should be traceable to a specific person or automation system. Also check the permissions on ~/.ssh/ directories (should be 700) and authorized_keys files (should be 600). An SSH daemon may reject keys in files with overly permissive modes.

Step 9: Examine Cron Jobs and Scheduled Tasks

Cron jobs run with the permissions of the user who owns them. If a root-owned cron job executes a script that is writable by a non-root user, that non-root user can effectively run arbitrary code as root. This is one of the most commonly overlooked privilege escalation paths in otherwise well-hardened environments.

terminal
# Review system-wide cron jobs
$ cat /etc/crontab
$ ls -la /etc/cron.d/
$ ls -la /etc/cron.daily/
$ ls -la /etc/cron.hourly/
$ ls -la /etc/cron.weekly/
$ ls -la /etc/cron.monthly/

For each script or binary referenced in these jobs, check its permissions. The script itself, the directory containing it, and any files it sources or includes should all be owned by root and not writable by other users.

terminal
# List all user-specific cron jobs
$ for user in $(cut -d: -f1 /etc/passwd); do crontab -u "$user" -l 2>/dev/null | grep -v '^#' | grep -v '^$' | sed "s/^/$user: /" done

On systems using systemd, also check for scheduled tasks defined as timer units. These are invisible to traditional cron auditing but can trigger scripts with elevated privileges just as cron jobs can.

terminal
# List all active systemd timers
$ systemctl list-timers --all # Inspect a specific timer's associated service
$ systemctl cat timer-name.timer
$ systemctl cat timer-name.service

Step 10: Leverage Automated Auditing Tools

While manual checks are important for depth and understanding, automated tools can accelerate the process and catch things that manual review might miss.

Lynis is an open-source security auditing tool specifically designed for Unix-based systems. Running it produces a detailed report with findings categorized by severity:

$ sudo lynis audit system

LinPEAS (Linux Privilege Escalation Awesome Script) is commonly used by penetration testers to identify privilege escalation paths, many of which are the same things you look for in a permission audit. Running it in an audit context gives you an attacker's-eye view of your own system. It covers SUID binaries, sudo misconfigurations, writable paths, cron abuse vectors, and much more in a single pass.

Auditd, the Linux Audit Daemon, provides real-time monitoring of system calls, file access, and user actions. Configuring it with appropriate rules before an audit period allows you to capture activity data rather than just static configuration snapshots. Key paths to monitor include /etc/passwd, /etc/shadow, /etc/sudoers, and any SUID binaries identified during the static review.

rkhunter (Rootkit Hunter) and chkrootkit scan for known rootkits, backdoor signatures, and suspicious files. While not a substitute for permission auditing, they complement it by checking for indicators of compromise that might explain how misconfigurations came to exist.

Pro Tip

Run both manual checks and automated tools. Manual review gives you depth and context. Automated tools give you breadth and consistency. The combination catches more than either approach alone. Save all tool output to timestamped files so you have evidence artifacts for compliance reporting and a baseline to diff against in future audits.

Documenting Findings and Prioritizing Remediation

An audit produces value only if its findings are acted upon. Document every finding with the following information: the specific item found, the system and path, the risk it presents, the recommended remediation, and the priority level.

Prioritization should be based on exploitability and impact. A world-writable SUID binary that any user can modify is critical and should be remediated immediately. An ACL granting unexpected write access to a system configuration file is high severity. An account with an expired password aging policy is a medium finding that can be addressed in the next maintenance window. Stale accounts for departed employees are low-to-medium depending on what groups they belong to.

For each finding, assign ownership to a specific person or team. Unowned findings have a tendency to remain unresolved indefinitely. Use a tracking system -- even a simple spreadsheet -- so that open findings are visible to management and cannot quietly age out of awareness.

After remediation, re-run the same audit procedures to confirm the findings are closed. This forms the basis of a continuous audit cycle rather than a one-time event.

Building a Continuous Auditing Practice

A single audit is a snapshot. Linux systems change constantly as software is installed, users are added or removed, and configurations drift. The findings from a one-time audit begin to go stale immediately.

Integrate permission auditing into regular operations. Use configuration management tools like Ansible, Chef, or Puppet to enforce baseline permission configurations and detect drift. Maintain a documented baseline of expected SUID binaries, sudo rules, and group memberships, and compare against it regularly. A simple approach is to save sorted output of key commands to a file, then diff future runs against it.

Set up file integrity monitoring using tools like AIDE or Tripwire to detect unauthorized changes to sensitive files and directories. Alert on changes to /etc/passwd, /etc/shadow, /etc/sudoers, SSH authorized_keys files, and SUID binaries. Configure auditd to log access to these files in real time so that if a change occurs between scheduled audits, you have a timestamped record of who made it.

In regulated environments, align your audit cycle with compliance requirements. SOC 2, PCI DSS, HIPAA, and similar frameworks all require evidence of access reviews and permission audits at defined intervals. Automate the evidence collection -- Lynis can output machine-readable reports, and auditd logs can be forwarded to a SIEM -- so that producing audit evidence does not require manual effort each cycle.

References and Primary Sources

For maximum verifiability, validate behavior and syntax against primary documentation:

Conclusion

Auditing Linux user permissions is not a single task but a discipline. It requires understanding the permission model -- including the aspects that standard tools don't surface, like ACLs -- knowing where to look, running the right commands, interpreting the output in context, and translating findings into action. The commands and techniques in this article give you a strong foundation for identifying the security gaps that persist in even well-managed environments.

The principle of least privilege is the north star of permission auditing. Every user account, every sudo rule, every SUID binary, every ACL entry, and every shared directory should be evaluated against a single question: does this level of access represent the minimum necessary to accomplish the intended purpose?