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.
Unix was built around the idea of a multiuser, shared environment. 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.
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” is just 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
0is 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
aliceat a login prompt, the system maps that to UID1000(or whatever number was assigned).
The kernel doesn’t care 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 like 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 systems were the norm.
- 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, andmysqlgot 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 typically 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 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 a local slice of it.
Think of these files as the bones of an identity. The commands you run (like 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, plussmeaning 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 (
son user perms): program runs with file owner’s UID. Example:/usr/bin/passwdruns as root. - setgid (
son group perms): files created in this directory inherit the group; executables run with group’s GID. - sticky bit (
ton others perms): on directories, only file owners can delete their files./tmpuses 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
bashandvimprocesses. - 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 the limited 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 will make more 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 hopefully 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.
Details
# === 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 groupNotes:
- Prefer
id -nGandgetentin scripts (NSS-aware). /etc/passwdshows local users only, which may miss LDAP/AD accounts.- On macOS,
dsclis the source of truth.
Create Users
Create human accounts or service accounts.
Details
# === 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 passwordNotes:
adduser(Debian) is friendlier thanuseradd(RHEL).- Always use
-mwithuseraddto ensure a home directory is created. - On macOS,
sysadminctlis preferred, butdsclgives more fine-grained control.
Modify Users
Change passwords, shells, groups, or lock accounts.
Details
# === 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 # only works if shell is *currently* `/bin/bash`
dscl . -append /Groups/admin GroupMembership aliceNotes:
- ⚠️
usermod -Gwithout-areplaces all groups. - Locking prepends
!to the shadow password field. - macOS uses
dsclto edit user attributes and groups.
Delete Users
Remove accounts and clean up files.
Details
# === 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/aliceNotes:
- Orphaned files will show as owned by a numeric UID.
- Always search for
-nouserfiles after deletion. - On macOS,
sysadminctlhandles home directory cleanup.
Manage Groups
Groups define shared access.
Details
# === 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/devsNotes:
- 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.
Details
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, andtalkare often disabled on
modern systems, andfingeris usually missing entirely due to security
concerns. - The
fingercommand would also display a user’s~/.planfile — a personal
status note people used for anything from office hours to quirky quotes.
In the early internet,.planfiles 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.
Details
# === 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: ALLNotes:
- 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.
Details
# === 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 credentialsNotes:
- If
/etc/passwdshows/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.
Advanced Topics
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.
Details
# === 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 60000Notes:
-
How
/etc/skelworks:- Any file or directory in
/etc/skelgets copied into the new user’s home when the account is created (unless disabled withuseradd -M -k). - Typical contents:
.bashrc,.profile,.bash_logout. - You can also include a company-wide
README, awelcome.txt, or even preconfigured dotfiles like.vimrcor.gitconfig. - Updating
/etc/skelonly affects future accounts, not existing ones.
- Any file or directory in
-
How
umaskworks:- 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
0002so teams in the same group can share files easily. - Security-sensitive environments often use
0077to prevent accidental leakage.
- Servers often use
-
Other considerations:
umaskcan differ between shells, cron jobs, and systemd services.- Linux: defaults can be set in
/etc/login.defsor 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.
Details
# === 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
getentmatters:getentqueries the entire NSS stack — so LDAP, AD, or other remote backends are included.cat /etc/passwdonly 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+sssdis 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.conffirst — if “sss” or “ldap” isn’t listed forpasswdandgroup, your system won’t even try querying the domain.
- Always check
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.
Details
# === 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.txtNotes:
-
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 -leandchmod +a. - The
+sign inls -loutput means “this file has ACLs.”
- Linux & BSD:
-
Gotchas:
- Not all filesystems support ACLs (may need
mount -o aclon ext4). - Backups that don’t preserve extended attributes may strip ACLs.
- ACLs can make permissions confusing — always use
getfaclto confirm.
- Not all filesystems support ACLs (may need
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.
Details
# === 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:
pinghad full root rights — if exploited, attacker gets root. - With capabilities: attacker only gains the one granted privilege.
- Old way:
-
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. Usersync -aXorinstall -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.
- Capabilities are stored as extended attributes — they don’t survive a normal
-
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.
Details
# /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=yesNotes:
-
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.
- Each start assigns a UID like
-
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=, orLogsDirectory=in the unit file to create system-managed dirs with correct ownership. - Example:
→ systemd creates
StateDirectory=mydaemon/var/lib/mydaemon/owned by the dynamic UID.
- Use
-
Security hardening:
- Combine
DynamicUser=yeswith:ProtectSystem=strict→ service sees/usras read-only.ProtectHome=yes→ blocks access to/home.PrivateTmp=yes→ gives the service its own/tmp.NoNewPrivileges=yes→ prevents privilege escalation.
- Combine
-
Gotchas:
- No permanent account entry → you can’t
suorsshinto 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/CacheDirectoryapproach, or files will be inaccessible.
- No permanent account entry → you can’t
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.
Details
# === 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/subuidand/etc/subgidallocate “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/subuidand/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.
- Without entries in
-
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 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.
Details
# === 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:
chageedits shadow file fields directly.faillock(PAM module) counts failed attempts and locks accounts temporarily.- Debian historically used
pam_tally2, but newer distros preferfaillock. - Lockouts can be reset:
faillock --user alice --reset.
-
BSD specifics:
passwdoptions enforce password minimum/maximum ages.- Some BSDs use
login.conffor global policy.
-
macOS specifics:
pwpolicymanages 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.
- Expired accounts often just show as “login incorrect” with no obvious hint. Always check
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.
Details
# === 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
# === Login history: failed ===
lastb
# bob ssh:notty 192.168.1.20 Sun Sep 21 17:40 Notes:
-
File locations:
- Debian/Ubuntu →
/var/log/auth.log. - RHEL/Fedora →
/var/log/secure. - BSD →
/var/log/auth.logor/var/log/messagesdepending on config. - macOS →
/var/log/asl/(older) orlog show --predicate 'eventMessage contains "sshd"'.
- Debian/Ubuntu →
-
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:
lastreads/var/log/wtmp→ shows successful logins.lastbreads/var/log/btmp→ shows failed logins (may need root to read).- Use
last -f /path/to/wtmp.oldto read rotated logs.
-
Why this matters:
- Failed logins reveal brute-force attempts.
sudolog 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/). lastboutput can flood if you’re under SSH brute-force attack; usegrepto 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.
Details
# === 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 UserorMatch Addressblocks insshd_config. - Example:
Match User backup ChrootDirectory /backups ForceCommand /usr/local/bin/backup.sh
- Combine with
-
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) andssh -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.
Details
# === 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 aliceNotes:
-
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.
fdesetupcontrols FileVault enrollment, token grants, and recovery keys.
-
BSD (
doas):doasis OpenBSD’s minimalist alternative tosudo.- Config lives in
/etc/doas.confor/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):loginctlshows 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.
- macOS → Directory Services (
- When troubleshooting across OSes, always check “what’s the local identity backend?”
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.
Email me if you found this guide useful or if I missed something:
feedback@adminjitsu.com
Happy administering