Intro

Unix isn’t a lonely single-player game.

Even when adventuring on your personal laptop, you’re never alone. A whole party of user accounts travels with you: root with absolute power, daemon running background jobs, nobody handling scraps, and dozens of service identities (mysql, www-data, postfix). They own files, run processes, and enforce boundaries between users.

That’s the point: Unix was built for multiuser life. In the 1970s, a single minicomputer would act as a hub for dozens of dumb terminals — green-screen VT100s, Teletypes, or glass TTYs — all wired in over RS-232 serial lines. Later, SLIP and PPP links carried those sessions across early networks. Everyone logged in concurrently, sharing the same CPU, disks, and memory. To survive in that environment, the OS enforced strict user separation: each login session mapped to a unique UID, file ownership was enforced at the kernel level, and processes were isolated by identity.

That DNA remains at the heart of Unix. Even today, the cast of characters — root, daemon, nobody, end-user accounts, and service identities — exists to enforce least privilege and prevent one misbehaving process from trampling another.

This guide explains:

  • History and concepts of Unix users
  • Where identities live (/etc/passwd, /etc/shadow, /etc/group, /etc/sudoers)
  • A cross-platform, task-based cheatsheet covering everything from inspecting to creating, modifying, and deleting users, plus group management and sudo.
  • Historical tidbits, gotchas, and modern tricks
  • All the pertinent documentation and links

NOTE By the end you’ll have your own player’s screen — every user-related command condensed into one place and neatly organized by task.


So, What is a User?

In Unix, a “user” isn’t a human being — it’s an identity (a user principal) that the kernel uses to decide what’s allowed. Every process runs as someone, every file is owned by someone, and the system enforces permissions based on those identities.

At the machine level, a user boils down to three things:

  • UID (User ID): a unique integer (UID). This is what the kernel actually checks. UID 0 is hard-coded as root, the all-powerful superuser.
  • GID (Group ID): groups are collections of users, identified by their own numeric IDs (GID). Group permissions are checked alongside individual user permissions.
  • Username: a human-friendly alias for a UID. When you type alice at a login prompt, the system maps that to UID 1000 (or whatever number was assigned).

The kernel never cares about names — only numbers. When you run ls -l, it looks up UIDs and GIDs in /etc/passwd and /etc/group just to display friendlier, human-readable names.

NOTE If you delete a user but keep their files, ownership doesn’t vanish. The UID stays behind, and you’ll see files owned by 1001 or 2000, etc. That’s the ghost of a user: the identity is gone, but the number persists in the filesystem.

This split between human-friendly names and machine-level numbers is deliberate. It means accounts can be automated, identities can be isolated, and multiuser systems don’t collapse into chaos.


History & Tidbits

Unix grew up in a world where multiuser wasn’t optional — it was the baseline.

  • 1970s multiuser terminals: A single PDP-11 or VAX might have dozens of dumb terminals (Teletypes, VT100s, Wyse displays) connected over serial lines. Each terminal was just a keyboard and screen; all the real computing happened on the host. Later, SLIP and PPP carried terminal sessions over early TCP/IP links, making multiuser logins possible from remote sites.
  • Strict separation of users: Because many people were logged in simultaneously, the kernel had to enforce sharp boundaries. Each session mapped to a unique UID; processes couldn’t touch each other’s files; and permissions were checked on every system call.
  • UID 0: Always root. The kernel literally has “if (uid == 0)” checks hard-coded in privileged operations. This convention has survived intact for 50+ years.
  • System accounts: Services needed their own identities to run safely. Instead of everything being root, daemons like mail, www-data, and mysql got UIDs of their own. That way, a web server compromise didn’t instantly mean total system takeover.
  • Different OS ranges:
    • Linux: human users typically start at UID 1000 today (but 500 on older RHEL/CentOS). Below that are system accounts.
    • macOS: human users start at 501; everything below is reserved. Apple prefixes many system accounts with underscores (_spotlight, _windowserver).
    • BSD: similar to Linux, but ranges vary; users should start at UID 1001; service accounts and reserved IDs are well documented in the BSD handbooks.
  • “Nobody” user: UID 65534 (or sometimes -2) is a special identity with the least privilege possible. It exists to run processes with almost no rights at all.

The takeaway: Unix users are a product of real hardware and real constraints — not just an abstract security idea. The multiuser DNA of the 1970s still shapes how your laptop and servers work today.


The User Database

So where do these identities actually live? In classic Unix, they’re just flat text files:

  • /etc/passwd — the account roster. Username, UID, GID, home directory, shell, and a password placeholder. World-readable.
  • /etc/shadow — password hashes and aging rules. Only root can read it. If this file leaks, the system is blown.
  • /etc/group — group definitions and memberships. Controls shared access.
  • /etc/sudoers — the privilege ledger. Defines who can become root (and how).
a pixel art depiction of an open book with mystical symbols

On modern enterprise systems, these files often act as a front-end to NSS (Name Service Switch), which may pull identities from LDAP, NIS, Kerberos, or Active Directory. That’s why tools like getent are preferred over cat /etc/passwd — they return the whole picture, not just the local slice.

