Domain 7 · System administration

Deploy, configure,
and maintain systems

User and group management, network configuration with nmcli, time services with chrony, automated installation with Kickstart, and container management with Podman on RHEL 9.

14objectives
6topic areas
8quiz questions
nmcliprimary tool

Objectives

What the exam tests

  • Create, delete, and modify local user accounts
  • Change passwords and adjust password aging policies
  • Create, delete, and modify local groups and group memberships
  • Configure superuser access using sudo
  • Configure network interfaces and hostname using NetworkManager
  • Configure time service clients using chrony
  • Install and update software from Red Hat Network or local repository
  • Modify the system bootloader (GRUB2)
  • Work with Kickstart files to automate installation
  • Manage containers using podman and skopeo
  • Run and manage rootless containers
  • Configure container services to start automatically as systemd services
  • Attach persistent storage to a running container
  • Find, inspect, and manage container images

User/group management and networking with nmcli are guaranteed on every RHCSA exam. Containers (Podman) were added to the EX200 objectives and carry significant weight on RHEL 9 exams.

Coverage weight by topic

Users & groups
Critical
Network (nmcli)
Very high
Containers (Podman)
Very high
Time / chrony
High
sudo configuration
High
Kickstart / GRUB2
Medium

Users and groups

User account management

── CREATE ──────────────────────────────────────────────────── useradd alice # create with defaults useradd -u 1500 alice # specific UID useradd -g devs alice # primary group useradd -G wheel,ops alice # supplementary groups useradd -s /bin/bash alice # login shell useradd -d /data/alice alice # home directory path useradd -m -d /data/alice alice # create home if it doesn't exist useradd -c "Alice Smith" alice # GECOS comment field useradd -e "2025-12-31" alice # account expiry date useradd -r myservice # system account (no home, no login) ── MODIFY ──────────────────────────────────────────────────── usermod -aG wheel alice # add to supplementary group (-a required!) usermod -g newgroup alice # change primary group usermod -l newname alice # rename login name usermod -d /newhome -m alice # move home directory usermod -s /sbin/nologin alice # disable interactive login usermod -L alice # lock account (prepends ! to password) usermod -U alice # unlock account usermod -e "2026-06-30" alice # set expiry date usermod -e "" alice # remove expiry date ── DELETE ──────────────────────────────────────────────────── userdel alice # remove user (keeps home dir) userdel -r alice # remove user AND home directory

usermod -aG group user — the -a flag (append) is critical. Without it, -G replaces all supplementary groups, removing the user from every other group.

Passwords and aging policy

# Set or change a password passwd alice # interactive prompt echo "NewP@ss1" | passwd --stdin alice # non-interactive (scripts) # Password aging with chage chage -l alice # list current aging info chage -M 90 alice # max days before password must change chage -m 7 alice # min days between password changes chage -W 14 alice # warn days before expiry chage -I 30 alice # inactive days after expiry before lock chage -E "2026-12-31" alice # account expiry date chage -E -1 alice # remove expiry (never expires) chage -d 0 alice # force password change at next login # Password aging defaults — /etc/login.defs # PASS_MAX_DAYS 99999 # PASS_MIN_DAYS 0 # PASS_WARN_AGE 7
Key fileContents
/etc/passwdUser accounts: name, UID, GID, home, shell (no passwords)
/etc/shadowHashed passwords and aging fields (root-readable only)
/etc/groupGroup names, GIDs, and member lists
/etc/gshadowGroup passwords and administrators
/etc/login.defsDefault values for new accounts (UID ranges, aging defaults)
/etc/skel/Template files copied to new user home directories

Group management

# Create a group groupadd devs groupadd -g 2000 devs # specific GID # Modify a group groupmod -n developers devs # rename group groupmod -g 2100 devs # change GID # Delete a group groupdel devs # Add user to group (use usermod -aG for existing users) usermod -aG devs alice # Temporarily switch primary group for the current session newgrp devs # List group memberships id alice # uid, gid, and all groups groups alice # group names only getent group devs # show group members

sudo — superuser access

