How su Works Under the Hood

When you run `su -`, the binary calls `pam_authenticate()` against the target account, which by default is root. PAM checks `/etc/pam.d/su`. On RHEL 9 and derivatives, the default PAM stack for `su` requires the caller to be in the `wheel` group before authentication even proceeds - that is a `pam_wheel.so` check, not a sudo policy.

After authentication succeeds, `su` calls `setuid(0)` and `setgid(0)`, then execs a new shell. The result is a full root session with root's environment, root's `$HOME`, and root's shell history. Every command typed after that runs as root with zero per-command visibility unless you have something like `auditd` watching at the kernel level.

The session ends when you type `exit`. There is no automatic timeout unless you configure `TMOUT` in root's `.bashrc`. Administrators sometimes forget sessions open in tmux panes for hours. `su` has no built-in mechanism to revoke an active session short of killing the process.

# Check current PAM config for su
grep -E 'pam_wheel|pam_rootok' /etc/pam.d/su

# See who is currently running a root shell via su
ps aux | awk '$1 == "root" && /\-bash|\-sh|\-zsh/ {print}'

How sudo Works Under the Hood

sudo reads `/etc/sudoers` (or files in `/etc/sudoers.d/`) before doing anything else. The sudoers file is parsed with a strict grammar - a syntax error renders the entire file invalid, which is why you must always edit it with `visudo`. The binary then calls PAM to authenticate the invoking user, not the target. This is the critical difference: root's password can be blank, rotated to something nobody knows, or locked entirely, and sudo still works.

After policy check and authentication, sudo forks a child process, sets credentials to the target user, and execs only the requested command. The parent process logs the command, the user, the tty, the working directory, and the exit code to syslog and optionally to a session I/O log stored under `/var/log/sudo-io/`. With sudo 1.9+, you can replay those sessions with `sudoreplay`.

The credential cache (the sudo timestamp mechanism) defaults to 15 minutes per tty. You can tighten this with `timestamp_timeout=5` in sudoers, or disable caching entirely with `timestamp_timeout=0`.

# Edit sudoers safely
visudo -f /etc/sudoers.d/ops-team

# Check sudo version and compiled-in defaults
sudo -V | head -20

# Replay a recorded sudo session
sudoreplay -l
sudoreplay /var/log/sudo-io/00/00/01

Privilege Scope: All or Nothing vs. Granular

`su` grants a complete session as the target user. You can do anything that user can do, for as long as the shell is open. There is no practical way to use `su` to allow a developer to restart nginx but not edit `/etc/passwd`. The permission model is binary.

sudo's sudoers syntax lets you define exactly which commands a user or group can run, on which hosts, as which target users, and with or without a password prompt. A real-world example from a deployment pipeline:

This policy lets the `deploy` user restart specific services without a password, edit the nginx config with `sudoedit` (which prevents shell escapes), and nothing else. The `NOPASSWD` flag is appropriate here because the deploy user is a service account whose actions are already constrained by the command list.

For DevOps teams building automated pipelines, this granularity matters. Tools like taskbotshub.ai that orchestrate multi-step deployment workflows can invoke specific sudo-permitted commands without ever needing or storing a root credential.

# /etc/sudoers.d/deploy-user
Cmnd_Alias DEPLOY_CMDS = /bin/systemctl restart nginx, \
                          /bin/systemctl restart app, \
                          /usr/bin/sudoedit /etc/nginx/nginx.conf

deploy ALL=(root) NOPASSWD: DEPLOY_CMDS
// advertisement

Audit Trail Comparison

This is where the gap between the two tools is widest in production environments.

With `su`, your audit trail depends entirely on external tooling. `auditd` with the right rules can log execve syscalls, but correlating those logs back to the human who ran `su` requires matching tty, session ID, and login UID fields in the audit log. It works, but it requires setup and discipline. Out of the box, `su` logs only the authentication event: who switched to which user and whether it succeeded.

With `sudo`, every invocation writes a structured log line to syslog that includes the original username, the target user, the full command with arguments, the working directory, and the tty. On our test server running Ubuntu 24.04 with rsyslog, a single `sudo systemctl restart nginx` by user `alice` produces:

If you enable `log_output` in sudoers, sudo captures the full terminal session - every character typed, every character displayed - and writes it to `/var/log/sudo-io/`. That recording is admissible evidence in a post-incident review and directly useful when someone asks 'what exactly did the contractor do at 2am'.

# Example sudo syslog entry
Jul 15 02:13:44 prod-web01 sudo: alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/bin/systemctl restart nginx

# Enable session recording in sudoers
Defaults log_output
Defaults iolog_dir=/var/log/sudo-io/%{user}

# Check auditd coverage for su (requires auditd configured)
auditctl -l | grep execve
aureport --auth | grep su

Root Password Management

Using `su` requires a known root password. That creates a shared secret that must be rotated whenever anyone with that knowledge leaves the team. On a fleet of 50 servers, rotating root passwords consistently is operationally expensive and often done poorly or not at all.