Think of these files as the bones of identity. The commands you run — passwd, useradd, dscl, visudo — are the muscles that safely move those bones around.

And above it all sits the kernel, acting as the nervous system.
It doesn’t care whether an identity came from a flat file or a directory
service; all it sees are UIDs and GIDs. That abstraction is the secret
that lets Unix scale from a single laptop to a campus full of machines
while keeping the rules of identity consistent.


Permissions & Ownership Recap

Users only matter because the kernel enforces who owns what and who can do what. That enforcement happens at two levels: files and processes.

File Permissions

Every file has two owners:

  • a user owner (UID)
  • a group owner (GID)

And three sets of permissions: user (u), group (g), and other (o).

Example:

-rwxr-sr-- 1 alice devs 532 Sep 21 13:01 script.sh

Breakdown:

  • - → type of file. (- = regular file, d = directory, l = symlink, c/b = device, s = socket, p = named pipe)
  • rwx → permissions for the user/owner (alice): read, write, execute.
  • r-s → permissions for the group (devs): read + execute, plus s meaning setgid is set.
  • r-- → permissions for others: read only.
  • 1 → hard link count. Directories show how many subdirs + self + parent.
  • alice → the owner (mapped from UID).
  • devs → the group (mapped from GID).
  • 532 → file size in bytes.
  • Sep 21 13:01 → last modification time.
  • script.sh → filename.

Other permission bits:

  • setuid (s on user perms): program runs with file owner’s UID. Example: /usr/bin/passwd runs as root.
  • setgid (s on group perms): files created in this directory inherit the group; executables run with group’s GID.
  • sticky bit (t on others perms): on directories, only file owners can delete their files. /tmp uses this.

💡 Gotcha: Permissions are checked on every system call (open(), execve(), etc). There’s no “once at login” caching — enforcement is continuous.


Process Ownership

Every process runs as a user and carries both a real UID (who started it) and an effective UID (who it’s acting as).

ps -u alice
UID   PID  CMD
1000  2345 bash
1000  2371 vim
0     2402 sudo
0     2403 systemctl

Here:

  • Alice owns her bash and vim processes.
  • When she runs sudo systemctl, the new process has UID 0 (root).

Key fields:

  • Real UID/GID → the account that launched the process.
  • Effective UID/GID → what the kernel uses for permission checks. (Setuid/setgid binaries modify this.)
  • Saved UID → allows a process to drop and later regain privileges (common in daemons).

💡 Example: Apache (httpd) starts as root to bind to port 80, then immediately drops privileges to www-data. If the web server is compromised, the attacker only gains www-data rights, not root.


Why It Matters

  • Every file belongs to someone. Delete the account, and the UID lingers.
  • Every process runs as someone. If that process is compromised, its UID defines the blast radius.
  • Least privilege works only if users and groups are defined properly. Services should almost never run as root.

This is the backbone of Unix security. Once you grasp how file permissions and process ownership interact, the user management commands in the cheatsheet make sense: you’re really just moving numbers and labels around to control who owns what and who can act as whom.


a pixel art image of a pair of 20-sided dice with a 20 and a 1 showing
Let's roll up some users!

Cheatsheet

Task-based, cross-platform, and complete. Each section expands with commands from basic to advanced.


Inspect Users & Groups

Check who you are, who exists on the system, and what groups users belong to.

👀 Show commands
# === Current identity ===
whoami                    # show effective username
id                        # UID, GID, groups
id -u                     # numeric UID only
id -g                     # numeric GID only
id -nG                    # group names only
id -G                     # numeric GIDs only
groups                    # list groups (Linux, BSD)
groups alice              # groups for another user

# === System-wide queries ===
getent passwd             # all users (NSS-aware: local, LDAP, AD)
getent passwd alice       # one user entry
getent group              # all groups
getent group sudo         # details of one group
getent group | grep alice # all groups containing alice

# === Local file lookups (local-only, not NSS-aware) ===
cut -d: -f1 /etc/passwd   # usernames only
awk -F: '$3 < 1000 {print $1, $3}' /etc/passwd   # system accounts
awk -F: '$3 >= 1000 {print $1, $3}' /etc/passwd  # human accounts

# === macOS Directory Services ===
dscl . -list /Users             # all users
dscl . -read /Users/alice       # full record for 'alice'
id alice                        # UID/GID/groups still works

# === BSD variants ===
pw usershow alice               # FreeBSD: show one user
pw groupshow wheel              # FreeBSD: show one group

Notes:

  • Prefer id -nG and getent in scripts (NSS-aware).
  • /etc/passwd shows local users only, which may miss LDAP/AD accounts.
  • On macOS, dscl is the source of truth.


Create Users

Create human accounts or service accounts.

👀 Show commands
# === Linux (Debian/Ubuntu) ===
sudo adduser alice                  # interactive, sets password, creates home, shell

# === Linux (RHEL/Fedora) ===
sudo useradd -m -s /bin/bash alice  # create with home and shell
sudo passwd alice                   # set password

# === Service/system accounts ===
sudo useradd -r -s /usr/sbin/nologin -d /var/www www-data