# Edit sudoers safely (always use visudo — validates syntax) visudo # edits /etc/sudoers visudo -f /etc/sudoers.d/myfile # edit a drop-in file ── /etc/sudoers syntax ─────────────────────────────────────── # user host=(run_as) commands alice ALL=(ALL) ALL # full sudo for alice %wheel ALL=(ALL) ALL # sudo for wheel group alice ALL=(ALL) NOPASSWD: ALL # no password required bob ALL=(ALL) /usr/bin/systemctl restart httpd # specific cmd only ── Drop-in files (preferred approach) ─────────────────────── # Create /etc/sudoers.d/alice with 0440 permissions echo "alice ALL=(ALL) NOPASSWD: ALL" | tee /etc/sudoers.d/alice chmod 0440 /etc/sudoers.d/alice # Add user to wheel group (members get sudo on RHEL by default) usermod -aG wheel alice

The safest exam approach: add users to the wheel group with usermod -aG wheel username. The %wheel ALL=(ALL) ALL line in /etc/sudoers is enabled by default on RHEL 9.

Network configuration

Network inspection commands

# Show IP addresses ip addr show ip addr show ens3 # specific interface ip a # short alias # Show routing table ip route show ip r # Show link status ip link show ip link show ens3 # Show DNS configuration cat /etc/resolv.conf nmcli dev show | grep DNS # Test connectivity ping -c 4 8.8.8.8 tracepath 8.8.8.8 ss -tulnp # listening sockets

nmcli — NetworkManager command line

── DEVICE STATUS ───────────────────────────────────────────── nmcli device status # list all interfaces and state nmcli device show # detailed info for all devices nmcli device show ens3 # specific device nmcli connection show # list saved connection profiles nmcli connection show --active # only active connections ── CONFIGURE STATIC IP ─────────────────────────────────────── nmcli connection modify "ens3" \ ipv4.method manual \ ipv4.addresses "192.168.1.100/24" \ ipv4.gateway "192.168.1.1" \ ipv4.dns "8.8.8.8,8.8.4.4" # Apply the change nmcli connection down "ens3" && nmcli connection up "ens3" ── CONFIGURE DHCP ──────────────────────────────────────────── nmcli connection modify "ens3" \ ipv4.method auto \ ipv4.addresses "" \ ipv4.gateway "" \ ipv4.dns "" ── CREATE A NEW CONNECTION ─────────────────────────────────── nmcli connection add \ type ethernet \ con-name "myconn" \ ifname ens4 \ ipv4.method manual \ ipv4.addresses "10.0.0.5/24" \ ipv4.gateway "10.0.0.1" \ ipv4.dns "1.1.1.1" nmcli connection up "myconn" ── DELETE / RELOAD ─────────────────────────────────────────── nmcli connection delete "myconn" nmcli connection reload # reload config from disk

Connection names and device names are different things. The connection name (e.g., "ens3") is a NetworkManager profile label; the device name (e.g., ens3) is the kernel interface name. They often match but don't have to.

Hostname configuration

# View current hostname hostname hostnamectl # Set hostname permanently hostnamectl set-hostname server1.example.com # Hostname types hostnamectl set-hostname "server1" --static # kernel hostname (/etc/hostname) hostnamectl set-hostname "My Server" --pretty # human-readable display name hostnamectl set-hostname "server1" --transient # runtime only (lost on reboot) # /etc/hosts — local name resolution # 127.0.0.1 localhost localhost.localdomain # 192.168.1.10 server1.example.com server1

firewalld — basic firewall management

# Check firewall status systemctl status firewalld firewall-cmd --state # List current rules firewall-cmd --list-all firewall-cmd --list-services firewall-cmd --list-ports # Add a service (temporary — lost on reload) firewall-cmd --add-service=http firewall-cmd --add-service=https # Add a service permanently (survives reload and reboot) firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reload # apply permanent rules to runtime # Add a port firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --permanent --remove-port=8080/tcp firewall-cmd --reload # Remove a service firewall-cmd --permanent --remove-service=http

Always use --permanent for rules that must survive a reboot, then follow with --reload. Rules added without --permanent are lost when the firewall reloads.

Time services and chrony

System time commands

# View current date, time, and timezone date timedatectl timedatectl status # Set system time manually timedatectl set-time "2025-04-09 14:30:00" # Set timezone timedatectl set-timezone America/Phoenix timedatectl set-timezone UTC timedatectl list-timezones | grep America timedatectl list-timezones | grep "America/New" # Enable/disable NTP synchronization timedatectl set-ntp true timedatectl set-ntp false # Verify NTP sync status timedatectl show

chrony — NTP client configuration

