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 asroot
, 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 UID1000
(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 likemail
,www-data
, andmysql
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).

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, pluss
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
andvim
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.

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.
Notes:👀 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
id -nG
and getent
in scripts (NSS-aware)./etc/passwd
shows local users only, which may miss LDAP/AD accounts.dscl
is the source of truth.
Create Users
Create human accounts or service accounts.
Notes:👀 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
adduser
(Debian) is friendlier than useradd
(RHEL).-m
with useradd
to ensure a home directory is created.sysadminctl
is preferred, but dscl
gives more fine-grained control.
Modify Users
Change passwords, shells, groups, or lock accounts.
Notes:👀 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
usermod -G
without -a
replaces all groups.!
to the shadow password field.dscl
to edit user attributes and groups.
Delete Users
Remove accounts and clean up files.
Notes:👀 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
-nouser
files after deletion.sysadminctl
handles home directory cleanup.
Manage Groups
Groups define shared access.
Notes:👀 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
sudo
(Ubuntu), wheel
(RHEL/BSD), admin
(macOS).chmod g+s
) makes files inherit group ownership.
Classic Multiuser Tools
Old-school, but still around.
Notes:👀 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)
machine often served an entire lab or office.
spotting a forgotten session or checking login history).write
, wall
, and talk
are often disabled on
modern systems, and finger
is usually missing entirely due to security
concerns.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.
Notes: 🔗 Docs & References:👀 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
/etc/sudoers.d/
instead of cluttering /etc/sudoers
.which systemctl
).%group
) to manage privileges cleanly for teams.sudo visudo -c
.
Troubleshooting
When a user can’t log in, can’t write files, or sudo
mysteriously fails, these checks will save you.
Notes:👀 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
/etc/passwd
shows /usr/sbin/nologin
, /sbin/nologin
, or /bin/false
, the user cannot log in interactively.passwd -S
, chage -l
).id -nG
) when file access doesn’t make sense._
and are not intended for login.

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.
Notes: How How Other considerations:👀 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
/etc/skel
works:
/etc/skel
gets copied into the new user’s home when the account is created (unless disabled with useradd -M -k
)..bashrc
, .profile
, .bash_logout
.README
, a welcome.txt
, or even preconfigured dotfiles like .vimrc
or .gitconfig
./etc/skel
only affects future accounts, not existing ones.umask
works:
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.
0022
(safe default).0002
so teams in the same group can share files easily.0077
to prevent accidental leakage.
umask
can differ between shells, cron jobs, and systemd services./etc/login.defs
or PAM config./etc/adduser.conf
.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.
Notes: NSS vs PAM: Why SSSD & caching: Joining domains: Troubleshooting tip:👀 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=...
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.
sss_cache -E
realmd
+ sssd
is the common modern combo (RHEL, Fedora, Ubuntu).dsconfigad
).nss_ldap
+ pam_ldap
.
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.
Notes: Why ACLs? Default ACLs: Implementation differences: Gotchas:👀 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
devs
.
setfacl
, getfacl
.ls -le
and chmod +a
.+
sign in ls -l
output means “this file has ACLs.”
mount -o acl
on ext4).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.
Notes: Safer than setuid: How capabilities are assigned: Common useful capabilities: Gotchas: Why care?👀 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)
ping
had full root rights — if exploited, attacker gets root.
setcap cap_name+ep file
→ give a binary capability (e=effective
, p=permitted
).getcap file
→ check capabilities.capsh --print
→ view current shell’s 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”).
cp
. Use rsync -aX
or install -m755 -o root -g root
.CAP_SYS_ADMIN
) defeats the purpose.
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.
Notes: How it works: When to use: Persistent data: Security hardening: Gotchas:👀 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
dynamic-1000
./etc/passwd
; it’s entirely managed by systemd.
StateDirectory=
, CacheDirectory=
, or LogsDirectory=
in the unit file to create system-managed dirs with correct ownership.StateDirectory=mydaemon
/var/lib/mydaemon/
owned by the dynamic UID.
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.
su
or ssh
into it.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.
Notes: How it works: Why it matters: Gotchas: Related commands:👀 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"
/etc/subuid
and /etc/subgid
allocate “ranges” of host IDs to a user.
/etc/subuid
and /etc/subgid
, rootless containers fail to start./etc/subuid
.CONFIG_USER_NS=y
).ls -l
.
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.
Notes: Why it matters: Linux specifics: BSD specifics: macOS specifics: Gotchas:👀 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"
chage
edits shadow file fields directly.faillock
(PAM module) counts failed attempts and locks accounts temporarily.pam_tally2
, but newer distros prefer faillock
.faillock --user alice --reset
.
passwd
options enforce password minimum/maximum ages.login.conf
for global policy.
pwpolicy
manages per-user or global rules._spotlight
) are exempt.
chage -l
.
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.
Notes: File locations: systemd journal tips: Login history: Why this matters: Gotchas:👀 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
/var/log/auth.log
./var/log/secure
./var/log/auth.log
or /var/log/messages
depending on config./var/log/asl/
(older) or log show --predicate 'eventMessage contains "sshd"'
.
_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.sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
last
reads /var/log/wtmp
→ shows successful logins.lastb
reads /var/log/btmp
→ shows failed logins (may need root to read).last -f /path/to/wtmp.old
to read rotated logs.
sudo
log entries show exactly which commands were run and by whom.
/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.
Notes: Why do this? Common options: Extra hardening: Gotchas:👀 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.
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).
Match User
or Match Address
blocks in sshd_config
.Match User backup
ChrootDirectory /backups
ForceCommand /usr/local/bin/backup.sh
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.
Notes: macOS: BSD ( Linux/systemd ( General tip:👀 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
fdesetup
controls FileVault enrollment, token grants, and recovery keys.doas
):
doas
is OpenBSD’s minimalist alternative to sudo
./etc/doas.conf
or /usr/local/etc/doas.conf
.permit :wheel
permit nopass keepenv alice as root cmd /usr/bin/pkg_add
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.loginctl enable-linger alice # keep user services running after logout
dscl
, sysadminctl
).pw
/ doas
.systemd-logind
, NSS, SSSD.

Links & Stuff
Core Resources
- man 5 passwd, man 5 shadow, man 5 group, man 5 sudoers
- Linux User Management Guide (Arch Wiki, applies broadly)
- FreeBSD Handbook: Users and Basic Account Management
macOS-Specific Resources
- Add a user or group on Mac - Apple Support
- Change Users & Groups settings on Mac - Apple Support
- dscl Man Page - SS64.com — comprehensive dscl reference
- dscl Examples - University of Utah — practical dscl usage
- Additional Features - Apple Developer Archive — includes user creation examples
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 ✨