# === macOS 10.13+ ===
sudo sysadminctl -addUser alice -fullName "Alice Smith" -password -
# Legacy alternative:
sudo dscl . -create /Users/alice
sudo dscl . -create /Users/alice UserShell /bin/zsh
sudo dscl . -create /Users/alice RealName "Alice Smith"
sudo dscl . -create /Users/alice UniqueID "501"
sudo dscl . -create /Users/alice PrimaryGroupID 20
sudo dscl . -create /Users/alice NFSHomeDirectory /Users/alice
sudo dscl . -passwd /Users/alice password

Notes:

  • adduser (Debian) is friendlier than useradd (RHEL).
  • Always use -m with useradd to ensure a home directory is created.
  • On macOS, sysadminctl is preferred, but dscl gives more fine-grained control.


Modify Users

Change passwords, shells, groups, or lock accounts.

👀 Show commands
# === Passwords ===
passwd alice                # change password

# === Shell ===
chsh -s /bin/zsh alice      # change login shell

# === Groups ===
usermod -aG sudo alice      # add alice to group (Linux)
gpasswd -a alice developers # alternative on some distros
gpasswd -d alice developers # remove from group

# === Lock/Unlock ===
usermod -L alice            # lock account (Linux)
usermod -U alice            # unlock account (Linux)
passwd -l alice             # lock via passwd tool
passwd -u alice             # unlock

# === macOS ===
dscl . -change /Users/alice UserShell /bin/bash /bin/zsh
dscl . -append /Groups/admin GroupMembership alice

Notes:

  • ⚠️ usermod -G without -a replaces all groups.
  • Locking prepends ! to the shadow password field.
  • macOS uses dscl to edit user attributes and groups.


Delete Users

Remove accounts and clean up files.

👀 Show commands
# === Linux ===
sudo userdel alice               # delete user, keep files
sudo userdel -r alice            # delete user and home directory
sudo deluser alice               # Debian helper
sudo deluser --remove-home alice # remove home too

# === Find orphaned files ===
find / -nouser -o -nogroup 2>/dev/null

# === macOS ===
sudo sysadminctl -deleteUser alice
# Or, with dscl:
sudo dscl . -delete /Users/alice

Notes:

  • Orphaned files will show as owned by a numeric UID.
  • Always search for -nouser files after deletion.
  • On macOS, sysadminctl handles home directory cleanup.


Manage Groups

Groups define shared access.

👀 Show commands
# === Linux ===
groupadd developers           # create group
groupdel developers           # delete group
usermod -aG developers alice  # add to group
gpasswd -d alice developers   # remove from group

# === BSD ===
pw groupadd developers
pw groupmod developers -m alice

# === macOS ===
dscl . -create /Groups/devs
dscl . -append /Groups/devs GroupMembership alice
dscl . -delete /Groups/devs

Notes:

  • Common admin groups: sudo (Ubuntu), wheel (RHEL/BSD), admin (macOS).
  • Setgid bit on directories (chmod g+s) makes files inherit group ownership.


Classic Multiuser Tools

Old-school, but still around.

👀 Show commands
who              # list logged-in users
w                # list logged-in users + what they’re doing
users            # just usernames
last             # login history
write bob        # message another user
wall "msg"       # broadcast to all users
talk bob         # split-screen chat
finger bob       # show user info (if finger service enabled)

Notes:

  • These tools reflect the multiuser roots of Unix — a reminder that one
    machine often served an entire lab or office.
  • Still useful today for audits, troubleshooting, or curiosity (e.g.
    spotting a forgotten session or checking login history).
  • Messaging commands like write, wall, and talk are often disabled on
    modern systems, and finger is usually missing entirely due to security
    concerns.
  • The finger command would also display a user’s ~/.plan file — a personal
    status note people used for anything from office hours to quirky quotes.
    In the early internet, .plan files became a proto–status update, years
    before blogs or Twitter.


Sudo & visudo

The /etc/sudoers file decides who can act as root (or another user).
Always use visudo to edit it — it locks the file and checks syntax so you don’t brick sudo.

👀 Sudo basics & examples
# === Change default editor (defaults to vi) ===
export EDITOR=nano
sudo visudo

# === Rule format ===
user_or_%group   host = (run_as) command_list

# Parts of a rule:
# - user_or_%group → single user (alice) or group (%wheel)
# - host           → usually ALL unless restricted to specific hosts
# - run_as         → ALL (default root) or another user (postgres, deploy)
# - command_list   → full path(s) to allowed commands

# === Open and validate sudoers ===
sudo visudo            # edit main sudoers file safely
sudo visudo -c         # check config syntax only
sudo visudo -f /etc/sudoers.d/webadmins   # edit a drop-in file

# === Give one user full root powers ===
alice ALL=(ALL) ALL

# === Group-based full access ===
%wheel ALL=(ALL) ALL      # common on RHEL/BSD
%sudo  ALL=(ALL) ALL      # common on Debian/Ubuntu
%admin ALL=(ALL) ALL      # common on macOS

# === Limit bob to restarting nginx only ===
bob ALL=(ALL) /usr/bin/systemctl restart nginx