sudo eliminates the need for a known root password. You can lock the root account entirely:

After doing this, no one can `su -` or log in directly as root. All privilege escalation goes through sudo, which authenticates users individually. When an employee leaves, you remove their sudo privileges and optionally expire their account. You do not touch root's password because it does not matter.

On our test environment we have run production servers with locked root accounts for three years. The only time this caused friction was during a kernel panic that required single-user mode - which bypasses both `su` and `sudo` anyway, making it irrelevant to this discussion.

# Lock the root password - root can no longer authenticate with a password
passwd -l root

# Verify root account status
passwd -S root
# Output: root L 2024-01-15 0 99999 7 -1
# L means locked

# Still allow sudo to work as root
# sudoers already handles this - no change needed

Environment Handling Differences

`su -` (with the hyphen, which you should always use) creates a login shell for root, sourcing root's `/etc/profile`, `~/.bash_profile`, and `~/.bashrc`. This gives you root's actual environment including `$PATH`, aliases, and any custom tooling root has configured. `su` without the hyphen inherits the calling user's environment with only UID/GID changes - this is the source of many 'command not found' bugs when people run `su` and then can't find `/sbin/ip`.

sudo's environment behavior is controlled by `env_reset` (enabled by default in most distributions). When `env_reset` is active, sudo starts with a minimal environment: `TERM`, `PATH`, `HOME`, `MAIL`, `SHELL`, `LOGNAME`, `USER`, `USERNAME`, and `SUDO_*` variables. Everything else is stripped unless you explicitly whitelist it with `env_keep`.

This stripping behavior is a security feature - it prevents environment variable injection attacks like `LD_PRELOAD` escalation. But it surprises developers who expect their `EDITOR` or `JAVA_HOME` to carry through. The fix is targeted `env_keep` entries, not disabling `env_reset` entirely.

# Compare environments
su - -c 'env | sort' > /tmp/su_env.txt
sudo -i env | sort > /tmp/sudo_env.txt
diff /tmp/su_env.txt /tmp/sudo_env.txt

# Keep specific variables in sudo sessions
# In sudoers:
Defaults env_keep += "EDITOR JAVA_HOME NPM_CONFIG_PREFIX"

# Run a command preserving full environment (use carefully)
sudo -E command_here
// advertisement

Centralized Management and LDAP Integration

On a single server, both tools are manageable. On a fleet of 200 servers, the difference in operational overhead is significant.

`su` has no centralized policy mechanism. If you want to restrict which users can run `su` on all your servers, you need to configure `/etc/pam.d/su` and the `wheel` group on every host individually, or push that configuration via Ansible, Puppet, or Salt.

sudo supports `sudoers` delivered via LDAP through the `sudo-ldap` package. With this setup, you define sudo policies in your directory (OpenLDAP, Active Directory with schema extensions, or FreeIPA) and every server queries the directory at sudo invocation time. Add a user to the `ops-sudoers` LDAP group and they get sudo access on all participating servers within seconds, with no configuration push required.

FreeIPA, which combines 389-ds with Kerberos and a sudo policy engine, makes this particularly clean. On our test infrastructure with 40 hosts enrolled in FreeIPA, granting a new team member full sudo access on the web tier takes one `ipa sudorule-add-user` command.

# FreeIPA: add user to existing sudo rule
ipa sudorule-add-user web-admin-sudo --users=newuser

# Verify on a target host (queries LDAP)
sudo -l -U newuser

# ldap-based sudoers entry example (cn=ops,ou=sudoers,dc=example,dc=com)
# sudoUser: %ops-team
# sudoHost: ALL
# sudoCommand: ALL
# sudoOption: !authenticate

When su Is Still Appropriate

We use sudo as the default everywhere, but there are specific scenarios where `su` is the right tool.

First: switching to non-root service accounts. If you need to debug an application running as `postgres` or `www-data`, `su - postgres` is direct and correct. You could configure sudo to run `sudo -u postgres -i`, but that is more typing for the same result. Neither tool has a meaningful audit advantage here if you are already logged in as yourself.

Second: recovery scenarios. During single-user boot, sudo may not be available or functional. `su` (or simply being dropped to a root shell) is what you use. Having root's password on a printed sheet in a sealed envelope in a physical safe is not paranoid - it is a recovery plan.

Third: container images with minimal PAM stacks. Some minimal containers do not have a full sudoers and PAM setup. If you are dropping into a debug container and need root, `su` with the container's root password (or no password, for a scratch container) is faster than reconfiguring sudo.

Fourth: testing PAM and authentication configurations. When you want to verify that a new PAM module works correctly for user authentication, `su` gives you a clean test path that is separate from your production sudo setup.

# Switch to postgres user for debugging
su - postgres
psql -c 'SELECT pg_stat_activity.* FROM pg_stat_activity;'

# Equivalent via sudo (requires sudoers entry)
sudo -u postgres psql -c 'SELECT ...' 

# In a debug container
docker run -it --rm alpine sh
# Inside: su is available without PAM complexity