# Install chrony (usually pre-installed on RHEL 9) dnf install -y chrony # Enable and start chronyd systemctl enable --now chronyd # Main configuration file: /etc/chrony.conf # Add or change NTP servers: # server pool.ntp.org iburst # server 0.rhel.pool.ntp.org iburst # server time.example.com iburst prefer # chronyc commands chronyc tracking # current sync status and offset chronyc sources # list NTP sources and their status chronyc sources -v # verbose source table chronyc sourcestats # source statistics chronyc makestep # force immediate time step (large offset)
chronyc sources columnMeaning
*Currently selected (syncing) source
+Acceptable source (could be used)
-Not selected (not used)
?Unreachable source
xSource marked as a falseticker (bad time)

On the exam: set the NTP server in /etc/chrony.conf, restart chronyd, then verify sync with chronyc sources. Look for * next to the configured server.

Kickstart automated installation

Kickstart overview and file structure

A Kickstart file (.cfg) is a plain-text script that answers all Anaconda installer prompts automatically, enabling unattended RHEL installations.

Boot media
ks= boot option
Anaconda reads .cfg
%pre scripts
Installation
%post scripts
Reboot

Kickstart file — annotated example

# Kickstart file — /root/anaconda-ks.cfg is auto-generated after install ## System installation method install url --url="http://repo.example.com/rhel9" ## Language and keyboard lang en_US.UTF-8 keyboard us ## Timezone timezone America/Phoenix --utc ## Network network --bootproto=dhcp --device=ens3 --onboot=yes --hostname=server1.example.com ## Root password (use openssl passwd -6 to generate) rootpw --iscrypted $6$rounds=656000$... ## Users user --name=alice --groups=wheel --password=$6$... --iscrypted ## Boot loader bootloader --location=mbr --boot-drive=sda ## Disk partitioning clearpart --all --drives=sda autopart # automatic partitioning ## Package selection %packages @^minimal-environment vim-enhanced -firewalld # prefix - removes a package %end ## Post-installation script %post systemctl enable chronyd echo "alice ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/alice %end ## Reboot after installation reboot

Validating Kickstart files

# Install the validator dnf install -y pykickstart # Validate syntax ksvalidator /root/anaconda-ks.cfg # The auto-generated Kickstart from last install lives here: cat /root/anaconda-ks.cfg # Boot a Kickstart from a URL (added to kernel boot line): # inst.ks=http://192.168.1.1/kickstart/server.cfg # inst.ks=ftp://192.168.1.1/ks.cfg # inst.ks=nfs:192.168.1.1:/ks/server.cfg # inst.ks=hd:sdb1:/ks.cfg (from a USB drive)

GRUB2 bootloader management

# View and edit GRUB2 default options cat /etc/default/grub # Regenerate GRUB2 configuration after changes grub2-mkconfig -o /boot/grub2/grub.cfg # BIOS systems grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg # UEFI systems # Set default boot entry grub2-set-default 0 # first entry (zero-indexed) grub2-set-default "Red Hat Enterprise Linux (5.14.0) 9" # View available boot entries grub2-editenv list awk -F\' '/menuentry/{print $2}' /boot/grub2/grub.cfg # Common /etc/default/grub options # GRUB_TIMEOUT=5 — seconds to show menu # GRUB_DEFAULT=0 — default entry index # GRUB_CMDLINE_LINUX="..."— kernel parameters added to all entries # GRUB_DISABLE_RECOVERY=true — hide recovery entries

Container management with Podman

Podman vs Docker — key differences

Podman (RHEL 9 default)

  • Daemonless — no background service required
  • Rootless containers run as regular users
  • OCI-compatible — same image format as Docker
  • Drop-in Docker CLI replacement (most commands identical)
  • Integrates with systemd for service management
  • Supports pods (multiple containers as a unit)

skopeo — image inspection tool

  • Inspect images without pulling them
  • Copy images between registries
  • Delete images from registries
  • Does not require a running daemon
  • Works with Docker and OCI registries

Managing container images

# Search for images podman search nginx podman search --filter=is-official=true nginx # Pull an image podman pull nginx podman pull registry.access.redhat.com/ubi9/ubi:latest podman pull docker.io/library/nginx:1.25 # List local images podman images podman image ls # Inspect an image (metadata, layers, environment) podman inspect nginx podman image inspect nginx # Inspect without pulling (skopeo) skopeo inspect docker://docker.io/library/nginx:latest # Remove images podman rmi nginx podman rmi --all # remove all local images podman image prune # remove dangling (untagged) images # Tag an image podman tag nginx myregistry.com/nginx:v1 # Push an image to a registry podman push myregistry.com/nginx:v1 # Log in to a registry podman login registry.access.redhat.com