# === Let webadmins group manage nginx + apache ===
%webadmins ALL=(ALL) /usr/bin/systemctl restart nginx, \
                     /usr/bin/systemctl restart apache2

# === Allow package updates only ===
dave ALL=(ALL) /usr/bin/apt update, /usr/bin/apt upgrade
dave ALL=(ALL) /usr/bin/yum update
dave ALL=(ALL) /usr/bin/dnf upgrade

# === Run Docker commands without full root ===
carol ALL=(ALL) /usr/bin/docker ps, /usr/bin/docker restart *

# === Run commands as a different user (deploy) ===
carol ALL=(deploy) /usr/bin/git pull, /usr/bin/git checkout

# === Force password every time (no caching) ===
Defaults timestamp_timeout=0

# === Allow passwordless sudo (⚠️ dangerous) ===
alice ALL=(ALL) NOPASSWD: ALL

Notes:

  • Prefer drop-in configs under /etc/sudoers.d/ instead of cluttering /etc/sudoers.
  • Always use absolute paths to commands in rules (which systemctl).
  • Use groups (%group) to manage privileges cleanly for teams.
  • Check syntax anytime with sudo visudo -c.

🔗 Docs & References:


Troubleshooting

When a user can’t log in, can’t write files, or sudo mysteriously fails, these checks will save you.

👀 Show Commands
# === File permission issues ===
ls -l file                  # check ownership + rwx bits
id alice                    # confirm UID + GIDs
groups alice                # confirm group memberships

# === Login problems ===
passwd -S alice             # Linux: check password status (L=locked, P=usable)
chage -l alice              # Linux: check password aging + expiry
grep ^alice: /etc/passwd    # check home dir + shell field
dscl . -read /Users/alice   # macOS: inspect account record

# === Orphaned files or groups ===
find / -nouser -o -nogroup 2>/dev/null   # files with no matching UID/GID

# === Process ownership ===
ps -u alice                 # all processes owned by alice
pgrep -u alice              # list PIDs only
pkill -u alice              # kill all processes for alice (⚠️ destructive)

# === Sudo debugging ===
sudo -l                     # list sudo rights for current user
sudo -v                     # refresh credentials (prompts password)
sudo -k                     # expire cached credentials

Notes:

  • If /etc/passwd shows /usr/sbin/nologin, /sbin/nologin, or /bin/false, the user cannot log in interactively.
  • Account expiry or locks often explain mysterious login failures (passwd -S, chage -l).
  • Always check group membership (id -nG) when file access doesn’t make sense.
  • On macOS, many system accounts start with _ and are not intended for login.


a pixel art image of 3 orcs armed with spears and swords in a dungeon setting
Onward to advanced topics — time to deal with pesky orcs.

Advanced Topics

The basics cover 90% of admin life, but sometimes you need sharper tools. These features extend the Unix user model into modern territory.


Defaults & Templates

When a new account is created, the system applies defaults: skeleton files, UID ranges, shells, and the default mask (umask). These define how a fresh user’s environment looks and how secure their files are.

👀 Show commands & notes
# === Skeleton files ===
ls -A /etc/skel          # files copied into new home dirs
# .bashrc  .profile  .bash_logout

# Add a custom file for all new users
sudo cp /etc/motd /etc/skel/welcome.txt

# === Default umask ===
umask                    # show current mask
# 0022 → new files 644 (-rw-r--r--) and dirs 755 (drwxr-xr-x)

# Change umask for tighter privacy (per shell session)
umask 0077
touch secret.txt && ls -l secret.txt
# -rw------- 1 alice users 0 Sep 21 16:10 secret.txt

# === Global login defaults (Linux) ===
grep -E 'UID_MIN|UID_MAX|GID_MIN|GID_MAX' /etc/login.defs
# UID_MIN 1000, UID_MAX 60000

Notes:

  • How /etc/skel works:

    • Any file or directory in /etc/skel gets copied into the new user’s home when the account is created (unless disabled with useradd -M -k).
    • Typical contents: .bashrc, .profile, .bash_logout.
    • You can also include a company-wide README, a welcome.txt, or even preconfigured dotfiles like .vimrc or .gitconfig.
    • Updating /etc/skel only affects future accounts, not existing ones.
  • How umask works:

    • It’s a subtractive mask: permissions are removed from the base defaults (666 for files, 777 for directories).
    • Example:
      • umask 0022 → files 644 (rw-r--r--), dirs 755 (rwxr-xr-x) → standard, readable by everyone.
      • umask 0077 → files 600 (rw-------), dirs 700 (rwx------) → private, nobody else can read.
      • umask 0002 → files 664, dirs 775 → collaborative group environments.
    • Why change it?
      • Servers often use 0022 (safe default).
      • Multiuser/dev environments may prefer 0002 so teams in the same group can share files easily.
      • Security-sensitive environments often use 0077 to prevent accidental leakage.
  • Other considerations:

    • umask can differ between shells, cron jobs, and systemd services.
    • Linux: defaults can be set in /etc/login.defs or PAM config.
    • BSD: see /etc/adduser.conf.
    • macOS: relies on sysadminctl + Directory Services defaults.


