Lightweight privilege switching for the real world — containers, CI pipelines, build systems, and anywhere sudo is overkill and su-exec is too blunt.
suex runs a command as another user. sush opens a shell as another user. Both use a direct exec() model with group-based access control. No config files. No password prompts. No daemons. No child processes.
# Drop from root to app user — clean exec, correct PID, proper signals
suex www-data nginx -g 'daemon off;'
# Switch users from a CI agent (must be in the suex group)
suex deploy /usr/local/bin/run-deployment
# Open an interactive shell as another user
sush postgres| Tool | Direct exec | Access control | Correct supplementary groups | Size |
|---|---|---|---|---|
sudo |
yes | sudoers (complex) | yes | ~2MB + PAM |
su |
no (child process) | PAM | yes | ~50KB |
gosu |
yes | none | yes | ~1.8MB (Go runtime) |
su-exec |
yes | none | partial | ~10KB |
suex |
yes | Unix group | yes | ~70KB |
The short version: sudo and su carry 45 years of multi-user timesharing assumptions into your container. gosu is correct but written in Go. su-exec is small and correct but has no access control — anyone who can execute it can become any user. suex adds group-based access control without any other overhead.
The longer version: sudo is 45 Years Old. Your Container Doesn't Care.
suex is a setuid root binary owned by root and executable only by members of the suex group. When invoked, it:
- Verifies the caller is root or in the
suexgroup (checked by the kernel before any C code runs) - Resolves the target user and group from
/etc/passwd - Sets up the full supplementary group list via
setgroups() - Calls
setgid()thensetuid() - Calls
execvp()— replacing itself entirely with your command
After step 5, suex no longer exists in the process tree. Your command inherits the PID, file descriptors, and signal disposition directly. This is the same model the kernel uses for setuid binaries, applied to user switching.
# with su or sudo:
PID 1 → sudo → your-command
# with suex:
PID 1 → your-command
git clone https://github.com/mobydeck/suex
cd suex
make install # installs to /usr/local/bin by defaultDownload the binary for your architecture from the releases page, copy to /usr/local/bin or /sbin, then set permissions:
chown root:root suex sush
chmod 4755 suex sushCOPY suex /sbin/suex
RUN chown root:root /sbin/suex && chmod 4755 /sbin/suex# Create the access control group
groupadd --system suex
# Restrict the binaries to the suex group (recommended for shared hosts)
chown root:suex suex sush
chmod 4750 suex sush
# Grant access to a user
usermod -a -G suex youruserThe chmod 4750 breakdown: 4 sets the setuid bit, 7 gives root full access, 5 gives the suex group read+execute, 0 locks out everyone else. Users outside the group cannot execute the binary at all — the kernel enforces this before a single line of code runs.
For containers where any process can use suex, chmod 4755 (world-executable) is fine.
Run a command as another user.
suex [-l] [USER[:GROUP]] COMMAND [ARGS...]Options
-l— login mode: clears the inherited environment and setsHOME,USER,LOGNAME,SHELL,MAIL,PATHfor the target user. Terminal and session variables (TERM,COLORTERM,LANG,LC_*,DISPLAY,TMUX,SSH_*, etc.) are inherited from the calling environment. Working directory is unchanged.
User specification
USER— username or numeric UIDUSER:GROUP— username and group name (or numeric IDs)@USERor+USER— prefix notation, same behavior- Omitting USER defaults to root (non-root callers in the
suexgroup only)
Examples
# Root dropping to a less privileged user
suex www-data nginx -g 'daemon off;'
suex nginx:www-data /usr/sbin/nginx -c /etc/nginx/nginx.conf
suex nobody /bin/program
# suex group member elevating to root
suex /usr/sbin/iptables -L
# suex group member switching to another user
suex deploy /usr/local/bin/run-deployment
suex @deploy:deploygroup /usr/bin/deploy-app
# Using numeric IDs
suex 100:1000 /bin/program
# Login mode — clean environment
suex -l postgres /usr/bin/pg_ctl start
suex -l www-data /usr/bin/configure-siteDual behavior
- Called by root: steps down to a less privileged user (like
su) - Called by a
suexgroup member: elevates to root or switches to any user (like a password-free, config-freesudo)
Open an interactive login shell as another user.
sush [-s SHELL] [USERNAME]Options
-s SHELL— use a specific shell instead of the user's defaultUSERNAME— defaults to root if omitted
sush sets up a clean login environment (HOME, USER, LOGNAME, SHELL, MAIL, PATH) and inherits terminal and session variables (TERM, COLORTERM, LANG, LC_*, DISPLAY, TMUX, SSH_*, etc.) from the calling environment. It changes to the target user's home directory and launches the shell with a leading dash in argv[0] — the Unix convention that triggers login shell initialization (.profile, .bash_profile, etc.).
PATH is always clean: ~/.local/bin first, then the standard system path.
Examples
sush # root shell
sush postgres # postgres user's default shell
sush -s /bin/zsh deploy # zsh as the deploy userUses the same permission model as suex — requires membership in the suex group.
The recommended entrypoint pattern for privilege-dropping containers:
FROM debian:bookworm-slim
COPY suex /sbin/suex
RUN chown root:root /sbin/suex && chmod 4755 /sbin/suex
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]#!/bin/sh
# entrypoint.sh
# Root-only initialization
chown -R app:app /data
# ... other setup ...
# Hand off to the application — suex replaces itself via exec()
# Result: PID 1 is your app, running as app, no wrappers
exec suex app "$@"The exec in the shell script replaces the shell with suex. suex then replaces itself with your application. Final result: one process, correct PID, correct user, correct signal handling.
Access control is handled entirely by standard Unix file permissions — no policy files, no configuration, no parser.
What you get:
- Access restricted to the
suexgroup, enforced by the kernel before any userspace code runs - Full user context: correct UID, GID, and all supplementary groups
- Tiny, auditable codebase — no plugins, no shared library loading, no complex parsing
- Direct execution: no privilege manager remaining in the process tree
What you trade:
- No per-command whitelisting (if you need
aliceto runsystemctlbut notbash, use sudoers) - No password prompts (access is determined by group membership)
- No per-command audit logging (group membership is your audit trail)
For environments that need fine-grained command authorization or mandatory password confirmation, sudo is the right tool. suex is for environments where that machinery is overhead.
Query user information from system files.
usrx COMMAND [OPTIONS] USERCommands (available to all users)
info [-j] [-i]— full user profile;-jfor JSON,-ito omit sensitive fieldshome— home directoryshell— login shellgecos— GECOS fieldid— UIDgid— primary GIDgroup— primary group namegroups— all group memberships
Commands (root only)
passwd— encrypted password from/etc/shadowdays— password aging informationcheck USER [PASSWORD]— verify a password; reads from stdin if PASSWORD is omitted; exits 0 on match, 1 on failure
JSON output
usrx info -j username{
"user": "username",
"group": "primary_group",
"uid": 1000,
"gid": 1000,
"home": "/home/username",
"shell": "/bin/bash",
"gecos": "Full Name",
"groups": [
{"name": "group1", "gid": 1000},
{"name": "group2", "gid": 1001}
],
"shadow": {
"encrypted_password": "...",
"last_change": 19168,
"min_days": 0,
"max_days": 99999,
"warn_days": 7,
"inactive_days": -1,
"expiration": -1
}
}The shadow section only appears when running as root. encrypted_password is omitted with -i.
/etc/passwd fields
/etc/shadow fields
Password verification
# Script usage — only exit code matters, no output
if suex usrx check username "$PASSWORD"; then
echo "correct"
fi
# From a file
suex usrx check username < password.txt
# Interactive prompt
suex usrx check usernameNote: passing passwords as command-line arguments exposes them in process listings and shell history. Use stdin redirection in scripts.
Print or convert architecture names for use in build scripts and cross-platform tooling.
uarch # normalized name of the current system: amd64, arm64, etc.
uarch -a # original kernel name: x86_64, aarch64, armv7l, etc.
uarch x86_64 # convert a name: prints amd64
uarch -a arm64 # convert to kernel name: prints aarch64
uarch -h # helpWorks as both a detector (no argument) and a converter (argument given). Handles macOS architecture quirks and maps kernel names to the names used by Linux package repositories, container registries, and Go toolchains.
| Kernel name | Normalized |
|---|---|
| x86_64 | amd64 |
| i686, i586, i486, i386 | i386 |
| aarch64, arm64 | arm64 |
| armv7l, armv7 | armhf |
| armv6l, armv5tel | armel |
| riscv64 | riscv64 |
| s390x | s390x |
| ppc64le | ppc64el |
| ppc64 | ppc64 |
| mips64el | mips64el |
| mips64 | mips64 |
| mipsel | mipsel |
| mips | mips |
| loongarch64 | loong64 |
suex is a reimplementation of su-exec by ncopa, with extended functionality: group-based access control, login mode, proper supplementary group initialization, and the sush companion tool.