Running containers

# Run a container interactively podman run -it ubi9 /bin/bash # Run a container in the background (detached) podman run -d --name webserver nginx # Run with port mapping podman run -d -p 8080:80 --name webserver nginx # host:container port # Run with environment variables podman run -d -e MYSQL_ROOT_PASSWORD=secret --name db mariadb # Run with a persistent volume mount podman run -d \ -v /host/data:/container/data:Z \ --name myapp myimage # :Z sets the SELinux label — required for rootless containers # Run with resource limits podman run -d --memory=512m --cpus=1 nginx # Remove container automatically when it exits podman run --rm -it ubi9 bash

When mounting host directories into rootless containers, always append :Z (or :z for shared) to the volume flag. Without it, SELinux will deny the container access to the host directory.

Container lifecycle management

# List running containers podman ps podman ps -a # all containers including stopped # Start / stop / restart podman start webserver podman stop webserver podman restart webserver # Execute a command inside a running container podman exec -it webserver /bin/bash podman exec webserver cat /etc/nginx/nginx.conf # View container logs podman logs webserver podman logs -f webserver # follow live podman logs --tail=50 webserver # View resource usage podman stats webserver podman top webserver # process list inside container # Remove containers podman rm webserver # must be stopped first podman rm -f webserver # force remove (even if running) podman rm --all # remove all stopped containers # Copy files to/from containers podman cp /local/file.txt webserver:/remote/ podman cp webserver:/etc/nginx/nginx.conf .

Containers as systemd services

On the RHCSA exam you will be asked to configure a container to start automatically at boot. The correct approach for rootless containers is to generate a systemd unit file with podman generate systemd and enable it as a user service.

Create and start the container (as the target user)
podman run -d --name webserver -p 8080:80 nginx
Create user systemd directory
mkdir -p ~/.config/systemd/user/
Generate the systemd unit file
podman generate systemd --name webserver --new --files mv container-webserver.service ~/.config/systemd/user/
Enable the user service
systemctl --user daemon-reload systemctl --user enable --now container-webserver.service
Enable lingering so service starts without login
loginctl enable-linger alice # start user services at boot loginctl show-user alice | grep Linger

loginctl enable-linger is the critical step that is often missed. Without it, user systemd services only start when the user logs in, not at system boot.

Persistent storage for containers

── BIND MOUNTS (host directory → container) ───────────────── # Create and label the host directory mkdir -p /home/alice/webdata # For rootless containers, set correct SELinux label: chcon -t container_file_t /home/alice/webdata # Mount when running podman run -d \ -v /home/alice/webdata:/usr/share/nginx/html:Z \ --name webserver nginx ── NAMED VOLUMES (managed by podman) ───────────────────────── # Create a named volume podman volume create mydata # Use a named volume podman run -d -v mydata:/data --name myapp myimage # List, inspect, remove volumes podman volume ls podman volume inspect mydata podman volume rm mydata podman volume prune # remove unused volumes

Cheat sheet

Most-tested commands — quick reference

Create user
useradd -m -s /bin/bash alice
Set password
passwd alice
Add to group (safe)
usermod -aG wheel alice
Lock / unlock account
usermod -L alice / usermod -U alice
Password expiry info
chage -l alice
Force pw change
chage -d 0 alice
Create group
groupadd devs
List group members
getent group devs
Edit sudoers safely
visudo
Network device status
nmcli device status
Set static IP
nmcli con mod "ens3" ipv4.method manual ipv4.addresses "x.x.x.x/24"
Apply connection
nmcli con up "ens3"
Set hostname
hostnamectl set-hostname srv1.example.com
Add firewall service
firewall-cmd --permanent --add-service=http
Reload firewall
firewall-cmd --reload
Set timezone
timedatectl set-timezone America/Phoenix
Check NTP sync
chronyc sources
Pull container image
podman pull nginx
Run container
podman run -d --name web -p 8080:80 nginx
List containers
podman ps -a
Container logs
podman logs -f webserver
Exec into container
podman exec -it webserver bash
Generate systemd unit
podman generate systemd --name web --new --files
Enable user service
systemctl --user enable --now container-web.service
Enable lingering
loginctl enable-linger alice
Volume with SELinux
podman run -v /host/dir:/ctr/dir:Z ...