Name Service Switch & Domains

On modern systems, /etc/passwd is just one source of truth. Enterprises often keep users in LDAP, Kerberos realms, or Active Directory. The Name Service Switch (NSS) decides where the system looks when resolving usernames, groups, and hosts.

👀 Show commands & notes
# === NSS lookup order (Linux) ===
grep passwd /etc/nsswitch.conf
# passwd: files systemd sss
# "files" = /etc/passwd, "systemd" = local systemd users, "sss" = SSSD (LDAP/AD)

# === Query via NSS (all backends) ===
getent passwd alice          # works even if alice is in LDAP/AD
getent group devs            # query group membership

# === Show only local file (not NSS aware) ===
cat /etc/passwd | grep alice # will miss LDAP/AD users

# === Join an AD domain (Linux, via realmd/SSSD) ===
realm discover example.com   # discover domain controllers
sudo realm join example.com  # join domain
systemctl status sssd        # domain users now available via SSSD

# === Test a domain account ===
id alice@example.com
# uid=123456789(alice@example.com) gid=123456789(domain users) groups=...

Notes:

  • NSS vs PAM:

    • NSS answers “does this identity exist?” (user/group lookups).
    • PAM answers “can this identity log in?” (authentication, password policy, session rules).
  • Why getent matters:

    • getent queries the entire NSS stack — so LDAP, AD, or other remote backends are included.
    • cat /etc/passwd only shows local users and will miss network accounts.
  • SSSD & caching:

    • On Linux, SSSD (System Security Services Daemon) acts as the glue for LDAP/AD lookups.
    • It caches users for performance and offline login.
    • If users/groups seem stale, clear the cache:
      sss_cache -E
      
  • Joining domains:

    • realmd + sssd is the common modern combo (RHEL, Fedora, Ubuntu).
    • Older setups may use nslcd or winbind for LDAP/AD.
    • macOS has its own directory service integration (dsconfigad).
    • BSD systems typically rely on nss_ldap + pam_ldap.
  • Troubleshooting tip:

    • Always check nsswitch.conf first — if “sss” or “ldap” isn’t listed for passwd and group, your system won’t even try querying the domain.


Access Control Lists (ACLs)

The classic Unix model (user/group/other) is simple but limited. What if you want multiple users with different rights on the same file, without changing ownership or creating new groups? That’s where Access Control Lists (ACLs) come in. ACLs add fine-grained permissions on top of the traditional model.

👀 Show commands & notes
# === See current permissions (no ACL yet) ===
ls -l project.txt
# -rw-r----- 1 alice devs 42 Sep 21 17:00 project.txt

# Only alice (owner) has rw, devs group has r, others none.

# === Add bob with RW access via ACL ===
setfacl -m u:bob:rw project.txt
getfacl project.txt
# file: project.txt
# owner: alice
# group: devs
user::rw-
user:bob:rw-          # new ACL entry
group::r--
mask::rw-
other::---

# === Remove bob's entry later ===
setfacl -x u:bob project.txt

# === Default ACL on a directory ===
setfacl -d -m g:devs:rwx /srv/project
ls -ld /srv/project
# drwxrwxr-x+ 2 root root 4096 Sep 21 17:05 /srv/project
# (+ indicates ACLs are set)
getfacl /srv/project
# default:group:devs:rwx

# Now any file created under /srv/project inherits group rwx.

# === macOS NFSv4 ACLs ===
ls -le project.txt
-rw-r-----+ 1 alice staff 42 Sep 21 17:10 project.txt
 0: user:bob allow read,write

chmod +a "bob allow read,write" project.txt

Notes:

  • Why ACLs?

    • u/g/o model works fine until you need to share a resource between specific users who don’t share a group.
    • ACLs let you grant per-user or per-group rights without redesigning ownership.
    • Great for project directories, shared data, or complex multiuser environments.
  • Default ACLs:

    • On directories, default ACLs ensure all new files inside inherit the access rules automatically.
    • Example: shared group workspaces, where all new files should be writable by devs.
  • Implementation differences:

    • Linux & BSD: setfacl, getfacl.
    • macOS: NFSv4 ACLs with ls -le and chmod +a.
    • The + sign in ls -l output means “this file has ACLs.”
  • Gotchas:

    • Not all filesystems support ACLs (may need mount -o acl on ext4).
    • Backups that don’t preserve extended attributes may strip ACLs.
    • ACLs can make permissions confusing — always use getfacl to confirm.


Linux Capabilities

Traditionally, if a program needed any privileged action (like opening a raw socket or binding to a low port), it had to be setuid root. That gave it full root power, even if it only needed one small permission.

Linux capabilities break root’s powers into fine-grained units — like CAP_NET_RAW (raw sockets) or CAP_SYS_ADMIN (system-wide admin). You can then grant a binary just the slice it needs instead of all-or-nothing root.

👀 Show commands & notes
# === Traditional setuid ping ===
ls -l /bin/ping
# -rwsr-xr-x 1 root root ...
# setuid root: ping runs as full root just to open raw sockets.

