Sysadmin commands, LVM walkthroughs, and SELinux troubleshooting drills while commuting or working out. New episodes covering RHCSA EX200 objectives drop weekly.
About the exam
Why earn the RHCSA?
EX200 is the floor cert for serious Linux work. It's a fully hands-on, performance-based exam — you sit in front of two RHEL 9 VMs in a kiosk and solve real tasks. No multiple choice, no notes, no internet. Passing it proves you can actually run a Linux system.
- Hands-on, performance-based — the certification that actually proves CLI fluency, not memorisation
- Foundation for the entire Red Hat ladder: RHCE EX294 (Ansible), RHCA Specialist tracks, and OpenShift certifications
- Prerequisite or fast-track signal for many SRE, platform-engineer, and Linux-ops roles in Red Hat-aligned shops
- Validates real CLI skill:
nmcli,firewalld,semanage, LVM, systemd — not abstract architecture - Performance-based format means no question banks to memorise — the test is the work itself
- Strong salary signal — ~€55–80k starting in EU markets, $90–110k median in the US for RHCSA-certified Linux engineers
mount -a and a reboot — broken persistent config is the #1 way candidates lose points. SELinux must stay enforcing.
Exam blueprint
RHCSA exam objectives
Red Hat doesn't publish explicit percentages, but task-cluster weight on past exams concentrates heavily on storage, users + permissions + SELinux, and systemd. Containers is the newest objective and growing.
Course content
11 modules · ~35 hours
Each module maps to a cluster of RHCSA objectives. Work through them in order, or jump to the area where you're weakest. Every lesson ends with a mini-quiz CTA so you can drill recall before moving on.
Essential Linux Commands3 lessons
The CLI is the entire surface of the RHCSA. Every minute of the exam is spent typing, so shell fluency directly translates into points. Master Bash quoting and redirection, file navigation with find and locate, archives with tar and star, and the text-processing trio grep/sed/awk. Build muscle memory on tab completion, history search, and stream redirection until you don't think about them.
📖 Read in-depth chapter ▾
Ctrl+R history search, &> file redirection, and the grep / sed / awk trio. Master find with combined criteria, tar with the right compression flag, and the hard-vs-soft-link distinction cold — these are reflex skills the timer rewards.
Every RHCSA task is typed into a Bash prompt. The exam rewards speed: tab completion, history search, and clean stream redirection are the difference between finishing and timing out.
- Bash shell fundamentals: Bash is the default shell on RHEL. Commands follow the structure
command [options] [arguments]. Useman commandfor documentation,info commandfor detailed manuals, andcommand --helpfor quick usage summaries. - Command history & editing: Use the up/down arrows to cycle through history,
historyto list all previous commands,!nto re-run command number n, andCtrl+Rto reverse-search history. Tab completion finishes commands, file names, and paths automatically. - Input/output redirection:
>redirects stdout and overwrites the file,>>appends.2>redirects stderr,&>redirects both stdout and stderr.<feeds a file as stdin. These operators are essential for scripting and log capture. - Piping & command chaining: The pipe
|sends stdout of one command as stdin to the next (e.g.,ps aux | grep httpd). Chain commands with&&(run next only if previous succeeds),||(run next only if previous fails), or;(run sequentially regardless). - Shell variables & environment: Set variables with
VAR=value, export them withexport VAR. Key environment variables includePATH,HOME,USER,SHELL, andPS1. Useenvorprintenvto list all environment variables.
Task: capture both stdout and stderr from a long-running dnf update while watching the output live. Solution: tee with combined streams — dnf -y update 2>&1 | tee /var/log/dnf-update.log. The 2>&1 merges stderr into stdout before the pipe; tee writes to file and stdout simultaneously. Verify the log with tail -f /var/log/dnf-update.log in another session. For a quick one-shot capture without live view, use dnf -y update &> /var/log/dnf-update.log.
Ctrl+R history search, and &> file.log redirection — they save minutes per task.
File operations are the spine of every exam task. The exam tests find with combined criteria, tar with the right compression flag, and the difference between hard and soft links cold.
- Core file operations:
ls -lalists files with permissions and hidden entries.cp -rcopies directories recursively.mvmoves or renames.rm -rfremoves recursively and forcefully.mkdir -pcreates nested directories in one command. - Finding files:
find / -name "*.conf" -type fsearches the filesystem in real time by name, type, size, permissions, or modification time.locate filenamequeries a pre-built database (updated viaupdatedb) for faster but potentially stale results. - Archiving & compression:
tar -czf archive.tar.gz /pathcreates a gzip-compressed archive.tar -xzf archive.tar.gzextracts it.tar -cjfuses bzip2,tar -cJfuses xz.staris an alternative archiver that preserves extended attributes and SELinux contexts. - Hard & soft links: Hard links (
ln file link) share the same inode and data blocks — deleting the original does not affect the link. Soft links (ln -s target link) are pointers to a path — if the target is deleted, the symlink breaks. Hard links cannot cross filesystems or link to directories. - File metadata:
stat fileshows inode number, size, permissions, timestamps (access, modify, change).file filenameidentifies file type.du -sh /pathshows disk usage for a directory, anddf -hshows filesystem disk space usage.
Task: find every .conf file under /etc owned by root, larger than 1KB, modified in the last 7 days, then archive them to /root/etc-backup.tar.gz preserving SELinux contexts. Solution: combine find with star — find /etc -name "*.conf" -type f -user root -size +1k -mtime -7 > /tmp/list.txt, then star -c -xattr -H=exustar -f /root/etc-backup.tar.gz list=/tmp/list.txt. Verify contexts survived with star -tv -xattr -f /root/etc-backup.tar.gz | head.
find with combined criteria for selection, star when SELinux contexts must survive an archive round-trip. Hard links don't cross filesystems; soft links break when the target moves.
Many RHCSA tasks reduce to "extract this from a config, transform it, write it back". The exam tests grep patterns, sed in-place edits, and awk field extraction often enough that hesitation costs minutes.
- Grep & regular expressions:
grep -i pattern filesearches case-insensitively.grep -r pattern /dirsearches recursively.grep -E "regex"enables extended regex. Basic patterns:^(start of line),$(end of line),.*(any characters),[a-z](character class). - Sed & awk:
sed 's/old/new/g' fileperforms global search-and-replace.sed -iedits files in place.awk '{print $1, $3}' fileextracts specific fields from structured text. Both are essential for automated configuration changes in scripts. - Text filtering tools:
cut -d: -f1 /etc/passwdextracts the first field (usernames).sortorders lines alphabetically or numerically (-n).uniqremoves adjacent duplicates (pipe fromsortfirst).wc -lcounts lines in a file. - File viewing:
head -n 20 fileshows the first 20 lines.tail -n 20 fileshows the last 20.tail -f /var/log/messagesfollows a log file in real time.lessprovides paginated viewing with search (press/patternto search forward). - Comparing files:
diff file1 file2shows line-by-line differences.diff -uproduces unified format output commonly used in patches.commcompares two sorted files and shows lines unique to each or common to both.
Task: from /etc/passwd, list usernames with UID ≥ 1000 (regular users), sorted alphabetically. Solution: awk + sort — awk -F: '$3 >= 1000 {print $1}' /etc/passwd | sort. Variant: count them with ... | wc -l. To make a backup-then-edit pattern stick, sed -i.bak 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config — the .bak suffix writes a safety copy before mutating.
grep -v "^#" | grep -v "^$" to strip comments and blanks; sed -i.bak for safe in-place edits; awk -F: '{print $1}' for fast field extraction.
awk -F: '$3>=1000 {print $1":"$6":"$7}' /etc/passwd — list real users with their home + shell; (2) pipe through awk -F: '$3=="" || $3=="/sbin/nologin" {print $1}' to flag dead accounts; (3) for each flagged user, usermod -L user to lock the password and chage -E 0 user to expire the account. Verify: passwd -S user shows L (locked); chage -l user shows account expired.
- Tab completion,
Ctrl+R, and!!save minutes per task — drill them until they're muscle memory before the exam. - Redirect stdout + stderr with
&> fileor2>&1 | tee; chain with&&,||, and pipes to compose tools on the fly. - Hard links share an inode (same data, no cross-fs); soft links are pointers (can cross filesystems but break if the target moves) — the exam tests the difference.
find options, tar flags, and link types.File Systems & Storage3 lessons
The single heaviest objective cluster. Expect to partition a fresh disk, build LVM on top, format with XFS or ext4, persist the mount in /etc/fstab by UUID, and extend the volume online. XFS is the RHEL default; LVM is the layer that makes growth painless. Broken fstab entries are the #1 way candidates fail — always mount -a before rebooting.
📖 Read in-depth chapter ▾
/etc/fstab entry, and an online extend. XFS is the RHEL default; LVM is the layer that makes growth painless. A broken /etc/fstab entry is the #1 way candidates lock themselves out of the grading VM, so always validate with mount -a before rebooting.
Before a filesystem, before LVM, there's a partition. The exam will hand you a fresh virtual disk and expect you to slice it correctly the first time.
- MBR vs GPT: MBR (Master Boot Record) supports up to 4 primary partitions and disks up to 2 TB. GPT (GUID Partition Table) supports 128 partitions and disks larger than 2 TB. UEFI systems require GPT; BIOS systems traditionally use MBR.
- Partitioning with fdisk:
fdisk /dev/sdbopens the interactive MBR partitioning tool. Usento create,dto delete,tto change type (83 for Linux, 8e for LVM),pto print, andwto write changes. Runpartprobeto inform the kernel of changes. - Partitioning with gdisk:
gdisk /dev/sdbis the GPT equivalent of fdisk. Supports the same workflow but with GPT partition types (8300 for Linux filesystem, 8e00 for LVM). Usesgdiskfor scriptable non-interactive GPT partitioning. - Parted:
parted /dev/sdbsupports both MBR and GPT. Usemklabel gptto initialize,mkpart primary ext4 1MiB 500MiBto create partitions with precise sizing. Parted makes changes immediately without a write step. - Partition types: Primary partitions are bootable and limited to 4 per MBR disk. Extended partitions act as containers for logical partitions, allowing more than 4 partitions on MBR. Linux LVM type (8e) marks partitions for use as physical volumes.
Task: take a fresh 10 GB disk /dev/sdb, label it GPT, carve a 4 GB partition for LVM use. Solution: parted non-interactive — parted /dev/sdb mklabel gpt, parted /dev/sdb mkpart primary 1MiB 4097MiB, parted /dev/sdb set 1 lvm on. Verify with lsblk and parted /dev/sdb print. If using fdisk or gdisk remember to press w to write — and run partprobe after if the kernel still shows the old table.
parted writes immediately (no undo); fdisk/gdisk need w. Always lsblk or fdisk -l first to identify the right device. partprobe if the kernel hasn't caught up.
Every storage task on the exam ends with "make it survive a reboot". That means a correct /etc/fstab entry by UUID, tested with mount -a before you ever reboot.
- ext4 vs XFS: ext4 supports shrinking and growing, has a maximum file size of 16 TB, and uses the
resize2fscommand. XFS is the default on RHEL, supports only growing (no shrink), has a maximum file size of 8 EB, and usesxfs_growfs. Both support journaling. - Creating file systems:
mkfs.xfs /dev/sdb1creates an XFS filesystem.mkfs.ext4 /dev/sdb1creates ext4. Use-L labelto assign a label.blkiddisplays the UUID, label, and filesystem type of block devices. - Mounting filesystems:
mount /dev/sdb1 /mnt/datamounts temporarily. For persistent mounts, add an entry to/etc/fstabwith the format:UUID=xxxx /mount/point xfs defaults 0 0. Runmount -ato test fstab entries without rebooting. - UUID and labels: Always use UUIDs in
/etc/fstabinstead of device names like/dev/sdb1, because device names can change between boots. Useblkidto find UUIDs andxfs_admin -L label /dev/sdb1ortune2fs -L label /dev/sdb1to set labels. - Swap space: Create swap partitions with
mkswap /dev/sdb2and activate withswapon /dev/sdb2. Add to/etc/fstabasUUID=xxxx swap swap defaults 0 0. Useswapon --showto verify active swap devices.
Task: format /dev/sdb1 as XFS labelled data, mount persistently at /srv/data. Solution: format + UUID-based fstab — mkfs.xfs -L data /dev/sdb1, capture the UUID with blkid /dev/sdb1, then append to /etc/fstab: UUID=<uuid> /srv/data xfs defaults 0 0. Critical validation: mkdir -p /srv/data && mount -a && df -h /srv/data. If mount -a errors, fix it now — a broken fstab can prevent the next boot.
mount -a before reboot. XFS can grow, never shrink — pick ext4 if the task says reduce.
LVM is the storage layer the exam tests most. Build it once, extend it twice — every RHCSA candidate should be able to walk PV → VG → LV → mkfs → mount → fstab without thinking.
- LVM architecture: Physical Volumes (PVs) are partitions or whole disks initialized for LVM. Volume Groups (VGs) pool one or more PVs into a single storage pool. Logical Volumes (LVs) are carved from VGs and used like regular partitions for filesystems.
- Creating LVM:
pvcreate /dev/sdb1initializes a PV.vgcreate myvg /dev/sdb1 /dev/sdc1creates a VG from multiple PVs.lvcreate -n mylv -L 5G myvgcreates a 5 GB LV. Then format withmkfs.xfs /dev/myvg/mylvand mount it. - Extending LVM: Add a new PV to the VG with
vgextend myvg /dev/sdd1. Extend the LV withlvextend -L +2G /dev/myvg/mylvor use-l +100%FREEto use all remaining space. Then grow the filesystem:xfs_growfs /mount/pointfor XFS orresize2fs /dev/myvg/mylvfor ext4. - Reducing LVM: Only ext4 supports shrinking. Unmount first, then
e2fsck -f /dev/myvg/mylv,resize2fs /dev/myvg/mylv 3G, and finallylvreduce -L 3G /dev/myvg/mylv. XFS volumes cannot be reduced — only recreated. - LVM verification:
pvs,vgs,lvsprovide concise summaries.pvdisplay,vgdisplay,lvdisplayshow detailed information.lsblkshows the block device hierarchy including LVM mappings.
Task: build a 6 GB XFS volume from two 4 GB PVs, then extend by 2 GB later. Solution: full lifecycle — pvcreate /dev/sdb1 /dev/sdc1, vgcreate datavg /dev/sdb1 /dev/sdc1, lvcreate -n datalv -L 6G datavg, mkfs.xfs /dev/datavg/datalv, mount + fstab. To extend: lvextend -r -L +2G /dev/datavg/datalv — the -r flag grows the LV AND the XFS filesystem in one shot. Verify with lvs and df -h.
pvcreate → vgcreate → lvcreate → mkfs → mount → fstab. lvextend -r grows volume + filesystem in one command. -l +100%FREE for "use everything left".
/var on vg_root/lv_var is at 95% and a new 10 GB disk /dev/sdb has just been attached. Grow the FS without a reboot. Sequence: (1) parted /dev/sdb mklabel gpt mkpart primary 0% 100%, then pvcreate /dev/sdb1; (2) vgextend vg_root /dev/sdb1; (3) lvextend -L +5G /dev/vg_root/lv_var; (4) xfs_growfs /var (use resize2fs for ext4). Verify: df -h /var shows the new size; lvs confirms LV size; pvs shows the new PV in the VG.
- Build the stack bottom-up:
partedpartition →pvcreate→vgcreate→lvcreate→mkfs.xfs→blkidfor the UUID →/etc/fstab. - XFS grows with
xfs_growfs(mounted, online); ext4 grows withresize2fs. Both requirelvextendfirst — pair the LV grow with the FS grow. - Always reference filesystems in
/etc/fstabby UUID, not/dev/sdXN; runmount -aafter editing to catch typos before reboot.
Users & Groups3 lessons
Account lifecycle: create, modify, lock, age, delete. Group membership and SGID for collaborative directories. sudo and visudo for delegated root — including NOPASSWD and the %wheel shortcut. The most common exam mistake is usermod -G without -a, which silently wipes a user's other supplementary groups.
📖 Read in-depth chapter ▾
useradd / usermod / chage / userdel, plus group membership and SGID for collaborative directories. sudo via visudo for delegated root — including NOPASSWD and the %wheel shortcut. The classic exam pitfall: usermod -G group1,group2 without -a silently wipes a user's other supplementary groups. Always use -aG.
Account tasks appear on every exam: create users with specific UIDs and shells, set password aging, lock accounts. Read the requirements precisely — exam graders match exact UID, shell, and group fields.
- Creating users:
useradd usernamecreates a user with defaults (home directory, shell, UID). Common options:-u UIDsets a specific UID,-s /sbin/nologinprevents interactive login,-d /home/customsets a custom home directory,-e 2026-12-31sets an expiration date. - Modifying & deleting users:
usermod -aG groupname usernameadds a user to a supplementary group (-aappends; without it, all other groups are removed).usermod -L usernamelocks an account.userdel -r usernamedeletes the user and their home directory. - Password management:
passwd usernamesets or changes a password.chage -l usernamelists password aging info.chage -M 90 usernamesets maximum password age to 90 days.chage -d 0 usernameforces a password change at next login. - User configuration files:
/etc/passwdstores user accounts (username:x:UID:GID:comment:home:shell)./etc/shadowstores encrypted passwords and aging data./etc/login.defsdefines system-wide defaults for UID ranges, password aging, and home directory creation. - System users vs regular users: System users (UID below 1000 on RHEL) are created for services and daemons with
useradd -r. They typically have/sbin/nologinas their shell and no home directory. Regular users start at UID 1000.
Task: create user deploy with UID 2001, shell /bin/bash, password forced to change at first login, and account expiry on 2026-12-31. Solution: useradd + chage — useradd -u 2001 -s /bin/bash -e 2026-12-31 deploy, passwd deploy (set initial), then chage -d 0 deploy to force change on first login. Verify with id deploy (UID + groups) and chage -l deploy (aging fields).
-a with usermod -G or you wipe supplementary groups. Verify everything with id and chage -l.
Groups + SGID is the canonical RHCSA pattern for "team X must share a directory". Expect a task that explicitly tests group creation, membership, and SGID inheritance.
- Creating & modifying groups:
groupadd groupnamecreates a new group.groupadd -g 5000 groupnamesets a specific GID.groupmod -n newname oldnamerenames a group.groupdel groupnamedeletes a group (fails if it is any user's primary group). - Primary vs supplementary groups: Each user has one primary group (set at creation, stored in
/etc/passwdGID field) and zero or more supplementary groups (listed in/etc/group). Files are created with the owner's primary group. Usenewgrp groupnameto temporarily switch the effective primary group. - Group administration:
gpasswd -a user groupadds a user to a group.gpasswd -d user groupremoves a user from a group.gpasswd -A user groupmakes a user a group administrator who can manage group membership without root access. - Group configuration files:
/etc/groupstores group definitions (groupname:x:GID:members)./etc/gshadowstores group passwords and administrator lists. View effective group memberships withid usernameorgroups username. - Collaborative directories: Set the SGID bit on a shared directory (
chmod g+s /shared) so that new files inherit the directory's group rather than the creator's primary group. This is essential for team collaboration on shared project directories.
Task: alice, bob, carol must share /shared/team with read-write access, new files automatically owned by the team group. Solution: group + SGID — groupadd team, usermod -aG team alice (and bob, carol), mkdir -p /shared/team, chgrp team /shared/team, chmod 2770 /shared/team. The leading 2 sets SGID; 770 is rwx for owner+group. Verify with ls -ld /shared/team — look for the s in the group execute slot.
Delegating root cleanly with sudo is a recurring RHCSA pattern. The exam may ask you to give a specific user passwordless access to a single command, or wire up the %wheel group. Never edit /etc/sudoers with anything except visudo.
- visudo & sudoers syntax: Always use
visudo(locks file + syntax-checks before saving). The sudoers line format:USER HOST=(RUNAS) COMMANDS— e.g.alice ALL=(ALL) ALLgrants alice full root via password. - The %wheel shortcut: RHEL ships with
%wheel ALL=(ALL) ALLalready uncommented in/etc/sudoers. Adding a user to thewheelgroup (usermod -aG wheel alice) grants full sudo immediately — no edit needed. - NOPASSWD entries:
bob ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart httpdlets bob run that one command without entering a password — useful for automation hooks. Always scope NOPASSWD as narrowly as possible. - Drop-in files in /etc/sudoers.d/: Instead of editing the main sudoers, create per-purpose files like
/etc/sudoers.d/10-deployand edit them withvisudo -f /etc/sudoers.d/10-deploy. RHEL parses every file in that directory. Files with a.or~in the name are ignored. - Command aliases & logging:
Cmnd_Alias WEBSVC = /usr/bin/systemctl start httpd, /usr/bin/systemctl stop httpdlets you group commands. All sudo invocations log to/var/log/securewith the target user and command — verify after delegating.
Task: user deploy must restart httpd without entering a password, but nothing else. Solution: scoped NOPASSWD in a drop-in — visudo -f /etc/sudoers.d/10-deploy, add the line deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart httpd. Save (visudo syntax-checks on exit). Verify as deploy: sudo systemctl restart httpd succeeds with no prompt; sudo systemctl restart sshd fails. Check /var/log/secure for the audit entry.
visudo, never a plain editor. Prefer %wheel for full delegation, narrow NOPASSWD entries in /etc/sudoers.d/ for single-command automation.
deploy user that can only run systemctl restart nginx via sudo, has a locked password (key-only login), and an SSH key already provided in /tmp/deploy.pub. Sequence: (1) useradd -m -s /bin/bash deploy; (2) passwd -l deploy — locks the password (login by key only); (3) install -d -m 700 -o deploy -g deploy /home/deploy/.ssh then install -m 600 -o deploy -g deploy /tmp/deploy.pub /home/deploy/.ssh/authorized_keys; (4) echo 'deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx' > /etc/sudoers.d/deploy then visudo -c to validate. Verify: sudo -l -U deploy lists the one allowed command; ssh -i key deploy@host sudo systemctl restart nginx works; any other sudo invocation is denied.
usermod -aG group user— the-a(append) is mandatory; without it you replace the user's supplementary group list.chage -M / -m / -Wsets max / min / warn days;chage -d 0 userforces a password change on next login.- Edit
/etc/sudoersonly viavisudo(syntax-checked); add overrides to/etc/sudoers.d/drop-ins so package upgrades don't clobber them.
useradd options, group append vs replace, password aging, and sudoers syntax.Permissions & Access Control3 lessons
Standard rwx for owner/group/other; special bits (SUID, SGID, sticky) for shared services and team folders; ACLs for fine-grained per-user permission grants where standard owner/group/other isn't enough. Numeric and symbolic chmod must both be reflex.
📖 Read in-depth chapter ▾
rwx for owner/group/other; special bits — SUID, SGID, sticky — for shared binaries and team folders; setfacl / getfacl for fine-grained per-user grants. Numeric (0755) and symbolic (u+rwx,g+rx,o+rx) chmod syntaxes must both be reflex — the exam alternates between them and you can't afford the conversion math.
Numeric-to-symbolic conversion must be automatic by exam day. The single most common trap: r without x on a directory lets users list names but not access anything inside.
- Permission triplets (rwx): Each file has three permission sets: owner (u), group (g), and others (o).
r(read = 4),w(write = 2),x(execute = 1). On directories,rlists contents,wallows creating/deleting files,xallows entering the directory. - Chmod numeric & symbolic:
chmod 755 filesets rwxr-xr-x.chmod u+x fileadds execute for the owner.chmod go-w fileremoves write for group and others.chmod -R 750 /dirapplies permissions recursively to a directory tree. - Chown & chgrp:
chown user:group filechanges both owner and group.chown -R user:group /dirapplies recursively.chgrp groupname filechanges only the group. Only root can change file ownership; group members can change group ownership. - Umask: The umask subtracts permissions from the default (666 for files, 777 for directories). A umask of 022 creates files with 644 and directories with 755. Set in
/etc/bashrcor~/.bashrc. Useumaskto view andumask 027to set. - Directory permissions: Execute (
x) on a directory means you cancdinto it and access files by name. Read (r) means you can list its contents withls. Withoutx, even if you know a file's name, you cannot access it. This distinction is a common exam topic.
Task: project directory /srv/app readable by group app, with the binary /srv/app/run executable only by group app members. Solution: chown + chmod — chown -R root:app /srv/app, chmod 750 /srv/app (rwxr-x---), chmod 750 /srv/app/run. Verify by becoming a non-group user: sudo -u nobody ls /srv/app should fail with permission denied. Read it back: ls -ld /srv/app.
x on a directory = "may enter". 750 is the canonical group-only directory mode. Conversion: 750 = rwxr-x---.
Special bits are the difference between "users can collaborate" and "users can wipe each other's work". The 4-digit chmod (e.g., 2770) is the exam-canonical way to express team folders.
- SUID (Set User ID): When set on an executable file (
chmod u+s fileor 4xxx), the process runs with the file owner's privileges. Example:/usr/bin/passwdhas SUID so regular users can modify/etc/shadow. SUID has no effect on directories. - SGID (Set Group ID): On files (
chmod g+s fileor 2xxx), the process runs with the file's group privileges. On directories, new files and subdirectories inherit the directory's group ownership instead of the creator's primary group — essential for shared collaborative directories. - Sticky bit: When set on a directory (
chmod +t /diror 1xxx), only the file owner, directory owner, or root can delete or rename files within it. The classic example is/tmp(permissions drwxrwxrwt), which prevents users from deleting each other's files. - Identifying special permissions: In
ls -loutput, SUID appears assin the owner execute position, SGID assin the group execute position, and sticky bit astin the others execute position. UppercaseSorTmeans the underlying execute bit is not set. - Security implications: SUID executables are a security risk — find them with
find / -perm -4000. SGID on binaries is less common. Never set SUID on shell scripts (the kernel ignores it for security). Regularly audit SUID/SGID files on production systems.
Task: shared drop-box at /data/share where all users can write but only the file owner can delete their own file. Solution: sticky + SGID combo — chmod 3770 /data/share (digits: 3 = SGID + sticky, 770 = rwxrwx---). New files inherit the group; the sticky bit blocks cross-user deletion. Verify ls -ld /data/share shows drwxrws--T. Audit SUID system-wide periodically: find / -perm -4000 -type f 2>/dev/null.
When the requirement says "alice gets rw, bob gets r, on the same file" — standard permissions can't express it. ACLs are the answer. Default ACLs on directories make new files inherit the permission grant automatically.
- Why ACLs: Standard Linux permissions only support one owner and one group. ACLs extend this to grant specific permissions to multiple users and groups. A
+at the end ofls -loutput indicates an ACL is present. - Setting ACLs:
setfacl -m u:username:rwx /filegrants a specific user rwx access.setfacl -m g:groupname:rx /filegrants a specific group rx access.setfacl -x u:username /fileremoves a specific ACL entry.setfacl -b /fileremoves all ACLs. - Default ACLs:
setfacl -m d:u:username:rwx /dirsets a default ACL on a directory so that new files and subdirectories automatically inherit the specified permissions. Default ACLs only apply to directories, not files. - ACL mask: The mask defines the maximum effective permissions for named users and groups (not the owner).
setfacl -m m::rx /filesets the mask to rx, limiting all named users/groups to at most rx regardless of their individual ACL entries. The mask is recalculated automatically when ACLs change. - Viewing ACLs:
getfacl /filedisplays all ACL entries including owner, group, mask, and other permissions. The output shows effective permissions when the mask restricts an entry. Usegetfacl -R /dirto recursively view ACLs on a directory tree.
Task: /srv/reports must let alice read+write, bob read-only, and new files must inherit those grants. Solution: setfacl + default ACLs — setfacl -m u:alice:rwx /srv/reports, setfacl -m u:bob:rx /srv/reports, then default counterparts: setfacl -m d:u:alice:rwx /srv/reports, setfacl -m d:u:bob:rx /srv/reports. Test by creating a file as root inside and check getfacl /srv/reports/newfile — alice and bob entries appear automatically.
d: prefix on a directory makes new entries inherit. Always verify with getfacl.
devs drop files into /srv/team, but new files keep being owned by each user's primary group. Sequence: (1) chgrp devs /srv/team; (2) chmod 2775 /srv/team — the leading 2 sets SGID, forcing new entries to inherit the directory's group; (3) setfacl -d -m g:devs:rwx /srv/team — default ACL guarantees rwx for the group regardless of the user's umask. Verify: su - alice; touch /srv/team/x; ls -la /srv/team/x — group should be devs; getfacl /srv/team shows the default mask carrying through.
- Special bits:
SUID(4xxx) runs as owner,SGID(2xxx) on a dir forces new files to inherit the group, sticky (1xxx) on a shared dir means only owners can delete their files (/tmppattern). - Apply a default ACL on a collaborative directory with
setfacl -d -m g:devops:rwx /srv/sharedso new files inherit the team grant automatically. umasksubtracts from666(files) or777(dirs); set system-wide in/etc/profileor per-user in~/.bashrc.
chmod, SUID/SGID/sticky scenarios, and ACL inheritance.Networking3 lessons
NetworkManager via nmcli is the only persistent-config path you should use on the exam. Hostname via hostnamectl. Connection testing with ping, ss, dig. SSH server hardening with key-based auth and root-login disabled. Every "network" task ends with "make it survive a reboot".
📖 Read in-depth chapter ▾
nmcli is the only persistent network-config path you should use on the exam — anything you type into ip addr add dies at the next reboot. Hostname via hostnamectl, troubleshooting with ping / ss / dig, SSH server hardening with key-based auth and disabled root login. Every "network" task ends with the same silent acceptance criterion: it must survive a reboot.
Use nmcli for every persistent network change. Never edit ifcfg files by hand — NetworkManager owns them. Set the hostname early; it's a recurring sub-task on every exam.
- NetworkManager & nmcli: NetworkManager is the default network management daemon on RHEL.
nmcli con showlists connections.nmcli con mod "eth0" ipv4.addresses 192.168.1.10/24 ipv4.gateway 192.168.1.1 ipv4.dns 8.8.8.8 ipv4.method manualconfigures a static IP.nmcli con up "eth0"activates the connection. - nmtui: A text-based user interface for NetworkManager. Run
nmtuito edit connections, activate/deactivate connections, and set the system hostname. Useful when you need a quick visual interface during the exam. - IP address verification:
ip addr show(orip a) displays all interfaces and their IP addresses.ip route showdisplays the routing table and default gateway.ip link showdisplays link-layer information and interface state (UP/DOWN). - Hostname configuration:
hostnamectl set-hostname server1.example.comsets the static hostname persistently. The hostname is stored in/etc/hostname. Add local name resolution entries in/etc/hostsfor hosts that do not have DNS records. - DNS configuration: DNS servers are configured through NetworkManager (preferred) or directly in
/etc/resolv.conf. The/etc/nsswitch.conffile controls name resolution order (files before dns means/etc/hostsis checked first).
Task: set hostname to server1.example.com and configure static IP 192.168.10.20/24 with gateway 192.168.10.1 and DNS 8.8.8.8 on ens3. Solution: hostnamectl + nmcli — hostnamectl set-hostname server1.example.com, then nmcli con mod ens3 ipv4.addresses 192.168.10.20/24 ipv4.gateway 192.168.10.1 ipv4.dns 8.8.8.8 ipv4.method manual, finally nmcli con up ens3 to apply. Verify with ip a, ip route, and cat /etc/resolv.conf.
nmcli, never raw ifcfg edits. nmcli con up reactivates after a mod. Hostname via hostnamectl, period.
When a service "doesn't work", a layered diagnostic walk — link → IP → gateway → DNS → firewall → listener — finds the cause faster than guesswork. The exam clocks every minute, so a systematic check pays.
- Connection testing:
ping -c 4 hosttests ICMP connectivity.traceroute host(ortracepath) shows the path packets take to a destination, identifying where connectivity fails.curl -v http://hosttests HTTP connectivity at the application layer. - Socket statistics:
ss -tlnpshows listening TCP sockets with process information.ss -ulnpshows listening UDP sockets.ss -anshows all connections with numeric addresses. This replaces the deprecatednetstatcommand on RHEL. - DNS troubleshooting:
dig example.comqueries DNS and shows detailed response information.nslookup example.comprovides simpler DNS lookup output.host example.comperforms a quick DNS resolution check. Verify/etc/resolv.conffor correct nameserver entries. - Firewall basics:
firewall-cmd --list-allshows the current zone configuration including allowed services and ports. If connectivity fails, check whether the required service or port is allowed through the firewall before investigating other causes. - Network teaming & bridging: Network teams bond multiple NICs for redundancy or load balancing using
nmcli con add type team. Network bridges connect virtual machines to the physical network. Both are configured through NetworkManager and tested at an awareness level on the RHCSA.
Symptom: web service unreachable from client. Diagnostic walk: 1. ip link show ens3 — interface UP? 2. ip a — IP assigned? 3. ping -c 2 <gateway> — L3 to gateway? 4. dig example.com — DNS resolves? 5. firewall-cmd --list-all — http service allowed? 6. ss -tlnp | grep :80 — httpd actually listening? Each step rules out one layer; the first failure tells you where to dig.
ss replaces netstat; learn it cold.
SSH is the only way you reach the second exam VM. The exam often asks you to disable root login, enable key-based authentication, or change the listening port — all of which involve sshd_config, a service restart, and a firewall + SELinux port update.
- sshd_config essentials:
/etc/ssh/sshd_configcontrols the server. Key directives:PermitRootLogin no,PasswordAuthentication no,PubkeyAuthentication yes,Port 22. After any edit,systemctl restart sshdto apply. Validate syntax first withsshd -t. - Key-based authentication: Client generates a key pair with
ssh-keygen -t ed25519. Public key goes to the server in~/.ssh/authorized_keys(mode 600), under a directory with mode 700 owned by the target user.ssh-copy-id user@hostautomates the deployment. - Drop-in config files:
/etc/ssh/sshd_config.d/*.confare parsed before the main file. RHEL ships with/etc/ssh/sshd_config.d/50-redhat.conf; put your overrides in a higher-numbered file like99-custom.confso they win. - Non-standard port + SELinux: Moving sshd to a non-default port requires
semanage port -a -t ssh_port_t -p tcp 2222ANDfirewall-cmd --add-port=2222/tcp --permanent+ reload. Skipping either makes the service look broken when SELinux or the firewall is the cause. - Connection client options:
ssh -i ~/.ssh/id_ed25519 user@hostpicks a specific key.~/.ssh/configper-host aliases (Host prod,HostName ...,User ...,IdentityFile ...) save typing. Always testsshd -tafter edits to avoid locking yourself out.
Task: disable root SSH login and password auth, allow only key-based auth for user admin. Solution: drop-in override + key deploy — create /etc/ssh/sshd_config.d/99-hardening.conf with PermitRootLogin no and PasswordAuthentication no, then sshd -t to validate, systemctl restart sshd. As admin: ssh-keygen -t ed25519, copy the pub key to the server's ~admin/.ssh/authorized_keys with mode 600 inside a mode-700 .ssh directory owned by admin. Test with ssh admin@host — succeeds with key, root and password attempts fail.
sshd -t before restart, or you can lock yourself out. Non-standard port = semanage port -a -t ssh_port_t + firewalld update.
192.168.10.50/24 with gateway 192.168.10.1 and DNS 1.1.1.1 to interface ens3, surviving reboot. Sequence: (1) nmcli con show — find the active profile name (often System ens3 or Wired connection 1); (2) nmcli con mod 'System ens3' ipv4.addresses 192.168.10.50/24 ipv4.gateway 192.168.10.1 ipv4.dns 1.1.1.1 ipv4.method manual; (3) nmcli con down 'System ens3' && nmcli con up 'System ens3'. Verify: ip -br addr show ens3 shows the new address; ip route shows the default via the gateway; ping -c2 1.1.1.1 succeeds; cat /etc/resolv.conf lists the DNS.
nmcli con mod <name> ipv4.addresses ... ipv4.method manual+nmcli con up <name>— anything not done via NetworkManager won't persist.ssh-keygen→ssh-copy-id user@hostdeploys the public key; then disablePasswordAuthentication+PermitRootLoginin/etc/ssh/sshd_configandsystemctl restart sshd.- Use
ss -tlnp(modern replacement fornetstat) to see who's listening on which port;dig +shortgives a one-line resolve check.
nmcli persistence, SSH key-auth setup, and connectivity-troubleshooting commands.Firewall & SELinux3 lessons
firewalld zones + permanent rules, then the SELinux trifecta: file contexts via semanage fcontext + restorecon, booleans via setsebool -P, port labels via semanage port. SELinux must stay enforcing — the exam zero-grades attempts to disable it.
📖 Read in-depth chapter ▾
firewalld zones with permanent rules, then the SELinux trifecta: file contexts via semanage fcontext + restorecon, booleans via setsebool -P, port labels via semanage port. SELinux must stay enforcing — the exam zero-grades any attempt to setenforce 0 or set SELINUX=disabled. Diagnose with ausearch / sealert instead.
firewalld is on every exam. The #1 mistake: forgetting --permanent and losing rules at reboot. Rules without --permanent are runtime-only and zero-graded.
- Zones & default zone: firewalld organizes rules into zones (public, work, home, dmz, trusted, etc.). Each network interface is assigned to a zone. The default zone (usually public) applies to interfaces not explicitly assigned.
firewall-cmd --get-default-zoneshows the current default. - Adding services & ports:
firewall-cmd --add-service=http --permanentallows HTTP traffic.firewall-cmd --add-port=8080/tcp --permanentopens a custom port. The--permanentflag writes the rule to disk; without it, rules are runtime-only and lost on reload. - Runtime vs permanent: Runtime rules take effect immediately but are lost on
firewall-cmd --reloador reboot. Permanent rules require--reloadto take effect. Best practice: add rules with--permanent, then runfirewall-cmd --reloadto apply them. - Rich rules:
firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="ssh" accept' --permanentcreates granular rules based on source address, destination, service, port, and action (accept/reject/drop). - Verification:
firewall-cmd --list-alldisplays all rules in the active zone.firewall-cmd --list-servicesshows allowed services.firewall-cmd --list-portsshows allowed ports.firewall-cmd --get-active-zonesshows zone-to-interface assignments.
Task: permanently allow http, https, and custom port 8080/tcp; restrict SSH access to only the 192.168.10.0/24 subnet. Solution: permanent service + rich rule — firewall-cmd --add-service=http --add-service=https --add-port=8080/tcp --permanent, then firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" service name="ssh" accept' --permanent, then remove the default ssh service with firewall-cmd --remove-service=ssh --permanent, finally firewall-cmd --reload. Verify firewall-cmd --list-all.
--permanent + --reload. Verify with --list-all. Predefined service names (http, https, ssh, nfs) save typing port numbers.
SELinux is mandatory access control on top of regular permissions. Every file, process, and port has a type label. Tasks where "permissions look right but service still fails" are almost always SELinux.
- SELinux modes: Enforcing (actively blocks policy violations and logs them), Permissive (logs violations but does not block — useful for troubleshooting), Disabled (SELinux is completely off). Check the current mode with
getenforce. Toggle between enforcing and permissive withsetenforce 1orsetenforce 0. - Persistent SELinux configuration: The file
/etc/selinux/configcontrols the mode at boot. SetSELINUX=enforcingfor production systems. Changing from disabled to enforcing requires a reboot and filesystem relabel, which can be time-consuming. - SELinux contexts: Every file, process, and port has a security context in the format
user:role:type:level. The type field is the most important for policy enforcement. View file contexts withls -Z, process contexts withps -eZ, and port contexts withsemanage port -l. - Common context types:
httpd_sys_content_tfor web content served by Apache,sshd_tfor the SSH daemon process,user_home_tfor user home directories. When a process type does not have permission to access a file type, SELinux denies the access. - Type enforcement: SELinux policy rules define which process types (domains) can access which file types. For example,
httpd_tcan readhttpd_sys_content_tbut notuser_home_t. This Mandatory Access Control (MAC) operates on top of standard Linux DAC permissions.
Symptom: httpd returns 403 on a file you can cat as root. Diagnostic: compare process and file contexts — ps -eZ | grep httpd shows process type httpd_t; ls -Z /var/www/html/index.html shows file type. If file type is user_home_t instead of httpd_sys_content_t (e.g., file copied from a user home), httpd is denied. Confirm in /var/log/audit/audit.log with ausearch -m avc -ts recent.
ls -Z + ps -eZ.
Three remediation tools cover ~95% of SELinux denials: restorecon for wrong file contexts, setsebool -P for blocked behaviors, semanage port for non-default service ports.
- Restoring contexts:
restorecon -Rv /pathresets file contexts to the policy defaults. This is the most common fix when files have been copied or moved and their contexts are wrong. Moving a file preserves its original context; copying inherits the destination context. - Managing file contexts:
semanage fcontext -a -t httpd_sys_content_t "/custom/web(/.*)?"adds a permanent context rule for a custom directory. Then runrestorecon -Rv /custom/webto apply. This is required when serving web content from non-default directories. - SELinux booleans: Booleans are on/off switches for specific policy behaviors.
getsebool -a | grep httpdlists all HTTP-related booleans.setsebool -P httpd_enable_homedirs onpersistently enables a boolean (-Pfor permanent). Without-P, the change is lost on reboot. - Audit log analysis: SELinux denials are logged in
/var/log/audit/audit.log.ausearch -m avc -ts recentfinds recent denials. Installsetroubleshoot-serverforsealert, which provides human-readable explanations and suggested fixes for each denial. - Port contexts:
semanage port -l | grep httplists ports assigned to HTTP types.semanage port -a -t http_port_t -p tcp 8888allows httpd to listen on a custom port. Without this, SELinux blocks the service from binding to non-standard ports.
Task: serve web content from /srv/www (non-default) and let httpd listen on port 8888. Solution: semanage + restorecon + boolean + firewall — semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?", restorecon -Rv /srv/www; semanage port -a -t http_port_t -p tcp 8888; setsebool -P httpd_can_network_connect on if it connects out; firewall-cmd --add-port=8888/tcp --permanent && firewall-cmd --reload. Verify with ls -Z /srv/www and semanage port -l | grep http_port_t.
restorecon, setsebool -P, semanage port -a — cover almost every SELinux fix. Always -P for permanent.
8443/tcp. By default both firewalld and SELinux block the new port. Sequence: (1) firewall-cmd --permanent --add-port=8443/tcp && firewall-cmd --reload; (2) semanage port -a -t http_port_t -p tcp 8443 (if missing: dnf -y install policycoreutils-python-utils); (3) edit /etc/httpd/conf/httpd.conf to add Listen 8443; (4) systemctl restart httpd. Verify: ss -lntp | grep 8443 shows httpd listening; firewall-cmd --list-ports includes 8443/tcp; semanage port -l | grep http_port_t includes 8443. If a request 503s, ausearch -m avc -ts recent reveals the missing label.
- Every
firewall-cmdchange needs--permanent+--reloadto survive reboot; without--permanentthe rule disappears on next restart. - Move a service's docroot? Always relabel:
semanage fcontext -a -t httpd_sys_content_t "/new(/.*)?"thenrestorecon -Rv /new. Skipping this is the #1 SELinux denial cause. - Toggle a boolean permanently with
setsebool -P httpd_can_network_connect on; relabel a non-standard port withsemanage port -a -t http_port_t -p tcp 8080.
Process Management3 lessons
Process inspection with ps, top, pgrep; signals and nice priority; systemctl service control and journalctl log inspection; writing custom systemd unit files for new services. Every service the exam asks you to configure must be both started AND enabled — systemctl enable --now does both in one shot.
📖 Read in-depth chapter ▾
ps / top / pgrep, signals + nice priority, and the systemd surface: systemctl for control, journalctl for logs, custom unit files for new services. Every service the exam configures must be both started and enabled — systemctl enable --now <svc> does both in one shot and is the correct reflex answer.
Find the runaway process, change its priority, kill it if necessary. The exam treats this as practical: identify the resource hog with top, then adjust or terminate.
- Viewing processes:
ps auxlists all processes with user, PID, CPU/memory usage, and command.ps -efprovides a similar view with parent PID.top(orhtopif installed) provides a real-time, interactive view of system resource usage sorted by CPU or memory. - Killing processes:
kill PIDsends SIGTERM (signal 15), which requests graceful termination.kill -9 PIDsends SIGKILL, which forces immediate termination (cannot be caught or ignored).killall processnamekills all processes with a given name.pkill -u usernamekills all processes owned by a user. - Process priority (nice): Nice values range from -20 (highest priority) to 19 (lowest priority). Default is 0.
nice -n 10 commandstarts a process with lower priority.renice -n 5 -p PIDchanges the priority of a running process. Only root can set negative nice values. - Job control:
Ctrl+Zsuspends the foreground process.bg %1resumes it in the background.fg %1brings it to the foreground.jobslists all background jobs.nohup command &runs a command immune to hangup signals, so it continues after logout. - Signal types: SIGTERM (15) — polite termination request. SIGKILL (9) — forced termination, cannot be caught. SIGHUP (1) — often used to reload configuration. SIGSTOP (19) — pauses a process. SIGCONT (18) — resumes a paused process. List all signals with
kill -l.
Task: a runaway compute.py is hogging CPU. Identify and de-prioritise it without killing. Solution: top + renice — open top, press P to sort by %CPU, note the PID. Exit; renice -n 15 -p <PID> drops it near the lowest priority. If it must die, kill <PID> (SIGTERM) first; only escalate to kill -9 <PID> if it ignores SIGTERM. Verify with ps -o pid,ni,comm -p <PID>.
renice for live priority. nohup & for "survive my logout".
systemctl is the most-used tool on the exam. Every service the exam configures must be both started AND enabled — graders check that it survives a reboot.
- Service management:
systemctl start httpdstarts a service.systemctl stop httpdstops it.systemctl restart httpdrestarts (stop + start).systemctl reload httpdreloads configuration without stopping.systemctl status httpdshows the current state, PID, and recent log entries. - Enabling & disabling:
systemctl enable httpdcreates symlinks so the service starts at boot.systemctl disable httpdremoves those symlinks.systemctl enable --now httpdenables and starts in one command.systemctl is-enabled httpdchecks the boot-time status. - Masking services:
systemctl mask httpdprevents a service from being started by any means (manual or dependency).systemctl unmask httpdreverses the mask. Masking creates a symlink to/dev/null. This is stronger than disabling, which only prevents automatic startup at boot. - systemd targets: Targets replace SysVinit runlevels.
multi-user.targetis equivalent to runlevel 3 (text mode).graphical.targetis equivalent to runlevel 5 (GUI).systemctl get-defaultshows the default target.systemctl set-default multi-user.targetchanges it. - Journalctl:
journalctl -u httpdshows logs for a specific unit.journalctl -ffollows the log in real time.journalctl --since "1 hour ago"filters by time.journalctl -p errshows only error-level and above messages. The journal is stored in/run/log/journalby default (volatile).
Task: install httpd, make sure it starts at boot, verify it's actually running, troubleshoot if it fails. Solution: install + enable --now + verify — dnf -y install httpd, systemctl enable --now httpd. Verify both states: systemctl is-active httpd (running?) and systemctl is-enabled httpd (boot?). If it fails, systemctl status httpd (last error) and journalctl -xeu httpd (full context). Check listener: ss -tlnp | grep :80.
enable --now is the canonical RHCSA combo. Verify with is-active + is-enabled. journalctl -xeu <unit> is your first stop for any service failure.
Beyond starting prebuilt services, RHCSA may ask you to make a custom script a real systemd-managed service — auto-restart on failure, logged to the journal, enabled at boot.
- Unit file location: System-wide unit files live in
/etc/systemd/system/<name>.service(admin-owned, highest precedence). Distro-supplied units live in/usr/lib/systemd/system/— never edit those directly; copy or override. - The three sections:
[Unit](description, dependencies viaAfter=/Requires=),[Service](Type=simple/forking/oneshot,ExecStart=,Restart=on-failure,User=),[Install](WantedBy=multi-user.target). - Reload after editing: Any change to a unit file requires
systemctl daemon-reloadbefore the nextstartorrestart. Forgetting this is the #1 reason a "fixed" unit still behaves the old way. - Overrides via drop-ins:
systemctl edit httpdopens an empty override at/etc/systemd/system/httpd.service.d/override.conf. Add only the directives you want to change — safer than copying the whole vendor unit. Always include the section header (e.g.,[Service]). - Verifying the unit:
systemctl cat <unit>shows the effective merged unit (vendor + overrides).systemctl show <unit>dumps every resolved property. Afterdaemon-reload,systemctl statusreflects the new definition.
Task: turn /usr/local/bin/sync.sh into a service that auto-restarts on failure. Solution: write unit + daemon-reload + enable --now — create /etc/systemd/system/sync.service with [Unit] Description=Custom sync\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/local/bin/sync.sh\nRestart=on-failure\nUser=root\n\n[Install]\nWantedBy=multi-user.target. Then systemctl daemon-reload, systemctl enable --now sync. Verify with systemctl status sync and journalctl -u sync.
daemon-reload after edits. systemctl edit for overriding vendor units cleanly.
top -b -n1 | head -20 — top of CPU% column; note the PID; (2) ps -p PID -o pid,user,etime,cmd for context (how long it has been running); (3) strace -p PID -e trace=openat,read 2>&1 | head -50 — what it's looping on; (4) deprioritise rather than kill if the work is legitimate: renice +15 -p PID, or stop it cleanly with kill -TERM PID; (5) for a recurring offender, cap it via systemd drop-in: systemctl edit unit-name and add [Service]\nCPUQuota=50%. Verify: uptime load decays; systemctl show unit-name -p CPUQuotaPerSecUSec reflects the cap.
systemctl enable --now <svc>= enable at boot + start immediately. Forgetting the--now(or just runningstart) is the most graded-against mistake.- Custom units live in
/etc/systemd/system/<name>.service; after any edit runsystemctl daemon-reloadbefore starting the service. - Persist journal logs across reboots: create
/var/log/journal(or setStorage=persistentin/etc/systemd/journald.conf) and restartsystemd-journald.
Package Management3 lessons
dnf for installs, updates, group packages, and module streams. rpm for low-level queries — "which package owns this file?" Configure custom repositories in /etc/yum.repos.d/ for the exam-supplied local mirror. Know which command to use when.
📖 Read in-depth chapter ▾
dnf for installs, updates, group packages, and module streams; rpm for low-level queries — "which package owns this file?". Custom repositories live in /etc/yum.repos.d/*.repo for the exam-supplied local mirror. Know which tool to reach for: dnf resolves dependencies, rpm queries the local database.
DNF (the YUM successor on RHEL 8/9) is the daily driver for everything: install, update, groups, modules. Same syntax as YUM — the alias is preserved for muscle memory.
- Core DNF operations:
dnf install httpdinstalls a package and its dependencies.dnf remove httpduninstalls it.dnf updateupdates all packages.dnf update httpdupdates a specific package. DNF is the replacement for YUM on RHEL 8+ and they share the same syntax. - Searching & querying:
dnf search keywordfinds packages by name or description.dnf info httpdshows package details (version, size, description).dnf list installedshows all installed packages.dnf provides /path/to/fileidentifies which package owns a specific file. - Group packages:
dnf group listlists available package groups.dnf group install "Development Tools"installs a full group of related packages. Groups bundle common tools (compilers, libraries, server components) for convenient installation. - Repository management: Repos are configured in
/etc/yum.repos.d/*.repofiles. Key fields:baseurlormirrorlist,enabled=1,gpgcheck=1,gpgkey.dnf repolistshows enabled repos.dnf config-manager --add-repo URLadds a new repository. - Module streams:
dnf module listshows available module streams (e.g., python36, python38, python39).dnf module enable python39selects a stream.dnf module install python39installs the module profile. Streams allow multiple versions of software to coexist in the same repository.
Task: install the "Development Tools" group, then install python 3.9 specifically. Solution: group install + module switch — dnf -y group install "Development Tools", then dnf module list python39 to confirm availability, dnf module enable -y python39, dnf module install -y python39. Verify with python3.9 --version. If a different python stream was previously active, run dnf module reset python first.
dnf is your default. Modules need enable + install; reset to clear a previous stream choice.
RPM is the layer underneath DNF. Use it for queries — DNF for installs. rpm -qf and rpm -ql answer most "where did this file come from?" questions in seconds.
- RPM queries:
rpm -qalists all installed packages.rpm -qi httpdshows detailed information about an installed package.rpm -ql httpdlists all files installed by a package.rpm -qf /usr/sbin/httpdidentifies which package owns a file.rpm -qd httpdlists documentation files. - RPM installation:
rpm -ivh package.rpminstalls a local RPM file (i = install, v = verbose, h = hash progress).rpm -Uvh package.rpmupgrades (installs if not present).rpm -e packagenameremoves a package. RPM does not resolve dependencies — use DNF for that. - GPG key verification:
rpm --import https://url/RPM-GPG-KEYimports a GPG public key.rpm -K package.rpmverifies the signature and integrity of an RPM file. GPG keys ensure packages have not been tampered with and come from a trusted source. - RPM vs DNF: RPM operates on individual package files without dependency resolution. DNF wraps RPM, adding repository support, automatic dependency resolution, and group/module management. Use RPM for querying installed packages and DNF for installing, updating, and removing.
- Verifying installed files:
rpm -V httpdcompares installed files against package metadata; output flagsS(size),M(mode),5(md5 hash),T(timestamp) for any drift. Useful for spotting tampered binaries or config files.
Task: figure out which package owns /usr/sbin/sshd, list its config files, and check whether any have been modified. Solution: rpm -qf + rpm -qc + rpm -V — rpm -qf /usr/sbin/sshd returns openssh-server-.... rpm -qc openssh-server lists every config file shipped. rpm -V openssh-server flags any with 5 (md5 differs from original) — those have been edited.
rpm -qf $(which cmd) for "what package owns this?" — fastest answer. rpm -V for drift detection.
RHCSA frequently hands you a URL for a local mirror and expects you to wire it up as a yum/dnf repo. The pattern is identical every time: drop a .repo file in /etc/yum.repos.d/ with the right four fields.
- Anatomy of a .repo file: Each repo block needs an
[id]header,name=(free-text label),baseurl=(ormirrorlist=),enabled=1, and eithergpgcheck=0(skip verification) orgpgcheck=1+gpgkey=. - Location & precedence: Files go in
/etc/yum.repos.d/, must end in.repo, and are read in alphabetical order. Multiple[id]blocks per file are allowed but one repo per file is cleaner for the exam. - GPG handling: If the exam provides a GPG key URL, set
gpgcheck=1andgpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-...or a remote URL.rpm --import <key-url>trusts the key system-wide. If no key is provided,gpgcheck=0is acceptable for a local lab mirror. - Verifying the repo:
dnf repolistmust show the new repo enabled.dnf repolist allincludes disabled ones.dnf --disablerepo='*' --enablerepo=myrepo list available | headisolates the new repo to confirm packages are reachable. - dnf history:
dnf historylists every transaction.dnf history undo <id>reverses one. Useful when an exam task asks you to roll back a misinstall.
Task: configure a local mirror at http://mirror.example.com/rhel9/baseos/ as repo localbase with no GPG check. Solution: drop a .repo file — create /etc/yum.repos.d/localbase.repo with [localbase]\nname=Local BaseOS Mirror\nbaseurl=http://mirror.example.com/rhel9/baseos/\nenabled=1\ngpgcheck=0. Verify with dnf repolist (must show localbase) and try a test install: dnf -y install bind-utils. If it pulls from localbase, you're done.
[id], name, baseurl, enabled=1 — plus gpgcheck. Verify with dnf repolist before trusting it.
dnf update ran 30 minutes ago and broke a production service. Revert just that transaction. Sequence: (1) dnf history list — find the offending transaction ID (e.g. 42); (2) dnf history info 42 confirms the packages that were upgraded; (3) dnf history undo 42 — restores the previous versions in one transaction; (4) systemctl restart broken-service and validate; (5) freeze the known-good versions: dnf install python3-dnf-plugin-versionlock then dnf versionlock add package-name. Verify: rpm -q package-name shows the rolled-back NVR; dnf versionlock list confirms the lock; dnf history shows the undo transaction.
- Add a local repo by dropping a
*.repofile in/etc/yum.repos.d/withbaseurl=file://orhttp://+gpgcheck=0if no key is provided. - Module streams:
dnf module list→dnf module enable nodejs:18→dnf module install nodejs. Streams pin a version family for the lifetime of the system. rpm -qf /path/to/filetells you which package owns a file;rpm -ql <pkg>lists everything an installed package shipped.
Scheduling & Logging3 lessons
Cron, anacron, and at for traditional scheduling. rsyslog + journald for logs, with persistent journal storage as a recurring exam objective. Modern systemd timers as the cron replacement — calendar expressions, paired service units, accuracy controls.
📖 Read in-depth chapter ▾
cron, anacron, and at for traditional scheduling. rsyslog + journald for logs, with persistent journal storage as a recurring exam objective. Modern systemd timers replace cron when you need calendar expressions, accuracy windows, or unit-aware scheduling.
Cron syntax must be reflex. The exam doesn't give partial credit for "almost right" — minute, hour, day, month, weekday, in that order, full stop.
- Crontab:
crontab -eedits the current user's cron schedule.crontab -llists it.crontab -e -u usernameedits another user's crontab (as root). The format is:minute hour day-of-month month day-of-week command. Example:30 2 * * 1 /scripts/backup.shruns at 2:30 AM every Monday. - System cron:
/etc/crontabis the system-wide crontab with an additional user field. Drop-in files in/etc/cron.d/follow the same format. Directories/etc/cron.hourly/,/etc/cron.daily/,/etc/cron.weekly/, and/etc/cron.monthly/run scripts placed within them at the named interval. - Anacron: Anacron runs missed cron jobs that were scheduled while the system was off. Configured in
/etc/anacrontab. Unlike cron, anacron does not assume the system is running 24/7, making it suitable for desktops and laptops. - at command:
at now + 5 minutesschedules a one-time job. Type commands, then pressCtrl+Dto save.atqlists pending jobs.atrm jobnumberremoves a scheduled job. Access control:/etc/at.allowand/etc/at.denycontrol which users can useat. - Absolute paths in cron: Cron runs with a minimal
PATH(usually/usr/bin:/bin). Always use absolute paths in cron commands (e.g.,/usr/bin/tarinstead oftar). Redirect output:... >> /var/log/job.log 2>&1.
Task: run /root/backup.sh at 02:30 every Monday and Friday as root. Solution: crontab -e — add the line 30 2 * * 1,5 /root/backup.sh >> /var/log/backup.log 2>&1. Verify with crontab -l. For a one-shot test in 5 minutes: echo "/root/backup.sh" | at now + 5 minutes, then atq to confirm queued.
1,5 for specific days.
Two logging stacks coexist on RHEL: traditional rsyslog (text files in /var/log/) and the systemd journal (structured, queryable via journalctl). The exam frequently asks for persistent journal storage.
- rsyslog: The default logging daemon on RHEL. Configuration in
/etc/rsyslog.confdefines rules that route messages by facility (auth, kern, mail, cron) and priority (emerg, alert, crit, err, warning, notice, info, debug) to log files. Most logs go to/var/log/. - Key log files:
/var/log/messagescaptures most system messages./var/log/securelogs authentication events./var/log/boot.logrecords boot messages./var/log/cronlogs cron job execution./var/log/audit/audit.logstores SELinux and auditd events. - journalctl filtering:
journalctl -u sshdfilters by unit.journalctl --since "2026-04-01" --until "2026-04-02"filters by date range.journalctl -p errshows only errors and above.journalctl _PID=1234filters by process ID. Combine filters for precise log queries. - Persistent journal storage: By default, the systemd journal is stored in
/run/log/journal(volatile, cleared on reboot). To make it persistent, create/var/log/journaldirectory and restart systemd-journald, or setStorage=persistentin/etc/systemd/journald.conf. - Log rotation:
logrotateautomatically compresses, rotates, and removes old log files. Configuration in/etc/logrotate.confand/etc/logrotate.d/. Settings include rotation frequency (daily, weekly), number of old files to keep, compression, and post-rotation scripts.
Task: make the systemd journal persist across reboots. Solution: create the directory + restart journald — mkdir -p /var/log/journal, systemd-tmpfiles --create --prefix /var/log/journal, systemctl restart systemd-journald. Verify with journalctl --disk-usage (should show MB in /var/log/journal/, not /run/log/journal/). Reboot and confirm journalctl --since yesterday still has data.
/var/log/journal directory + restart journald. Verify with journalctl --disk-usage.
systemd timers replace cron with better logging, calendar expressions, and dependency awareness. Every timer is a .timer unit paired with a matching .service unit. The exam may accept either cron or a timer — timers are the modern answer.
- Two units per timer: A timer needs
foo.timer(when) ANDfoo.service(what). Same base name — systemd pairs them by convention unlessUnit=overrides. Both live in/etc/systemd/system/. - OnCalendar expressions:
OnCalendar=*-*-* 02:30:00= daily at 02:30.OnCalendar=Mon..Fri 09:00= weekdays at 9 AM.OnCalendar=hourly/daily/weeklyare shortcuts. Test parsing withsystemd-analyze calendar "Mon..Fri 09:00". - Monotonic timers:
OnBootSec=10min= run 10 minutes after boot.OnUnitActiveSec=1h= re-fire 1 hour after the last activation. Useful for "run periodically while the system is up". - Persistent & AccuracySec:
Persistent=trueruns missed jobs after a poweroff window (like anacron).AccuracySec=1mlets systemd batch firings to save power — set to1usfor cron-level precision. - Enabling & inspecting:
systemctl daemon-reloadafter writing units, thensystemctl enable --now foo.timer(enable the timer, NOT the service).systemctl list-timersshows next-fire times.journalctl -u foo.serviceshows the actual run output.
Task: replace a cron job that ran /root/backup.sh at 02:30 with a systemd timer. Solution: two unit files + enable the timer — create /etc/systemd/system/backup.service with [Service]\nType=oneshot\nExecStart=/root/backup.sh. Then /etc/systemd/system/backup.timer with [Timer]\nOnCalendar=*-*-* 02:30:00\nPersistent=true\n\n[Install]\nWantedBy=timers.target. systemctl daemon-reload; systemctl enable --now backup.timer. Verify with systemctl list-timers backup.timer.
Persistent=true for catch-up after downtime. systemctl list-timers for verification.
0 2 * * * /usr/local/bin/backup.sh is in root's crontab; the journal shows it fires nightly, but no backup file is created. Sequence: (1) grep CRON /var/log/cron | tail -20 — confirm the job is actually triggered; (2) journalctl -u crond --since '24 hours ago' for daemon errors; (3) mail or cat /var/spool/mail/root — cron mails stderr if there's no redirection; the error is almost always missing $PATH; (4) fix it once for the whole crontab: prepend PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin in crontab -e, and append 2>&1 | logger -t backup to the job line so output lands in journalctl -t backup. Verify: next run produces output; journalctl -t backup --since today shows the trace; the backup file appears.
- Per-user cron:
crontab -eedits the current user's table. System cron lives in/etc/cron.d/and includes a username field. Format:m h dom mon dow command. - A systemd timer is two units:
foo.timer(when) +foo.service(what). Enable withsystemctl enable --now foo.timer; inspect withsystemctl list-timers. - Persistent journal:
mkdir -p /var/log/journal(or setStorage=persistentinjournald.conf) sojournalctl --since yesterdayworks after a reboot.
Boot Process & Troubleshooting3 lessons
BIOS/UEFI → GRUB2 → kernel + initramfs → systemd target. The single most-tested troubleshooting drill: reset a forgotten root password via rd.break at the GRUB menu. Don't forget touch /.autorelabel — without it, SELinux blocks the new shadow file and login fails.
📖 Read in-depth chapter ▾
systemd target. The single most-graded troubleshooting drill: reset a forgotten root password via rd.break at the GRUB menu. Don't forget touch /.autorelabel — without it, SELinux blocks the new shadow file and login fails after the reboot.
Knowing the boot order lets you pick the right intervention point. The exam may ask you to add a kernel parameter, change the default target, or rebuild GRUB after a config change.
- BIOS/UEFI firmware: The first stage of boot. BIOS uses MBR to locate the bootloader. UEFI reads the GPT and loads the bootloader from the EFI System Partition (ESP, typically mounted at
/boot/efi). UEFI supports Secure Boot, which verifies bootloader signatures. - GRUB2 bootloader: GRUB2 loads the kernel and initramfs into memory. Configuration is in
/boot/grub2/grub.cfg(generated, do not edit directly). Defaults are set in/etc/default/grub. After changes, regenerate withgrub2-mkconfig -o /boot/grub2/grub.cfg. - Kernel & initramfs: The kernel initializes hardware, mounts the root filesystem (initially from initramfs, a temporary RAM-based filesystem containing essential drivers), then pivots to the real root filesystem. Kernel parameters can be passed via GRUB2.
- systemd initialization: Once the kernel starts PID 1 (systemd), it reads the default target and starts all units required to reach that target in parallel.
multi-user.targetprovides a full multi-user text environment;graphical.targetadds a GUI. - Boot targets:
systemctl get-defaultshows the current default target.systemctl set-default multi-user.targetchanges it.systemctl isolate rescue.targetswitches to rescue mode (single-user, root shell, minimal services). Targets replace the older SysVinit runlevels.
Task: switch the default boot target from graphical to text-mode (multi-user). Solution: systemctl set-default — systemctl set-default multi-user.target. This updates /etc/systemd/system/default.target as a symlink. Verify with systemctl get-default. Reboot to confirm. For a one-time switch without changing the default, systemctl isolate multi-user.target.
set-default persists; isolate is one-shot.
Root password reset is the canonical RHCSA boot-recovery drill. Walk through the procedure in a lab until it's reflex — minutes count, and missing touch /.autorelabel means the relabeled system can't log in.
- Root password reset (rd.break): At the GRUB2 menu, press
eto edit, appendrd.breakto the linux line, thenCtrl+Xto boot. This drops you into initramfs before the real root is mounted. Remount withmount -o remount,rw /sysroot, chroot into/sysroot, runpasswd root, thentouch /.autorelabeland exit. - Emergency & rescue targets: Append
systemd.unit=emergency.targetto the GRUB2 kernel line for the most minimal environment (root filesystem mounted read-only). Usesystemd.unit=rescue.targetfor single-user mode with more services. Both require the root password. - GRUB2 editing at boot: Press
eat the GRUB2 menu to edit kernel parameters for the current boot only. Common uses: appendrd.breakfor password reset,systemd.unit=rescue.targetfor rescue mode, orinit=/bin/bashfor an emergency shell without systemd. - Filesystem repair: If the system fails to boot due to a corrupt filesystem, boot into emergency mode. Run
fsck /dev/sdXnto check and repair the filesystem. Fix/etc/fstaberrors that prevent mounting. Remount root as read-write withmount -o remount,rw /to make changes. - SELinux relabeling: After any
rd.breakpassword reset,touch /.autorelabelis mandatory to trigger a full SELinux relabel on the next boot. Without it, the new password file will have the wrong SELinux context and login will fail. The relabel process can take several minutes.
Task: forgotten root password — recover. Solution: rd.break drill — at GRUB2, press e, append rd.break to the linux line, Ctrl+X to boot. At the switch_root:/# prompt: mount -o remount,rw /sysroot, chroot /sysroot, passwd root (set new), touch /.autorelabel, exit, exit. The system reboots, relabels (takes 1-3 minutes), then comes up with the new password. Skip /.autorelabel and login will fail — guaranteed point loss.
Beyond emergency edits, RHCSA expects you to change persistent kernel parameters, rebuild the initramfs, and protect the bootloader. Every change to /etc/default/grub must be followed by grub2-mkconfig.
- Editing kernel parameters: Persistent boot parameters live in
/etc/default/grubasGRUB_CMDLINE_LINUX="...". After editing, regenerate withgrub2-mkconfig -o /boot/grub2/grub.cfg(BIOS) or/boot/efi/EFI/redhat/grub.cfg(UEFI). Alternative:grubby --update-kernel=ALL --args="quiet"applies to every installed kernel without a full regenerate. - Rebuilding initramfs (dracut):
dracut -frebuilds the initramfs for the running kernel. Needed after adding drivers, changing root device, or modifying/etc/dracut.conf.d/*.conf.dracut -f /boot/initramfs-$(uname -r).img $(uname -r)for an explicit target. - GRUB2 password protection:
grub2-setpassword(interactive) sets a bootloader password stored in/boot/grub2/user.cfg. After this, editing GRUB entries at boot requires the password — blocks the unauthenticatedrd.breakrecovery path. - Kernel updates:
dnf update kernelinstalls a new kernel alongside the existing one (RHEL keeps the last 3 by default, configurable viainstallonly_limitin/etc/dnf/dnf.conf).grubby --default-kernelshows the default;grubby --set-default /boot/vmlinuz-...changes it. - Verifying the running kernel:
uname -rshows the running kernel version.rpm -q kernellists installed kernel packages. After a kernel update, reboot and re-checkuname -r.
Task: add the audit=1 kernel parameter persistently. Solution: edit + mkconfig (or grubby) — edit /etc/default/grub, append audit=1 inside GRUB_CMDLINE_LINUX="...", then grub2-mkconfig -o /boot/grub2/grub.cfg (for BIOS) or the EFI variant. Faster equivalent: grubby --update-kernel=ALL --args="audit=1". Reboot, then verify with cat /proc/cmdline — must contain audit=1.
grub.cfg directly — change /etc/default/grub and regenerate. grubby is the shortcut. dracut -f after driver or storage changes.
e on the kernel line; (2) append rd.break enforcing=0 to the linux line and press Ctrl-X to boot; (3) at the switch_root:/# prompt: mount -o remount,rw /sysroot && chroot /sysroot; (4) passwd — set a new password; (5) touch /.autorelabel — forces SELinux to relabel /etc/shadow on next boot, otherwise login still fails; (6) exit; exit — the system reboots, relabels, then reboots again. Verify: log in with the new password; getenforce is back to Enforcing; ls -laZ /etc/shadow shows the correct shadow_t context.
- The
rd.breaksequence: edit kernel line at GRUB → addrd.break→mount -o remount,rw /sysroot→chroot /sysroot→passwd→touch /.autorelabel→ exit, exit. - Switch the default target with
systemctl set-default multi-user.target(CLI) orgraphical.target(GUI); rescue/emergency for repair work. - After any GRUB config edit, run
grub2-mkconfig -o /boot/grub2/grub.cfg(BIOS) or/boot/efi/EFI/redhat/grub.cfg(UEFI) to regenerate the bootloader file.
rd.break password reset, target switching, and grub2-mkconfig paths.Containers & Automation3 lessons
Podman is the RHCSA's container engine — daemonless, rootless, and integrated with systemd. Pull, run, expose, and persist containers across reboots via systemd user services + lingering. Shell scripting basics for batch automation (user creation loops, config sweeps).
📖 Read in-depth chapter ▾
loginctl enable-linger. Plus enough shell-scripting reflex (loops, conditionals, exit codes) to automate the small batch tasks the exam throws at you.
Podman replaces Docker on RHEL. Daemonless, OCI-compatible, and runs rootless by default — the RHCSA expects you to run containers as a regular user, not as root.
- Podman basics: Podman is a daemonless container engine that runs OCI containers. Unlike Docker, it does not require a running daemon.
podman pull registry.access.redhat.com/ubi9/ubidownloads an image.podman imageslists local images. Podman commands mirror Docker syntax. - Running containers:
podman run -d --name myapp -p 8080:80 httpdruns a container in detached mode (-d), with a name (--name), mapping host port 8080 to container port 80 (-p).podman pslists running containers.podman ps -aincludes stopped containers. - Container lifecycle:
podman stop myappsends SIGTERM then SIGKILL after a timeout.podman start myapprestarts a stopped container.podman rm myappremoves a container.podman rmi image-idremoves an image.podman exec -it myapp /bin/bashopens a shell inside a running container. - Rootless containers: Podman runs containers as a regular user without root privileges. Rootless containers use user namespaces for isolation. This is a key security advantage over Docker and is the expected mode on the RHCSA exam. Rootless containers cannot bind to ports below 1024.
- Building images: A
Containerfile(equivalent to Dockerfile) defines the image build.FROM ubi9/ubisets the base image.RUN dnf install -y httpdruns a command during build.EXPOSE 80documents the port.podman build -t myimage .builds the image from the Containerfile.
Task: as user admin (not root), pull ubi9/httpd-24 and run it on host port 8080. Solution: rootless run with -p — as admin: podman pull registry.access.redhat.com/ubi9/httpd-24, podman run -d --name web -p 8080:8080 registry.access.redhat.com/ubi9/httpd-24. Note: rootless can't bind below 1024, so target port 8080 is correct. Test with curl localhost:8080. Open the firewall: sudo firewall-cmd --add-port=8080/tcp --permanent && sudo firewall-cmd --reload.
podman ps -a includes stopped; podman rm only deletes stopped.
Containers vanish on reboot unless wired into systemd. The RHCSA tests both volume mounts (data survives container deletion) and systemd user-services for auto-start.
- Bind mounts & named volumes:
-v /host/path:/container/path:Zmounts a host directory; the:Zrelabels for SELinux private use. Named volumes:podman volume create mydata, then-v mydata:/var/lib/data. Volumes survivepodman rm; the container does not. - SELinux container labels: Bind mounts on RHEL need
:Z(private to one container) or:z(shared across containers) to set thecontainer_file_tlabel. Skipping this causes "permission denied" inside the container even when host permissions look fine. - podman generate systemd:
podman generate systemd --new --name web --filesemits acontainer-web.serviceunit. The--newflag makes the unit recreate the container each start (safer than reattaching to a stopped one). - User services + lingering: Drop the generated unit into
~/.config/systemd/user/, thensystemctl --user daemon-reload && systemctl --user enable --now container-web. By default, user services stop at logout —loginctl enable-linger adminkeeps them running after logout and across reboots. - Quadlet (RHEL 9+): The modern alternative — drop a
.containerfile into~/.config/containers/systemd/, and systemd auto-generates the service ondaemon-reload. Less ceremony thanpodman generate systemd; preferred direction going forward.
Task: run an httpd container as user admin with /home/admin/www serving content, surviving reboot. Solution: volume mount + systemd user service + linger — podman run -d --name web -p 8080:8080 -v /home/admin/www:/var/www/html:Z registry.access.redhat.com/ubi9/httpd-24. Generate the unit: mkdir -p ~/.config/systemd/user; cd ~/.config/systemd/user; podman generate systemd --new --name web --files. systemctl --user daemon-reload, systemctl --user enable --now container-web. As root: loginctl enable-linger admin. Reboot — container comes back automatically.
:Z for SELinux). podman generate systemd --new + --user + loginctl enable-linger for reboot-survival.
The RHCSA doesn't require advanced Bash, but you must be able to write loops that batch-create users, conditionals that check service state, and scripts that run cleanly under cron.
- Bash script structure: Start with
#!/bin/bash(the shebang). Make scripts executable withchmod +x script.sh. Use variables (NAME="server1"), command substitution (DATE=$(date +%F)), and meaningful comments. Always setset -eto exit on errors in production scripts. - Conditionals:
if [ -f /path/file ]; then ... fitests if a file exists.if [ "$VAR" = "value" ]; then ... elif ... else ... fihandles multiple conditions. Use[[ ]]for pattern matching and regex. Common test operators:-d(directory),-r(readable),-z(empty string),-eq(numeric equal). - Loops:
for user in alice bob charlie; do useradd $user; doneiterates over a list.for i in $(seq 1 10); do ... doneiterates over numbers.while read line; do ... done < file.txtprocesses a file line by line. Loops are essential for batch user creation and configuration tasks. - Exit codes: Every command returns an exit code: 0 = success, non-zero = failure.
$?holds the last command's exit code. Useexit 0for success andexit 1for failure in your scripts.&&runs the next command only if the previous succeeded;||runs it only if the previous failed. - Cron + scripts: Combine shell scripts with cron for automated administration: backup scripts, log cleanup, user provisioning, system health checks. Example:
0 2 * * * /root/scripts/backup.sh >> /var/log/backup.log 2>&1runs a backup at 2 AM daily and logs output.
Task: read /root/users.txt (one username per line) and create each user with a random initial password. Solution: while-read loop + useradd + openssl — script: #!/bin/bash\nset -e\nwhile read user; do\n [ -z "$user" ] && continue\n useradd -m "$user"\n pw=$(openssl rand -base64 12)\n echo "$user:$pw" | chpasswd\n echo "$user $pw" >> /root/credentials.txt\ndone < /root/users.txt. chmod +x and run. Verify with id alice. Lock /root/credentials.txt with chmod 600 immediately.
set -e, absolute paths, redirect both streams. while read for file-driven loops; for user in for inline lists.
postgres:16 container as user pgops, with /home/pgops/pgdata persisted, auto-starting on boot even when nobody is logged in. Sequence: (1) as pgops: podman run -d --name pg -v /home/pgops/pgdata:/var/lib/postgresql/data:Z -e POSTGRES_PASSWORD=secret postgres:16; (2) podman generate systemd --new --files --name pg — produces container-pg.service; (3) install the unit: mkdir -p ~/.config/systemd/user && mv container-pg.service ~/.config/systemd/user/; (4) systemctl --user daemon-reload && systemctl --user enable container-pg.service; (5) as root: loginctl enable-linger pgops — required for user services to start without an active session. Verify: reboot; machinectl shell pgops@ then systemctl --user status container-pg shows active (running); podman ps shows the container; psql connects.
- Rootless Podman = run as a normal user; combine with
loginctl enable-linger <user>so the user's services keep running after logout. - Generate a systemd user unit from a running container with
podman generate systemd --name <ctn> --new --files; place under~/.config/systemd/user/and enable withsystemctl --user enable --now <name>. - Bash script essentials:
#!/bin/bashshebang,set -euo pipefailsafety,for var in list; do ... done, and$?to read the previous exit code.
Hands-on
Capstone labs
Four labs that exercise the modules end-to-end. Run each in a RHEL 9, Rocky Linux 9, or AlmaLinux 9 VM (libvirt, VirtualBox, or a cloud free tier) and tear it down when done. These are the patterns the exam recurs on — building them once burns the muscle memory.
Take a fresh 10 GB virtual disk. Create one GPT partition with parted, mark it LVM-flagged. pvcreate on the partition, vgcreate datavg, lvcreate -n datalv -L 4G datavg. Format XFS, mount at /srv/data, persist via UUID in /etc/fstab, verify with mount -a + reboot. Then add a second disk, vgextend, and lvextend -r -L +2G to grow the LV and filesystem in one shot.
Create users alice, bob, carol; create group team; usermod -aG team each (with -a!). Make /shared/team, chgrp team, chmod 2770 (SGID + group rwx). Set default ACLs so new files inherit group rw: setfacl -m d:g:team:rw /shared/team. Verify by touching a file as alice and confirming with getfacl + ls -l that the group is team with group-rw.
dnf -y install httpd; deploy content under /srv/www; set the SELinux file context: semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?" && restorecon -Rv /srv/www. Open the firewall: firewall-cmd --add-service=http --permanent && firewall-cmd --reload. Set the SELinux boolean if httpd connects out: setsebool -P httpd_can_network_connect on. systemctl enable --now httpd. Verify with curl localhost and ls -Z /srv/www.
Forget the root password. At GRUB2, press e, append rd.break, Ctrl+X. At the initramfs prompt: mount -o remount,rw /sysroot, chroot /sysroot, passwd root, touch /.autorelabel, exit twice. Confirm login works after the relabel. Then set a GRUB password to block this on future reboots: grub2-setpassword, verify by trying e at GRUB — it now demands credentials.
Top 4 mistakes candidates make on RHCSA
- fstab entries that don't survive reboot: using device names instead of UUIDs, or skipping
mount -averification before committing. A broken fstab can stop the next boot — fix it before you ever reboot. - usermod -G without -a: silently wipes a user's other supplementary groups. Always
usermod -aG newgroup userand verify withid user. - Ignoring SELinux denials: when a service "looks configured but won't work", check
ausearch -m avc -ts recent. Don't disable SELinux — the exam zero-grades it. Userestorecon,semanage, or a boolean. - Forgetting firewall-cmd --reload after --permanent: permanent rules don't take effect until reload. Always
--permanent && reload, then verify withfirewall-cmd --list-all.
Ready for RHCSA?
Scenario-based practice questions across every EX200 objective. Free, no signup, instant feedback on every answer. Open the Cert Quest path to combine practice with mini-game drills.
Related certifications
Build the Linux career stack
RHCSA is the floor. From here, the natural follow-ons are Linux+ as a vendor-neutral comparison, Docker for containers, and CKA once you move from VMs to Kubernetes.