useradd / usermod flag reference

Flaguseraddusermod
-u UIDSet UIDChange UID
-g groupPrimary groupChange primary group
-G groupsSupplementary groupsReplace supplementary groups (use with -a!)
-aN/AAppend — must combine with -G
-s shellLogin shellChange shell
-d dirHome directory pathNew home path (use -m to move contents)
-mCreate home if missingMove home directory contents
-c commentGECOS fieldChange comment
-e dateAccount expiryChange expiry
-rSystem accountN/A
-LN/ALock account
-UN/AUnlock account
-l nameN/ARename login name

Container systemd service — exam checklist

Run container as target user
podman run -d --name myapp myimage
Create user systemd dir
mkdir -p ~/.config/systemd/user/
Generate unit file
podman generate systemd --name myapp --new --files mv container-myapp.service ~/.config/systemd/user/
Reload and enable
systemctl --user daemon-reload systemctl --user enable --now container-myapp.service
Enable lingering (boot without login)
loginctl enable-linger $USER

Practice quiz

Question 1 of 8

You want to add user alice to the wheel group without removing her from any other groups. Which command is correct?

usermod -aG wheel alice — the -a (append) flag is mandatory. Without it, usermod -G wheel alice replaces all of alice's supplementary groups with only wheel, effectively removing her from every other group. Options C and D use non-existent flags.

Question 2 of 8

Which command forces user bob to change his password the next time he logs in?

chage -d 0 bob sets the "last password change" date to day 0 (epoch), making the password immediately expired. On next login, the system will require a new password. Option C (passwd --expire) is also valid and does the same thing. Option A (usermod -e 0) sets the account expiry to epoch — it locks the account entirely. Option D (chage -M 0) sets the max days to 0, which means the password is always expired — different use case.

Question 3 of 8

You need to persistently configure interface ens3 with IP 192.168.1.50/24, gateway 192.168.1.1, and DNS 8.8.8.8. Which command sequence is correct?

nmcli con mod modifies the NetworkManager connection profile and persists across reboots. The subsequent nmcli con up applies the changes immediately. Option A (ifcfg files) was the RHEL 7/8 approach but is deprecated on RHEL 9. Options C and D use ip and ifconfig — these are runtime-only and do not survive a reboot.

Question 4 of 8

After configuring a chrony NTP server, how do you verify the system is actively synchronizing with it?

chronyc sources lists all configured NTP sources and their synchronization status. A * symbol next to a server means chronyd is currently syncing from it. Option A confirms the service is running but not whether it is actually syncing. Option C is a command to enable NTP (action), not a verification. ntpstat works with the older ntpd, not chrony.

Question 5 of 8

You run a rootless container with: podman run -d -v /home/alice/data:/app/data --name myapp myimage. The container cannot access the mounted directory. What is the most likely cause?

When mounting host directories into rootless containers on SELinux-enabled systems, you must append :Z (private unshared label) or :z (shared label) to the volume flag. Without it, SELinux will deny the container process access to the host directory. The correct flag is: -v /home/alice/data:/app/data:Z. Rootless containers do support bind mounts, and --privileged is not needed here.

Question 6 of 8

You generate a systemd unit file for a rootless container and enable it with systemctl --user enable --now container-myapp.service. After a system reboot, the container does not start. What step was missed?

loginctl enable-linger username allows the user's systemd session (and therefore user services) to persist after logout and start at boot without requiring the user to log in first. Without lingering enabled, user services only start when the user has an active login session — they stop at logout and do not restart at boot.

Question 7 of 8

Which firewall-cmd command permanently allows HTTP traffic and applies the rule immediately?

--permanent writes the rule to disk (persists reboots) but does not activate it in the running firewall. --reload then loads the permanent rules into the runtime configuration. Option A adds to runtime only — lost on reload. Option C adds to permanent but never activates it immediately. Option D — --now is not a valid firewall-cmd flag.

Question 8 of 8

Which tool should you use to inspect the metadata of a remote container image without pulling it to local storage?

skopeo inspect docker://registry/image:tag queries the registry directly and returns image metadata (labels, environment, layers, etc.) without downloading the image to local storage. podman inspect only works on images already in local storage. podman search lists matching images but does not show detailed metadata. Option D pulls the entire image first — unnecessary if you only need metadata.