# === Replace with capability ===
sudo chmod u-s /bin/ping                   # remove setuid bit
sudo setcap cap_net_raw+ep /bin/ping       # give only raw socket ability
getcap /bin/ping
# /bin/ping = cap_net_raw+ep

# === Test that it works ===
ping -c1 127.0.0.1                         # works without setuid

# === Drop capabilities from a running process (demo with sleep) ===
sleep 100 &
pid=$!
grep CapEff /proc/$pid/status              # shows effective caps (usually 0000000000000000)

Notes:

  • Safer than setuid:

    • Old way: ping had full root rights — if exploited, attacker gets root.
    • With capabilities: attacker only gains the one granted privilege.
  • How capabilities are assigned:

    • setcap cap_name+ep file → give a binary capability (e=effective, p=permitted).
    • getcap file → check capabilities.
    • capsh --print → view current shell’s capabilities.
  • Common useful capabilities:

    • CAP_NET_BIND_SERVICE → bind to ports <1024 without root.
    • CAP_NET_ADMIN → manage networking.
    • CAP_SYS_TIME → set the system clock.
    • CAP_SYS_ADMIN → (⚠️ extremely broad, “root-lite”).
  • Gotchas:

    • Capabilities are stored as extended attributes — they don’t survive a normal cp. Use rsync -aX or install -m755 -o root -g root.
    • Some filesystems don’t support extended attributes (e.g. older NFS).
    • Granting too many caps (especially CAP_SYS_ADMIN) defeats the purpose.
  • Why care?

    • Capabilities let you follow least privilege in service design.
    • Systemd services can also drop or restrict capabilities with CapabilityBoundingSet= in unit files.


Systemd Dynamic Users

Normally, services run under pre-created system accounts like www-data or mysql. But that clutters /etc/passwd with dozens of long-lived identities that stick around even if the service is removed.

Dynamic users solve this: systemd can allocate a throwaway UID/GID at runtime when the service starts. When the service stops, the UID disappears. It’s perfect for daemons that don’t need persistent files or shells.

👀 Show config & notes
# /etc/systemd/system/web.service
[Service]
ExecStart=/usr/bin/mydaemon
DynamicUser=yes                    # allocate ephemeral UID at runtime
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

Notes:

  • How it works:

    • Each start assigns a UID like dynamic-1000.
    • No entry is written to /etc/passwd; it’s entirely managed by systemd.
    • The UID disappears once the service stops.
  • When to use:

    • For services that don’t need a home directory or persistent files.
    • Great for stateless daemons, network listeners, or sandboxed apps.
  • Persistent data:

    • Use StateDirectory=, CacheDirectory=, or LogsDirectory= in the unit file to create system-managed dirs with correct ownership.
    • Example:
      StateDirectory=mydaemon
      
      → systemd creates /var/lib/mydaemon/ owned by the dynamic UID.
  • Security hardening:

    • Combine DynamicUser=yes with:
      • ProtectSystem=strict → service sees /usr as read-only.
      • ProtectHome=yes → blocks access to /home.
      • PrivateTmp=yes → gives the service its own /tmp.
      • NoNewPrivileges=yes → prevents privilege escalation.
  • Gotchas:

    • No permanent account entry → you can’t su or ssh into it.
    • UIDs are reused; you can’t rely on the number being the same between runs.
    • If the service needs to write to disk, you must use the StateDirectory/CacheDirectory approach, or files will be inaccessible.


Rootless Containers & Subuids

Normally, containers run as root, which maps directly to the host’s root — a big risk if the container is compromised. Rootless containers avoid this by mapping container UIDs/GIDs to high-numbered “subuids” and “subgids” on the host.

That way, root inside the container is really just UID 100000+ on the host — isolated, unprivileged, and unable to harm the real system.

👀 Show commands & notes
# === Show subuid/subgid allocation ===
grep alice /etc/subuid /etc/subgid
# alice:100000:65536
# means: user 'alice' gets a block of 65,536 UIDs/GIDs starting at 100000.

# === Run a rootless container ===
podman run --rm alpine id
uid=0(root) gid=0(root) groups=0(root)

# From inside the container it looks like root,
# but on the host it's actually UID 100000+ from /etc/subuid.

# === Inspect namespace mapping ===
podman unshare cat /proc/self/uid_map
# 0 100000 65536
# "container UID 0 maps to host UID 100000, size 65536"

Notes:

  • How it works:

    • /etc/subuid and /etc/subgid allocate “ranges” of host IDs to a user.
    • When a rootless container starts, container UID 0 → host UID 100000, container UID 1 → host UID 100001, etc.
    • This mapping isolates container processes from the host.
  • Why it matters:

    • Running as root in a container no longer equals root on the host.
    • Even if the container is compromised, the attacker only controls high-numbered, unprivileged UIDs.
    • This is how tools like Podman, Buildah, and rootless Docker enforce least privilege.
  • Gotchas:

    • Without entries in /etc/subuid and /etc/subgid, rootless containers fail to start.
    • Each user gets ~65k IDs by default; this can be adjusted in /etc/subuid.
    • Requires user namespaces in the kernel (CONFIG_USER_NS=y).
    • Files created by container processes will show up on the host as UID 100000+, which can look odd in ls -l.
  • Related commands:

    • podman unshare → enter the container’s user namespace for debugging.
    • newuidmap / newgidmap → helper programs to set up ID ranges.


