id is one of the commands I run almost reflexively at the start of any debugging session involving permissions — “who am I, actually, right now, in this shell/container/cron job/SSH session.” It looks trivial, but the effective vs real UID distinction it reports is at the heart of a huge share of permission bugs on Linux. Here’s the full picture.
What id Does
id prints the identity of the current user (or a specified user): their user ID (UID), primary group ID (GID), and every supplementary group they belong to. On SELinux-enabled systems, it also reports the current security context.
Syntax
id [OPTION]... [USER]
Options
Tested directly:
| Option | Long form | Description |
|---|---|---|
| (none) | Full identity: uid, gid, and all groups | |
-u | --user | Print only the effective user ID |
-g | --group | Print only the effective group ID |
-G | --groups | Print all group IDs (primary + supplementary) |
-n | --name | Print names instead of numeric IDs (used with -u/-g/-G) |
-r | --real | Print the real (not effective) ID instead |
-Z | --context | Print only the SELinux security context |
-z | --zero | Delimit output with NUL instead of newline |
-a | Ignored, kept for compatibility with other Unix id implementations |
Tested Examples
$ id
uid=0(root) gid=0(root) groups=0(root)
$ id -u
0
$ id -un
root
$ id -g
0
$ id -gn
root
$ id -G
0
$ id -Gn
root
On a regular multi-group user, id -G/id -Gn would list every supplementary group, not just the primary one — for example, id -Gn deploy on a typical server might return deploy docker sudo if that account belongs to multiple groups.
Checking another user’s identity
id www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Real UID vs Effective UID vs Saved UID
This is the concept id exists to expose, and it’s worth understanding properly because it explains a huge amount of Linux permission behavior:
- Real UID (RUID) — who actually launched the process; stays constant across most operations.
- Effective UID (EUID) — the identity the kernel actually uses for permission checks right now. This is what changes when a setuid binary runs — the process’s effective UID becomes the file owner’s UID for the duration.
- Saved UID (SUID, not to be confused with the setuid bit) — a stored value allowing a process to temporarily drop and later reclaim elevated privileges (common in daemons that need root briefly, like binding to a low port, then dropping to run unprivileged).
By default, id reports the effective IDs. id -r reports the real IDs instead. These normally match, but diverge specifically during setuid execution:
$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root ... /usr/bin/passwd
That s in the owner execute position means: while passwd runs, its effective UID becomes root (0), regardless of which real user launched it — which is exactly how an unprivileged user is able to modify /etc/shadow (a root-only-writable file) through passwd, safely, because the setuid binary’s own code controls exactly what gets written.
How id Works Internally
id reads the calling process’s credentials directly from the kernel via getuid(), geteuid(), getgid(), getegid(), and getgroups() — for the current process, this is essentially free and instantaneous, no file lookups required, since the kernel already tracks these values as part of every process’s task structure. When you run id someuser for another account, it instead consults the name service (/etc/passwd, /etc/group, or NSS-configured sources like LDAP/SSSD if configured) via getpwnam()/getgrouplist(), rather than reading live kernel process state, since there’s no running process for that user to query directly.
This distinction matters in enterprise environments using centralized identity — id ldapuser on a properly configured SSSD/LDAP-integrated system correctly resolves group memberships from the directory, not just local /etc/group, which is a good sanity check that directory integration is actually working.
Real-World Use Cases
Confirming a script or service actually runs as the intended user
# Inside a systemd unit's ExecStartPre, or at the top of a deploy script
id
I add a bare id call as the first line of many deployment scripts specifically to catch the class of bug where a script silently runs as the wrong user (common when sudo, su, or a container’s USER directive doesn’t behave as expected) — far better to fail loud immediately than debug a mysterious permission error five steps later.
Verifying group membership took effect
usermod -aG docker deploy
su - deploy -c "id -Gn"
A classic gotcha: group membership changes via usermod don’t apply to already-running sessions — you have to start a new login session (or newgrp) to see the new group reflected in id. Checking with id -Gn in a fresh shell is the standard way to confirm the change actually took effect versus just existing in /etc/group.
Checking effective identity inside a container
docker run --rm myimage id
This is one of the fastest ways to confirm whether a Dockerfile’s USER directive is actually taking effect, and to catch the common mistake of an image still running as root when it was intended to drop privileges.
Using id in conditional scripting
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root" >&2
exit 1
fi
This is the single most common pattern I write in any script that needs root — checking $(id -u) is more portable and reliable than checking $USER or $LOGNAME, both of which can be unset or manipulated in ways UID cannot.
SELinux Context Reporting
On SELinux-enabled distributions (RHEL, Fedora, CentOS by default):
$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
This shows the full SELinux context: user, role, type, and sensitivity range — critical information when debugging SELinux-related “permission denied” errors that occur despite standard Unix permissions looking correct. ausearch -m avc -ts recent paired with knowing the exact context from id -Z is a standard SELinux troubleshooting combination.
Troubleshooting
id shows unexpected groups missing after usermod -aG — start a fresh login shell or run newgrp groupname; group membership is evaluated at login/session start, not live.
id someuser returns “no such user” but the account clearly exists — check NSS configuration (/etc/nsswitch.conf); if the account comes from LDAP/SSSD/Winbind and that service is down or misconfigured, local lookup tools like id will fail even though the account “exists” in the directory.
Effective and real UID differ unexpectedly outside of setuid context — check for setuid()/seteuid() calls in wrapper scripts or the process’s own capability-dropping logic; this is expected behavior for privilege-dropping daemons but worth confirming intentional.
id vs Related Commands
| Command | Purpose |
|---|---|
id | Full identity report: UID, GID, groups, optionally SELinux context |
whoami | Prints only the effective username — a small subset of id -un |
who / w | Shows who is logged into the system generally, not the current process’s identity |
groups | Prints only group memberships by name, a friendlier subset of id -Gn |
logname | Prints the login name from the session’s audit/login record, distinct from effective UID |
whoami and id -un return the same value in almost every situation, but whoami has no equivalent for real vs effective distinction or group reporting, so I reach for id whenever I need more than just the username.
Security Implications
id itself carries no privilege risk — it’s read-only and reports what the kernel already knows. Its real security value is as a verification tool: confirming that privilege-dropping actually worked in a daemon or container, confirming that a setuid binary’s effective UID during execution matches expectations, and confirming group-based access control changes took effect before relying on them. I treat “run id and actually read the output” as a mandatory step any time I change a service’s runtime user, not an optional sanity check — silent failures here (a service still running as root when you believe it dropped to an unprivileged user) are a genuinely common source of real-world security incidents.
Distribution Compatibility
id is part of GNU coreutils and behaves identically across Debian, Ubuntu, Fedora, RHEL/CentOS, Arch, and openSUSE. The -Z (SELinux context) flag only produces meaningful output on SELinux-enabled systems; on AppArmor-based systems (Ubuntu, Debian, openSUSE) it simply reports nothing extra, since AppArmor’s confinement model isn’t exposed the same way. BusyBox’s id (Alpine, minimal containers) supports the core flags (-u, -g, -G, -n) but omits SELinux context reporting entirely.
id in Shell Prompt Customization
Beyond debugging, id (or more commonly whoami, its narrower cousin) frequently shows up embedded in shell prompt configuration, specifically to visually flag when a session is running as root — a genuinely useful safety cue on systems where accidentally leaving a root shell open is a real risk:
if [ "$(id -u)" -eq 0 ]; then
PS1='\[\033[1;31m\]\u@\h:\w#\[\033[0m\] ' # red prompt for root
else
PS1='\u@\h:\w\$ '
fi
I’ve used variations of this on every server I administer — a bright red prompt is a much more reliable warning than remembering to check whoami before running a destructive command.
id and Capabilities: Beyond Simple UID Checks
Modern Linux security models extend well past the classic UID 0 = root binary. Linux capabilities (CAP_NET_BIND_SERVICE, CAP_SYS_ADMIN, and dozens more) allow a process to hold specific privileged abilities without full root UID, and id alone doesn’t report these — a process can show uid=1001(appuser) from id while still holding meaningful elevated capabilities granted via setcap on its executable or inherited through its capability set. For a complete privilege picture beyond what id reports, getpcaps <pid> or capsh --print are the tools that fill that gap. I mention this specifically because it’s a common misunderstanding — seeing a non-root UID in id output doesn’t automatically mean a process has no elevated privileges at all; it just means it isn’t using the traditional all-or-nothing root mechanism.
getpcaps $$
This shows the capability set of the current shell process, a useful complement to id when auditing exactly what a process can do, not just which account it’s nominally running as.
id in User Namespaces and Rootless Containers
Modern container runtimes increasingly rely on Linux user namespaces to run containers “rootlessly” — a process can appear as UID 0 inside its own namespace while actually mapping to an unprivileged UID on the host, dramatically reducing the blast radius of a container escape. id run inside such a namespace reports the namespace-local identity, which can look identical to genuine root (uid=0(root)) even though the host sees a completely different, unprivileged UID for that same process:
podman run --rm alpine id
# uid=0(root) gid=0(root) groups=0(root)
This is expected and correct rootless-container behavior, not a security bug — but it does mean that id output alone, viewed from inside a namespace, is not sufficient evidence of genuine host-level root privilege. Confirming the actual host-side mapping requires checking /proc/<pid>/uid_map from outside the namespace, a detail worth knowing before treating in-container id output as a full security assessment.
Summary
id is a small, fast, read-only window into exactly how the kernel currently sees a process’s identity — real UID, effective UID, and full group membership — and understanding the real/effective distinction it exposes explains a large share of “why does this have permission to do that” questions on Linux. I treat it as a default first step whenever a permission-related bug shows up, in scripts, containers, or interactive sessions alike.
References
- GNU Coreutils Manual —
idinvocation: https://www.gnu.org/software/coreutils/manual/html_node/id-invocation.html - Linux man-pages —
id(1): https://man7.org/linux/man-pages/man1/id.1.html - Linux man-pages —
credentials(7): https://man7.org/linux/man-pages/man7/credentials.7.html - Red Hat Documentation — SELinux User’s and Administrator’s Guide: https://access.redhat.com/documentation/
