๐ŸŽฉ 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

Video: Rocky Linux and the Red Hat Family (Linux 101 video)
Prefer to watch? This short video shows dnf, rpm, the wheel group and nginx on a real Rocky Linux 9 machine, with on-screen captions and no voiceover. It does not cover the firewall or SELinux sections. Click to play, or watch it on YouTube.

Two big families

Linux comes in many distributions (distros), but most servers you will meet belong to one of two families:

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

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:

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.

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

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:

I tried to explain SELinux to my cat.<br>
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

๐Ÿงช 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?

← Lesson 11 · ↑ All lessons