Password Policy & Lockouts

User accounts aren’t just about existence — they also have lifespans and safety rules. Password policy defines how often a user must change their password, how complex it must be, and how many failed logins before the account locks.

This is enforced through shadow file aging fields (Linux/Unix), PAM modules for lockouts, and platform-specific tools on macOS and BSD.

👀 Show commands & notes
# === Linux: show password aging/expiry ===
chage -l alice
# Last password change                                    : Sep 21, 2025
# Password expires                                       : Nov 20, 2025
# Password inactive                                      : never
# Account expires                                        : never
# Minimum number of days between password change         : 0
# Maximum number of days between password change         : 60
# Number of days of warning before password expires      : 7

# === Linux: check failed logins ===
faillock --user alice
# alice:
# When        Type  Source
# 2025-09-21  TTY   ssh:notty
# 2025-09-21  TTY   ssh:notty

# Lock out after 3 failures (RHEL/Ubuntu with pam_faillock)
sudo faillock --setdeny=3

# === BSD: enforce minimum password age ===
passwd -n 30 alice     # must wait 30 days before changing again

# === macOS: show password policy ===
pwpolicy -u alice -getpolicy
# prints dictionary of rules:
# usingHistory=15 minChars=8 requiresMixedCase=1 requiresNumeric=1

# Set stricter policy on macOS
sudo pwpolicy -u alice -setpolicy "minChars=12 requiresMixedCase=1 requiresNumeric=1 requiresSymbol=1"

Notes:

  • Why it matters:

    • Password aging prevents accounts from using the same password forever.
    • Lockouts protect against brute force attacks but can cause accidental denial of service.
    • Compliance frameworks (HIPAA, PCI-DSS, etc.) often mandate specific expiry and complexity rules.
  • Linux specifics:

    • chage edits shadow file fields directly.
    • faillock (PAM module) counts failed attempts and locks accounts temporarily.
    • Debian historically used pam_tally2, but newer distros prefer faillock.
    • Lockouts can be reset: faillock --user alice --reset.
  • BSD specifics:

    • passwd options enforce password minimum/maximum ages.
    • Some BSDs use login.conf for global policy.
  • macOS specifics:

    • pwpolicy manages per-user or global rules.
    • Many system accounts (like _spotlight) are exempt.
    • SecureToken: separate from password policy, it controls FileVault unlock ability. Losing SecureToken can lock a user out of disk encryption.
  • Gotchas:

    • Expired accounts often just show as “login incorrect” with no obvious hint. Always check chage -l.
    • Too strict a lockout policy can become a DoS if an attacker keeps intentionally failing logins.
    • Remote directory systems (LDAP/AD) often enforce their own policies that override local rules.


Audit & Logs

When a user can’t log in, sudo fails, or permissions seem wrong, the logs tell the story. Different Unix-like systems store them in different places, but the principles are the same: check authentication logs, check system journals, and look at login history.

👀 Show commands & notes
# === Debian/Ubuntu: SSH & sudo events ===
tail -f /var/log/auth.log
# Sep 21 17:40 server sshd[2345]: Failed password for bob from 192.168.1.20 port 55312 ssh2
# Sep 21 17:42 server sudo:   alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/bin/ls

# === RHEL/Fedora equivalents ===
tail -f /var/log/secure

# === systemd journal: filter by UID ===
journalctl _UID=1000 --since today
# shows all messages generated by UID 1000 (alice)

# === systemd journal: filter by command ===
journalctl _COMM=sudo -S today
# shows all sudo invocations since today

# === Login history: successful ===
last
# alice   pts/0        192.168.1.20     Sun Sep 21 17:00   still logged in

# === Login history: failed ===
lastb
# bob     ssh:notty    192.168.1.20     Sun Sep 21 17:40   still failed login

Notes:

  • File locations:

    • Debian/Ubuntu → /var/log/auth.log.
    • RHEL/Fedora → /var/log/secure.
    • BSD → /var/log/auth.log or /var/log/messages depending on config.
    • macOS → /var/log/asl/ (older) or log show --predicate 'eventMessage contains "sshd"'.
  • systemd journal tips:

    • _UID=1000 → filter logs from a specific user ID.
    • _COMM=sudo → filter by executable name.
    • -S yesterday / --since "2025-09-20 18:00" → time filters.
    • Enable persistence:
      sudo mkdir -p /var/log/journal
      sudo systemctl restart systemd-journald
      
  • Login history:

    • last reads /var/log/wtmp → shows successful logins.
    • lastb reads /var/log/btmp → shows failed logins (may need root to read).
    • Use last -f /path/to/wtmp.old to read rotated logs.
  • Why this matters:

    • Failed logins reveal brute-force attempts.
    • sudo log entries show exactly which commands were run and by whom.
    • Filtering by UID is useful for tracing a specific account across the system.
  • Gotchas:

    • Journald defaults to in-memory logs; without persistence, entries vanish after reboot.
    • Log rotation may remove history faster than expected (/etc/logrotate.d/).
    • lastb output can flood if you’re under SSH brute-force attack; use grep to filter by username.


