Chapter 2 — Shell, files, permissions, users, sudo
Portable shell baseline versus Bash extensions
Teach and write the POSIX baseline first, then label extensions. POSIX Issue 8 (IEEE Std 1003.1-2024) is the reference for portable sh syntax and utilities (POSIX Shell & Utilities). Reliable orientation commands: pwd, printf '%s\n' "$PWD", ls -la, cd -- /path, cd .., command -v tool, type tool, man command, command --help. cd is a shell builtin because it must change the calling shell's working directory; a cd in a child process cannot move the parent shell (Bash Reference Manual: What is a shell?).
The POSIX shell does not promise Bash arrays, [[ ... ]], process substitution <(...), brace expansion, or set -o pipefail. Use #!/bin/sh only for portable syntax; use #!/usr/bin/env bash when the script requires Bash features, and say so (Bash Reference Manual: Bash and POSIX). Also, a pipeline's exit status is normally that of the final command; set -o pipefail makes Bash report a failing component — it is Bash-specific (Bash Reference Manual: Pipelines). Avoid teaching set -e as a complete error-handling strategy: its behavior has conditional/list/pipeline exceptions, so check statuses intentionally and test scripts.
Quoting and filename discipline. Double quotes prevent word splitting and pathname expansion of a variable's contents; single quotes preserve literal characters (Bash Reference Manual: Quoting). Quote expansions and pathnames, and use -- with supporting utilities when a filename could begin with - (GNU Coreutils: Common options):
# [POSIX sh] [READ-ONLY] preview, then copy
printf '<%s>\n' "$source" "$destination" # inspect what will be touched
cp -- "$source" "$destination"
For scripts that must handle arbitrary filenames, do not parse ls output or use whitespace-delimited for f in $(...). POSIX find ... -exec command {} + avoids filename splitting (POSIX find); GNU find/xargs additionally support NUL-delimited pipelines — label find -print0 | xargs -0 as a GNU extension, not POSIX (GNU Coreutils: Target directory examples).
The permission model, and why "open" files still fail
Start every permission question with inspection: ls -ld -- path, stat -- path, namei -l /path/to/file (where installed), getfacl -p path, and findmnt -T path (GNU Coreutils: File permissions; getfacl(1)). The classic mode is owner/group/other read-write-execute bits, with one subtlety worth memorizing: on a directory, execute means search/traverse, not run a program — and a file needs search permission on every parent directory to be openable at all (GNU Coreutils: File permissions). When permission behavior is surprising, inspect each path component, then the ACL mask, then the MAC layer (below).
Prefer narrow symbolic changes; numeric modes are concise but replace the complete mode (chmod 0640 file). umask 027 limits permissions on newly created files and directories — it repairs nothing that already exists (GNU Coreutils: chmod; GNU Coreutils: Umask). A common private-file pattern is install -m 0600 /dev/null /etc/example.conf, followed by editing and validation.
Ownership: chown user:group file and chgrp group file; changing owner can clear set-user-ID/set-group-ID bits on Linux (GNU Coreutils: chown). For shared project directories, a deliberate pattern is chgrp project /srv/project && chmod 2770 /srv/project — the directory setgid bit makes new entries inherit the directory group, subject to filesystem and application behavior (GNU Coreutils: Directory setgid).
POSIX ACLs, when installed and supported by the filesystem:
# [READ-ONLY]
getfacl -p file
# [MUTATING] add named-user and default-directory entries, then verify
setfacl -m u:alice:rw file
setfacl -m d:g:project:rwx /srv/project
getfacl -p file
ACL masks can restrict the effective rights of named users/groups and the group-class mode bits — check the effective: annotations from getfacl, not only the named entries (getfacl(1); setfacl(1)).
HIGH-RISK · MUTATING · root — recursive permission/ownership changes. Broad
chmod -R,chown -R, ACL removal, or recursive SELinux relabel can expose secrets, break services, alter container volume access, and is difficult to undo exactly (operations notes). What it changes: owner/mode/ACL of every reachable path under the root. What can be lost: executable distinctions, private-data protection, special files, service access. Preflight: inventory current owner/mode/ACL/context; scope to the smallest path; preview affected paths withfind; constrain the exact root. Rollback: saved prior state (getfacl -R,stat) or a tested configuration-management change. Success check: the intended principal can access the target, and a service that depends on it still works. GNUchmod/chownprovide--preserve-rootas an additional guard for recursive operations on/(GNU Coreutils: Treating / specially; GNU Coreutils: Traversing symlinks).chmod -R 777is never the fix.
Permissions are only one layer. SELinux labels/policy on Fedora/RHEL-derived systems and AppArmor profiles on Ubuntu may deny access despite permissive Unix mode bits; inspect audit/service logs and labels before weakening security controls (Chapter 7) (Fedora SELinux: getting started; Ubuntu Server AppArmor).
Users, groups, and identity lookup
Read account and group data through NSS with getent passwd NAME, getent group GROUP, and inspect the current identity with id; do not assume /etc/passwd is the only source when LDAP/SSSD/NIS or another identity service may be configured (getent(1); GNU Coreutils: id).
# [MUTATING · root] create an admin user — group name differs by family
sudo useradd --create-home alice # ([useradd(8)](https://man7.org/linux/man-pages/man8/useradd.8.html))
sudo passwd alice
sudo usermod --append --groups wheel alice # Fedora/RHEL/AlmaLinux
# sudo usermod --append --groups sudo alice # Ubuntu/Debian
id alice # verify — ([usermod(8)](https://man7.org/linux/man-pages/man8/usermod.8.html))
MUTATING trap —
usermod -Gwithout-a.-Galone replaces the user's supplementary group list and can remove existing access; the safe add operation is-aG(usermod(8)). New supplementary groups also only take effect in a fresh login session. User-creation defaults (home, groups, UID ranges, shell) vary by distro/configuration — inspectuseradd --defaults/local policy and verify the resulting account rather than assuming defaults (useradd(8)).
sudo: elevate one command, not a shell session
Use sudo -l to list your privileges, sudo -v to validate/refresh credentials, and sudo command ... to elevate only the intended command; use sudoedit /etc/example.conf for safe editing instead of a persistent root shell (Sudo manual). Edit sudo policy only with visudo or visudo -f /etc/sudoers.d/NAME (syntax validation + locking), then visudo -c; keep drop-ins root-owned and mode 0440 where the platform requires it (Sudoers manual; visudo manual).
HIGH-RISK — direct sudoers edits and broad grants. A sudoers syntax error can prevent privilege escalation entirely; keep an existing privileged session open until a new
sudocheck succeeds. Avoid unrestrictedNOPASSWD: ALLand broad command wildcards: an allowed editor, interpreter, or package manager can often execute arbitrary commands, so command rules are not a substitute for understanding the grant (Sudoers manual).
The classic redirection trap. sudo echo text > /root/file writes unprivileged, because the calling shell performs > redirection before sudo runs — shell parsing, not sudo, defines execution order (Bash Reference Manual: Redirections; Sudo manual). Use sudoedit, or a deliberate printf '%s\n' ... | sudo tee /etc/file after checking target and content, and verify the resulting owner/mode.