๐ฉ Lesson 12: The Red Hat Family: Meet Rocky Linux
Everything you learned on Ubuntu also works on Rocky Linux, a free rebuild of Red Hat Enterprise Linux. This lesson shows the handful of things that are different
Two big families
Linux comes in many distributions (distros), but most servers you will meet belong to one of two families:
- The Debian family: Debian, Ubuntu (what the first eleven lessons used), Linux Mint. Software comes as .deb packages, managed by apt.
- The Red Hat family: Red Hat Enterprise Linux (RHEL), and the free rebuilds of it such as Rocky Linux and AlmaLinux, plus Oracle Linux and the fast-moving community distro Fedora. Software comes as .rpm packages, managed by dnf.
Large companies run a lot of RHEL, so there is a good chance you will meet this family at work. Rocky Linux is built from the same source code as RHEL and made to behave identically, but it costs nothing, which makes it the best way to practise. Everything you learned in lessons 1 to 11 (files, permissions, processes, pipes, scripts, systemctl, journalctl, ss, ip) works exactly the same. This lesson is the short list of what differs.
The screens below were captured on a real Rocky Linux 9.4 system container (the same virtual lab as lesson 8, so it has the same limits). Rocky 9 matches RHEL 9.
[rocky@practice ~]$ cat /etc/redhat-release
Rocky Linux release 9.4 (Blue Onyx)
[rocky@practice ~]$ head -4 /etc/os-release
NAME="Rocky Linux"
VERSION="9.4 (Blue Onyx)"
ID="rocky"
ID_LIKE="rhel centos fedora"
[rocky@practice ~]$ hostnamectl
Static hostname: practice
Icon name: computer-container
Chassis: container โ
Machine ID: 90440fca7cec41e6a25e63dea6f7329f
Boot ID: fcb594ce829c49859b99e0dc87c4b258
Virtualization: lxc
Operating System: Rocky Linux 9.4 (Blue Onyx)
CPE OS Name: cpe:/o:rocky:rocky:9::baseos
Kernel: Linux 7.0.2-6-pve
Architecture: x86-64
Firmware Version: 1.16.3-debian-1.16.3-2
- /etc/redhat-release is a one-line answer to "what is this?". /etc/os-release is the modern, standard version of it, found on Ubuntu too. ID_LIKE="rhel centos fedora" says "I am a relative of these".
- Look at the prompt: [rocky@practice ~]$ has square brackets, and no colour or : between the parts. It is only a different default setting, and works just as before.
- The Virtualization: lxc line is the lab container, as in lesson 9.
Packages: dnf instead of apt
On Ubuntu you used apt. On Rocky the tool is **dnf, and the files are **RPM packages. The commands map almost one to one:
- apt search WORD is dnf search WORD
- sudo apt install NAME is sudo dnf install NAME
- sudo apt remove NAME is sudo dnf remove NAME
- sudo apt update (refresh the list) is not needed: dnf refreshes its list by itself when it is old. apt list --upgradable is dnf check-update, and sudo apt upgrade is **sudo dnf update**.
First the same lesson as in lesson 6: a normal user may not install software.
[rocky@practice ~]$ dnf install tree Error: This command has to be run with superuser privileges (under the root user on most systems). [rocky@practice ~]$ sudo dnf install tree [sudo] password for rocky: Last metadata expiration check: 0:03:14 ago on Mon 05 Oct 2026 08:25:12 PM UTC. Dependencies resolved. ================================================================================ Package Architecture Version Repository Size ================================================================================ Installing: tree x86_64 1.8.0-10.el9 baseos 55 k Transaction Summary ================================================================================ Install 1 Package Total download size: 55 k Installed size: 113 k Is this ok [y/N]: y Downloading Packages: tree-1.8.0-10.el9.x86_64.rpm 55 kB/s | 55 kB 00:01 -------------------------------------------------------------------------------- Total 42 kB/s | 55 kB 00:01 Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Installing : tree-1.8.0-10.el9.x86_64 1/1 Running scriptlet: tree-1.8.0-10.el9.x86_64 1/1 Verifying : tree-1.8.0-10.el9.x86_64 1/1 Installed: tree-1.8.0-10.el9.x86_64 Complete!
Without sudo, dnf tells you at once: This command has to be run with superuser privileges. With sudo it asked for the password, looked up the package and printed a plan: what it will install, from which repository (baseos), and how big it is. Is this ok [y/N]: is the question. The capital letter is the default, so just pressing Enter means No. We typed y. Adding -y answers yes for you (handy in scripts, dangerous when you did not read the plan).
The packages are named differently in the two families sometimes, for example Apache is apache2 on Ubuntu and httpd on Rocky. dnf search finds out. If you only know a file or command you want, dnf provides NAME tells you which package contains it (from the documentation, not run here).
Asking rpm about what is installed
dnf is the friendly front-end. Underneath is the low-level tool **rpm**, the equivalent of dpkg. Its question commands are worth knowing:
[rocky@practice ~]$ tree --version tree v1.8.0 (c) 1996 - 2018 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro [rocky@practice ~]$ rpm -q tree tree-1.8.0-10.el9.x86_64 [rocky@practice ~]$ rpm -ql tree /usr/bin/tree /usr/lib/.build-id /usr/lib/.build-id/d0 /usr/lib/.build-id/d0/a010245f25272aff8e82aa32809790125b7f0b /usr/share/doc/tree /usr/share/doc/tree/README /usr/share/licenses/tree /usr/share/licenses/tree/LICENSE /usr/share/man/man1/tree.1.gz [rocky@practice ~]$ rpm -qf /usr/bin/tree tree-1.8.0-10.el9.x86_64 [rocky@practice ~]$ rpm -qi tree Name : tree Version : 1.8.0 Release : 10.el9 Architecture: x86_64 Install Date: Mon 05 Oct 2026 08:28:40 PM UTC Group : Unspecified Size : 115876 License : GPLv2+ and LGPLv2+ Signature : RSA/SHA256, Mon 16 May 2022 12:23:23 PM UTC, Key ID 702d426d350d275d Source RPM : tree-pkg-1.8.0-10.el9.src.rpm Build Date : Mon 16 May 2022 12:21:25 PM UTC Build Host : pb-f6db5b05-253f-46f4-ae18-b2ffd9f128b2-b-x86-64 Packager : Rocky Linux Build System (Peridot) <releng@rockylinux.org> Vendor : Rocky Enterprise Software Foundation URL : http://mama.indstate.edu/users/ice/tree/ Summary : File system tree viewer Description : The tree utility recursively displays the contents of directories in a tree-like format. Tree is basically a UNIX port of the DOS tree utility.
- tree --version shows the program works.
- rpm -q tree asks "is it installed, and which version?". If it is not, it says so.
- rpm -ql tree lists every file the package installed: the program, its documentation and its manual page.
- rpm -qf /usr/bin/tree answers the opposite question: which package does this file belong to? Very useful when you find a strange file and want to know where it came from.
- rpm -qi tree prints the package information: version, size, licence, who built and signed it, and what it is for. The Signature line shows packages are cryptographically signed, as in lesson 6.
Searching, removing and repositories
[rocky@practice ~]$ dnf search nano Last metadata expiration check: 0:07:12 ago on Mon 05 Oct 2026 08:22:09 PM UTC. ========================== Name Exactly Matched: nano ========================== nano.x86_64 : A small text editor
[rocky@practice ~]$ sudo dnf remove -y tree Dependencies resolved. ================================================================================ Package Architecture Version Repository Size ================================================================================ Removing: tree x86_64 1.8.0-10.el9 @baseos 113 k Transaction Summary ================================================================================ Remove 1 Package Freed space: 113 k Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Erasing : tree-1.8.0-10.el9.x86_64 1/1 Verifying : tree-1.8.0-10.el9.x86_64 1/1 Removed: tree-1.8.0-10.el9.x86_64 Complete!
dnf search nano found the editor, and sudo dnf remove shows a plan too, ending in Complete!. We also installed nano the same way (it is not in the minimal Rocky image, as it is not in minimal Ubuntu).
[rocky@practice ~]$ dnf repolist repo id repo name appstream Rocky Linux 9 - AppStream baseos Rocky Linux 9 - BaseOS extras Rocky Linux 9 - Extras [rocky@practice ~]$ dnf check-update | head -8 Last metadata expiration check: 0:08:24 ago on Mon 05 Oct 2026 08:22:09 PM UTC. NetworkManager.x86_64 1:1.54.3-5.el9_8 baseos NetworkManager-libnm.x86_64 1:1.54.3-5.el9_8 baseos acl.x86_64 2.4.0-1.el9_8 baseos alternatives.x86_64 1.24-2.el9 baseos audit-libs.x86_64 3.1.5-8.el9 baseos bzip2-libs.x86_64 1.0.8-11.el9 baseos
dnf repolist shows where software comes from: BaseOS (the core system), AppStream (additional programs) and Extras. This is like Ubuntu's main and universe. Another popular source for the Red Hat family is EPEL, an add-on repository with thousands of extra packages (from the documentation, not run here).
dnf check-update lists the packages that have newer versions. The practice image is older than the current release, so there are many. On a real server you would now run sudo dnf update, and we did not run it because it downloads hundreds of megabytes. Both families get security fixes through this one command, so run it regularly.
Users and sudo: wheel, not sudo
[rocky@practice ~]$ id
uid=1000(rocky) gid=1000(rocky) groups=1000(rocky),10(wheel)
[rocky@practice ~]$ getent group wheel
wheel:x:10:rocky
[rocky@practice ~]$ getent group sudo
[rocky@practice ~]$ sudo whoami
[sudo] password for rocky:
root
[rocky@practice ~]$ sudo -l
Matching Defaults entries for rocky on practice:
!visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin,
env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS",
env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE",
env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES",
env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE",
env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY",
secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin
User rocky may run the following commands on practice:
(ALL) ALL
id shows the user rocky belongs to a group named **wheel**. On Ubuntu the administrators' group is called sudo; on the Red Hat family it is wheel (the old UNIX word for "a big wheel", an important person). getent group wheel lists its members, and getent group sudo prints nothing because that group does not exist here. sudo whoami printed root, and sudo -l shows what you are allowed to do: (ALL) ALL means everything. To make a new user an administrator on Rocky, you add them to wheel: sudo usermod -aG wheel NAME (on Ubuntu, sudo).
Services: installed does not mean running
Install a web server and see what happens. On Ubuntu (lesson 8), installing nginx started it at once. On Rocky:
[rocky@practice ~]$ sudo dnf install -y nginx Last metadata expiration check: 0:06:36 ago on Mon 05 Oct 2026 08:25:12 PM UTC. Dependencies resolved. ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: nginx x86_64 2:1.20.1-28.el9_8.6.rocky.0.1 appstream 38 k Installing dependencies: nginx-core x86_64 2:1.20.1-28.el9_8.6.rocky.0.1 appstream 571 k nginx-filesystem noarch 2:1.20.1-28.el9_8.6.rocky.0.1 appstream 11 k rocky-logos-httpd noarch 90.17-1.el9 appstream 24 k Transaction Summary ================================================================================ Install 4 Packages Total download size: 644 k Installed size: 1.8 M [... the middle of the output is left out to keep this page short ...] Installed: nginx-2:1.20.1-28.el9_8.6.rocky.0.1.x86_64 nginx-core-2:1.20.1-28.el9_8.6.rocky.0.1.x86_64 nginx-filesystem-2:1.20.1-28.el9_8.6.rocky.0.1.noarch rocky-logos-httpd-90.17-1.el9.noarch Complete!
[rocky@practice ~]$ systemctl is-active nginx
inactive
[rocky@practice ~]$ sudo systemctl enable --now nginx
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service โ /usr/lib/systemd/system/nginx.service.
[rocky@practice ~]$ systemctl is-active nginx
active
[rocky@practice ~]$ systemctl is-enabled nginx
enabled
[rocky@practice ~]$ curl -sI http://localhost | head -3
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Mon, 05 Oct 2026 20:32:47 GMT
[rocky@practice ~]$ sudo ss -tlnp | grep ':80'
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=677,fd=6),("nginx",pid=676,fd=6),("nginx",pid=675,fd=6))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=677,fd=7),("nginx",pid=676,fd=7),("nginx",pid=675,fd=7))
[rocky@practice ~]$ systemctl status nginx --no-pager | head -6
โ nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabled)
Active: active (running) since Mon 2026-10-05 20:32:35 UTC; 20s ago
Process: 672 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
Process: 673 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 674 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
After the install, systemctl is-active nginx says **inactive: the Red Hat family **does not start or enable a service just because you installed it. This is a deliberate safety habit. sudo systemctl enable --now nginx starts it now (--now) and makes it start at every boot (enable), and from then on is-active says active and is-enabled says enabled. curl got HTTP/1.1 200 OK from the new server, and ss shows nginx listening on port 80 (lesson 9). Also notice the status line: preset: disabled is exactly this policy. Everything else about systemctl and journalctl is as you learned in lesson 8.
The firewall: firewalld
Rocky's usual firewall tool is firewalld. It groups rules into zones and opens services by name instead of port numbers:
[rocky@practice ~]$ sudo dnf install -y firewalld Last metadata expiration check: 0:07:48 ago on Mon 05 Oct 2026 08:25:12 PM UTC. Dependencies resolved. ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: firewalld noarch 1.3.4-20.el9_8 baseos 452 k Installing dependencies: dbus-libs x86_64 1:1.12.20-8.el9 baseos 151 k firewalld-filesystem noarch 1.3.4-20.el9_8 baseos 8.9 k gobject-introspection x86_64 1.68.0-11.el9 baseos 246 k ipset x86_64 7.11-11.el9_5 baseos 41 k ipset-libs x86_64 7.11-11.el9_5 baseos 68 k iptables-libs x86_64 1.8.10-11.el9_5 baseos 397 k iptables-nft x86_64 1.8.10-11.el9_5 baseos 187 k jansson x86_64 2.14-1.el9.0.1 baseos 45 k libnetfilter_conntrack x86_64 1.0.9-1.el9 baseos 58 k libnfnetlink x86_64 1.0.1-23.el9_5 baseos 28 k libnftnl x86_64 1.2.6-4.el9_4 baseos 87 k nftables x86_64 1:1.0.9-9.el9_8 baseos 453 k python3-dbus x86_64 1.2.18-2.el9.0.1 baseos 131 k python3-firewall noarch 1.3.4-20.el9_8 baseos 355 k python3-gobject-base x86_64 3.40.1-6.el9 baseos 176 k python3-gobject-base-noarch noarch 3.40.1-6.el9 baseos 152 k python3-nftables x86_64 1:1.0.9-9.el9_8 baseos 43 k Installing weak dependencies: libcap-ng-python3 x86_64 0.8.2-7.el9 appstream 29 k Transaction Summary ================================================================================ Install 19 Packages Total download size: 3.0 M Installed size: 11 M [... the middle of the output is left out to keep this page short ...] Installed: dbus-libs-1:1.12.20-8.el9.x86_64 firewalld-1.3.4-20.el9_8.noarch firewalld-filesystem-1.3.4-20.el9_8.noarch gobject-introspection-1.68.0-11.el9.x86_64 ipset-7.11-11.el9_5.x86_64 ipset-libs-7.11-11.el9_5.x86_64 iptables-libs-1.8.10-11.el9_5.x86_64 iptables-nft-1.8.10-11.el9_5.x86_64 jansson-2.14-1.el9.0.1.x86_64 libcap-ng-python3-0.8.2-7.el9.x86_64 libnetfilter_conntrack-1.0.9-1.el9.x86_64 libnfnetlink-1.0.1-23.el9_5.x86_64 libnftnl-1.2.6-4.el9_4.x86_64 nftables-1:1.0.9-9.el9_8.x86_64 python3-dbus-1.2.18-2.el9.0.1.x86_64 python3-firewall-1.3.4-20.el9_8.noarch python3-gobject-base-3.40.1-6.el9.x86_64 python3-gobject-base-noarch-3.40.1-6.el9.noarch python3-nftables-1:1.0.9-9.el9_8.x86_64 Complete!
[rocky@practice ~]$ sudo systemctl enable --now firewalld [rocky@practice ~]$ sudo firewall-cmd --state running [rocky@practice ~]$ systemctl is-active firewalld active
(Rocky servers usually have it installed already. We installed it in the container and used --now to start it.) And the rules in action:
[rocky@practice ~]$ sudo firewall-cmd --get-default-zone [sudo] password for rocky: public [rocky@practice ~]$ sudo firewall-cmd --list-all public (active) target: default icmp-block-inversion: no interfaces: eth0 sources: services: cockpit dhcpv6-client ssh ports: protocols: forward: yes masquerade: no forward-ports: source-ports: icmp-blocks: rich rules: [rocky@practice ~]$ sudo firewall-cmd --permanent --add-service=http success [rocky@practice ~]$ sudo firewall-cmd --reload success [rocky@practice ~]$ sudo firewall-cmd --list-services cockpit dhcpv6-client http ssh [rocky@practice ~]$ sudo firewall-cmd --permanent --remove-service=http success [rocky@practice ~]$ sudo firewall-cmd --reload success [rocky@practice ~]$ sudo firewall-cmd --list-services cockpit dhcpv6-client ssh
- The default zone is public, the one for untrusted networks, and it already allows ssh (and two others).
- --permanent --add-service=http adds the web service to the rules saved to disk, and --reload applies them. --list-services confirms that http is now allowed.
- We then removed it again the same way. Always **say --permanent**, otherwise a rule disappears at the next reload or reboot (from the documentation, not run here).
On Ubuntu the matching tool is ufw (lesson 9 showed which ports are listening; the firewall decides who may reach them). If you installed a web server and nobody can reach it, the firewall is the first suspect.
SELinux: the extra lock
[rocky@practice ~]$ getenforce Disabled [rocky@practice ~]$ sestatus SELinux status: disabled [rocky@practice ~]$ ls -Zd /etc /usr ? /etc ? /usr [rocky@practice ~]$ exit logout [root@practice ~]# exit
The Red Hat family adds a second layer of protection called SELinux. Ordinary permissions (lesson 5) say which user may read a file. SELinux adds rules for what each program may touch, so that a hacked web server still cannot read your home folder. getenforce shows its mode: Enforcing (on), Permissive (log only) or Disabled. Here it says Disabled because the lab container shares the host's kernel, which has no SELinux, and ls -Z shows ? instead of a label. **On a normal Rocky or RHEL server it says Enforcing**, and these are the points you need to know, from the documentation and not run here:
- Files and processes have labels, shown with ls -Z and ps -Z.
- When a program is blocked, the file permissions look fine and the log says **avc: denied**. If something works as root but fails as a service, suspect SELinux.
- sudo setenforce 0 switches to Permissive until the next reboot, as a test to see whether SELinux is the cause. Switching it off permanently is the wrong fix: learn to label the files properly.
It denied everything and logged it.
Network settings: NetworkManager
[rocky@practice ~]$ ip -br address lo UNKNOWN 127.0.0.1/8 ::1/128 eth0@if6 UP 10.0.2.17/24 fec0::be24:11ff:fe34:7c8c/64 fe80::be24:11ff:fe34:7c8c/64 [rocky@practice ~]$ nmcli general status STATE CONNECTIVITY WIFI-HW WIFI WWAN-HW WWAN connected full missing enabled missing enabled [rocky@practice ~]$ ls /etc/NetworkManager/system-connections [rocky@practice ~]$ ls /etc/sysconfig console firewalld network nftables.conf run-parts crond modules network-scripts rsyslog [rocky@practice ~]$ cat /etc/resolv.conf # Generated by NetworkManager search lab.example.com nameserver 10.0.2.3 [rocky@practice ~]$ exit logout [root@practice ~]# exit
ip -br address is the same command as in lesson 9. The settings tool differs: Rocky uses NetworkManager. nmcli general status shows its state (connected), and nmcli can list and change connections. The comment # Generated by NetworkManager in /etc/resolv.conf says who writes the DNS settings. Ubuntu Server uses netplan instead. You rarely need to edit these by hand.
The cheat sheet
- Same on both: ls, cd, cp, grep, pipes, chmod, systemctl, journalctl, ip, ss, df, free, ssh, scripts, nano.
- Packages: dnf search, sudo dnf install, sudo dnf remove, dnf check-update, sudo dnf update. rpm -q, rpm -ql, rpm -qf FILE, rpm -qi.
- Administrators: group **wheel** (Ubuntu: sudo).
- Services: installing does not start. sudo systemctl enable --now NAME.
- Firewall: sudo firewall-cmd --list-all, --permanent --add-service=NAME, --reload.
- SELinux: getenforce, ls -Z, setenforce 0 to test only. Never leave it off.
- Network: nmcli. Release file: /etc/redhat-release.
๐งช Try it yourself
On a Rocky Linux (or AlmaLinux) machine or VM:
1. Find out its exact version in two different ways. 2. Which package does /usr/bin/ls belong to? (Use rpm -qf.) 3. Install tree and nano, list the files of tree, then remove tree again. 4. Is your user in the wheel group? 5. Install nginx, check it is not running, then start it and enable it. Test with curl -I localhost. 6. List the firewall's open services. Is ssh there? 7. What does getenforce say?