SSH Key Restrictions

SSH public keys don’t just allow or deny login — you can control what they’re allowed to do. This is especially useful for automation accounts (backups, deploy scripts, CI/CD) where you don’t want full shell access.

Restrictions are written in ~/.ssh/authorized_keys before the key itself. Multiple restrictions can be combined with commas.

👀 Show config & notes
# === Restrict login to a specific subnet ===
from="192.168.1.0/24" ssh-ed25519 AAAAC3Nza...

# === Force a command (ignore user input) ===
command="/usr/local/bin/backup.sh" ssh-ed25519 AAAAC3Nza...
# When this key logs in, it *always* runs backup.sh — no shell access.

# === Disable shell/TTY allocation ===
no-pty,command="/usr/bin/rsync --server --sender ..." ssh-ed25519 AAAAC3Nza...
# Useful for file transfers only.

# === Combine multiple restrictions ===
from="10.0.0.5",no-pty,command="/usr/local/bin/deploy.sh" ssh-ed25519 AAAAC3Nza...
# Only works from 10.0.0.5, no interactive shell, forced deploy script.

# === Log key usage for auditing ===
environment="DEPLOY_KEY_ID=ci-runner" ssh-ed25519 AAAAC3Nza...
# Adds DEPLOY_KEY_ID to environment for logging in scripts.

Notes:

  • Why do this?

    • Automation accounts (backups, deployments, monitoring) don’t need full shell access.
    • Restricting keys reduces the blast radius if a key leaks.
  • Common options:

    • from="addrlist" → restrict to specific IPs or subnets.
    • command="cmd" → always run this command instead of a shell.
    • no-pty → disables interactive sessions.
    • environment="VAR=value" → injects env vars, useful for logging or scripts.
    • restrict (newer OpenSSH) → a safe default that implies multiple restrictions (no port forwarding, no agent, no PTY).
  • Extra hardening:

    • Combine with Match User or Match Address blocks in sshd_config.
    • Example:
      Match User backup
          ChrootDirectory /backups
          ForceCommand /usr/local/bin/backup.sh
      
  • Gotchas:

    • Syntax is strict — options must come before the key, separated by commas.
    • A single typo can prevent login.
    • Forced commands must use absolute paths.
    • Debug failures with sshd -T (shows effective config) and ssh -vvv user@host.


Other OS-Specific Nuggets

Not every Unix-like does user management the same way. Here are a few platform-specific details that matter when you step outside Linux.

👀 Show commands & notes
# === macOS: FileVault & SecureToken ===
# Check if a user has SecureToken (needed to unlock FileVault at boot)
sysadminctl -secureTokenStatus alice

# Grant FileVault unlock rights to an existing account
sudo fdesetup add -usertoadd alice

# List all FileVault-enabled users
fdesetup list

# === BSD: doas instead of sudo ===
# Install and configure a simple allow rule for wheel group
echo "permit :wheel" | sudo tee /usr/local/etc/doas.conf

# Test it
doas whoami
# root

# === Linux/systemd: user sessions ===
# List all active systemd user sessions
loginctl list-users

# Show details for a single user session
loginctl user-status alice

Notes:

  • macOS:

    • SecureToken is a flag that determines whether a user can unlock FileVault at boot.
    • Creating a new admin user does not automatically give it SecureToken. You may need to explicitly grant it via another SecureToken user.
    • fdesetup controls FileVault enrollment, token grants, and recovery keys.
  • BSD (doas):

    • doas is OpenBSD’s minimalist alternative to sudo.
    • Config lives in /etc/doas.conf or /usr/local/etc/doas.conf.
    • Syntax is simple:
      permit :wheel
      permit nopass keepenv alice as root cmd /usr/bin/pkg_add
      
    • Security philosophy: fewer moving parts, less chance of misconfiguration.
  • Linux/systemd (loginctl):

    • loginctl shows how users map to systemd sessions. Useful for debugging lingering sessions, linger (allowing services to keep running after logout), and session limits.
    • Example:
      loginctl enable-linger alice   # keep user services running after logout
      
    • Helpful when running background daemons or timers under a regular user account.
  • General tip:

    • When troubleshooting across OSes, always check “what’s the local identity backend?”
      • macOS → Directory Services (dscl, sysadminctl).
      • BSD → pw / doas.
      • Linux → systemd-logind, NSS, SSSD.


pixel art image of a party of fantasy adventurers happily gathered around a treasure chest
If you made it this far you've earned some loot and XP!

Core Resources

macOS-Specific Resources

History & Lore


Conclusion

Every file belongs to someone. Every process runs as someone.
Unix enforces those boundaries with precision.

User and group management isn’t glamorous, but it’s foundational.
Once you can inspect, create, modify, and delete accounts across platforms,
you hold the keys to the kingdom.

Drop me a line if you found this guide useful or if I missed something:
feedback@adminjitsu.com

Happy administering ✨