Rocky Linux 10 and AlmaLinux 10 don’t boot into the same server, and the gap decides what you should fix first. Our older Rocky Linux 10 post install guide says the opposite: “Everything works on AlmaLinux 10 and RHEL 10 as well since all three share the same package set.” The packages are close. The defaults around them aren’t: AlmaLinux ships CRB enabled, Rocky’s cloud image answers on port 9090 with no firewall in front of it, and only one of the two keeps its logs across a reboot.
This list covers the things to do after installing Rocky Linux 10 or AlmaLinux 10, and it says which distro needs each step. You’ll fix the repository order, patch, add a firewall and decide on Cockpit, then handle the SELinux change that makes /var/opt behave like /opt, root SSH login, the journal, tuned and security updates. Everything below was run on Rocky Linux 10.2 and AlmaLinux 10.2 GenericCloud images in October 2026, with SELinux enforcing on both.
Rocky Linux 10 and AlmaLinux 10 ship different defaults
Both images come up with SELinux enforcing, password SSH disabled and chronyd syncing the clock, so the basics match. Almost everything else in the first hour differs. We booted each vendor’s official cloud image, logged in as the cloud-init user, and recorded what was installed and enabled before touching anything:
| Fresh cloud image | Rocky Linux 10 | AlmaLinux 10 |
|---|---|---|
| CRB repository | disabled | enabled |
| firewalld | not installed | not installed |
| Cockpit | cockpit-ws installed, socket enabled, 9090 listening | not installed |
| tuned | not installed | installed, virtual-guest profile active |
| semanage (policycoreutils-python-utils) | installed | not installed |
| Persistent journal (/var/log/journal) | yes | no, logs live in /run |
| Root SSH login | key only (without-password) | key only (without-password) |
| epel-release in the extras repo | 10-7.el10_1 | 10-6.el10 |
Read the table as a to-do list split by distro, because each image is missing different things:
- Rocky: enable CRB, deal with the Cockpit socket, add tuned
- AlmaLinux: persist the journal, install semanage
- Both: install firewalld, block root SSH login
An ISO install with a different package group will not match this table line for line. Run the same checks on your own machine before you follow any step that assumes a default.
dnf repolist --all | grep -E '^(crb|epel) '
rpm -q firewalld cockpit-ws tuned policycoreutils-python-utils
systemctl is-enabled firewalld cockpit.socket tuned
ls -d /var/log/journal
A line that says not installed or not-found is a step you can’t skip below. If cockpit.socket prints enabled, you’re on a box that already serves a login page on 9090, and the Cockpit section further down matters more to you than the rest.
The hostname and timezone go first
Cloud images name the host after the VM template or leave it as localhost, which is what our AlmaLinux image did. Every log line, shell prompt and Cockpit title uses that name, so set it before anything starts writing logs. Both machines also booted in UTC. Change the zone if your logs need to line up with local time.
sudo hostnamectl set-hostname rocky01
sudo timedatectl set-timezone Africa/Nairobi
Confirm both settings. hostnamectl also prints the release and running kernel, which you’ll want before the update step:
hostnamectl | grep -E "Static hostname|Operating System|Kernel"
timedatectl | grep "Time zone"
On the Rocky box this printed the new name and the stock image kernel. Note that kernel string, because the update step replaces it:
Static hostname: rocky01
Operating System: Rocky Linux 10.2 (Red Quartz)
Kernel: Linux 6.12.0-211.16.1.el10_2.0.1.x86_64
Time zone: Africa/Nairobi (EAT, +0300)
Use alma01 or your own name on the AlmaLinux machine. The commands are identical on both, and the Alma image printed localhost before the change, which is the name you’d otherwise see in every log it ships.
Updates end with a reboot check
A cloud image is a snapshot, so it’s always behind. The AlmaLinux image we pulled was published on 12 October and still had 21 upgrades plus a new kernel to install, glibc among them. Refresh the metadata and upgrade everything in one pass:
sudo dnf upgrade --refresh -y
Don’t guess whether that needs a reboot. The needs-restarting plugin is already installed on both distros as part of dnf-plugins-core. Its exit code is scriptable, and 1 means reboot.
sudo dnf needs-restarting -r
On AlmaLinux it named the three packages that forced it. glibc alone is enough, since every running process still has the old library mapped:
Core libraries or services have been updated since boot-up:
* glibc
* kernel
* kernel-core
Reboot is required to fully utilize these updates.
More information: https://access.redhat.com/solutions/27943
Then close the loop in this order, because each check only means something after the one before it. You’re done when all three agree:
- Reboot with
sudo systemctl reboot - Compare
uname -rwith the newest entry inrpm -q kernel-core - Run
sudo dnf needs-restarting -ragain and expect exit 0
If step 2 shows an older kernel than the one dnf installed, the boot entry is the problem, not the update. That is exactly what happened on our Rocky VM.
The Rocky VM taught us why that comparison matters. Proxmox’s cloud-init runs a package upgrade on first boot, and we powered the VM off about a minute in to add an SSH key. The new kernel package was registered, but its boot entry never got written, so after the reboot the machine was still on the old kernel and grubby still pointed there:
sudo grubby --default-kernel
rpm -q kernel-core
If grubby names an older kernel than the newest one rpm lists, reinstalling that newest package rewrites the boot entry. Substitute the version from your own rpm -q output:
sudo dnf reinstall -y kernel-core-6.12.0-211.67.1.el10_2
sudo grubby --default-kernel
After that and one more reboot, both machines ran 6.12.0-211.67.1.el10_2. The lesson is simple: don’t hard-stop a cloud VM while its first boot is still installing packages.
CRB must come before EPEL
EPEL is where tools like htop, fail2ban and many monitoring agents come from, and a lot of its packages build against the CodeReady Builder (CRB) repo. On version 10 a wrong order also costs you SELinux policy, and three facts combine to cause it:
/usr/bin/crbships insideepel-release, not in the base OSepel-releaserecommendsselinux-policy-extra, which lives in CRB- dnf silently drops a recommended package it can’t find
Red Hat moved the SELinux modules for EPEL-only software into selinux-policy-targeted-extra in the CRB repo, so that enabling EPEL can pull them in automatically (Considerations in adopting RHEL 10, SELinux chapter). That only happens when CRB is already on at the moment you install epel-release. On Rocky, enable CRB first with config-manager, which needs nothing beyond the dnf-plugins-core both images already have.
sudo dnf config-manager --set-enabled crb
sudo dnf install -y epel-release
With CRB enabled, the transaction pulls the extra policy in as a dependency and its metapackage as a weak dependency. Both come from crb, which is why the order matters:
Installing:
epel-release noarch 10-7.el10_1 extras 19 k
Installing dependencies:
selinux-policy-targeted-extra noarch 42.1.18-4.el10_2.4 crb 719 k
Installing weak dependencies:
selinux-policy-extra noarch 42.1.18-4.el10_2.4 crb 35 k
On AlmaLinux, skip the first line, because CRB is already enabled. sudo dnf install -y epel-release alone produced the same three packages there. Alma’s extras repo carries the older 10-6.el10 build of epel-release, and the next upgrade replaces it from EPEL itself on both distros. The whole Rocky sequence, including the old order failing, looked like this:

Our dedicated CRB repository guide covers RHEL subscriptions too, and the EPEL install guide for RHEL 10 lists the repo layout.
Error: “sudo: /usr/bin/crb: command not found”
That’s what sudo /usr/bin/crb enable prints on a fresh Rocky Linux 10 machine, because the script arrives with epel-release. If you followed the older order and installed epel-release first, it installed alone, printed a hint to enable CRB, and skipped the extra policy. We checked: rpm -q selinux-policy-targeted-extra returned “not installed”. Enabling CRB afterwards does not fix that retroactively. Install the two packages by hand.
sudo dnf config-manager --set-enabled crb
sudo dnf install -y selinux-policy-targeted-extra
sudo dnf install -y selinux-policy-extra
Without them, an EPEL daemon that relies on one of those modules runs in the wrong domain or gets denied, and the AVC log is the only place that tells you why.
Several version 9 habits stop working
If you’re coming from Rocky or AlmaLinux 9, some of your muscle memory is now wrong. These are the changes from the RHEL 10 adoption guide that matter for this setup. We saw the first three on both machines; the rest come from the guide:
| Change since 9 | What you’ll notice |
|---|---|
| dnf skips filelists metadata | The first file-path lookup downloads it on demand |
| Modularity deprecated, no modular content | dnf module list prints a deprecation warning |
| ifcfg files removed | No /etc/sysconfig/network-scripts; connections are NetworkManager keyfiles |
| firewalld lockdown removed | The lockdown option and its allow list are gone |
| cgroups v1 removed | systemd boots in cgroups v2 only |
| nscd removed | Name caching moves to systemd-resolved and sssd |
| x86-64-v3 baseline | CPUs without AVX2 can’t run the default build |
The filelists change is the one you’ll feel. On the Rocky VM, the cache held no filelists before this lookup:
time (sudo dnf provides /usr/sbin/semanage)
The first run took 55 seconds because dnf fetched filelists for every enabled repo, EPEL included. The second took half a second. That trade is fine on a server you configure once. If you run dnf provides constantly, Red Hat documents adding optional_metadata_types=filelists under [main] in /etc/dnf/dnf.conf; the option is empty on both distros. Network connections tell the same story about ifcfg, since both images keep theirs as keyfiles.
nmcli -g NAME,FILENAME connection show
The connection file sits under NetworkManager, not under the old network-scripts directory:
cloud-init eth0:/etc/NetworkManager/system-connections/cloud-init-eth0.nmconnection
lo:/run/NetworkManager/system-connections/lo.nmconnection
Edit connections with nmcli or nmtui. NetworkManager no longer reads ifcfg files, so one copied from a version 9 box won’t configure anything.
Neither cloud image runs a firewall
Both GenericCloud images leave firewalld out, on the assumption that a cloud security group does the filtering. That does not hold on Proxmox, a home lab or a VPS with a public address, so install it and start it on boot.
sudo dnf install -y firewalld
sudo systemctl enable --now firewalld
Now list what the default zone lets in. Nothing in either image customises firewalld, so this is the stock policy:
sudo firewall-cmd --list-all
Both distros came up with the same public zone on eth0:
public (default, active)
target: default
ingress-priority: 0
egress-priority: 0
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:
Look at the services line, because it is the whole policy for that interface. Anything you don’t see there is rejected:
ssh: TCP 22dhcpv6-client: UDP 546, for IPv6 address leasescockpit: TCP 9090
The default zone allows Cockpit, so firewalld on its own does not close port 9090 on Rocky. It only stops other ports from opening by accident, and the next section decides what happens to 9090. For zones, rich rules and per-interface policy, see our firewalld walkthrough.
Rocky’s image leaves Cockpit listening
The Rocky cloud image installs four Cockpit packages (cockpit-ws, cockpit-system, cockpit-bridge and cockpit-ws-selinux) and enables cockpit.socket. Before we added firewalld, curl from a second machine on the LAN got an HTTP 200 login page from port 9090. The cloud user has no password, so nobody could log in with it, but any local account with a password could, and you didn’t ask for that listener. The AlmaLinux image has no Cockpit at all.
Decide whether you want the web console, and make the listener and the firewall agree with that decision:
- No console: disable the socket, drop the zone service
- Console wanted: install
cockpit, keep the zone service
The first case, run on Rocky, turns the socket off and removes the service from the zone in one go.
sudo systemctl disable --now cockpit.socket
sudo firewall-cmd --permanent --remove-service=cockpit
sudo firewall-cmd --reload
sudo firewall-cmd --list-services
The zone keeps only SSH and the DHCPv6 client, and ss -tln no longer shows 9090. The socket unit is disabled as well, so a reboot won’t bring the listener back:
dhcpv6-client ssh
If you do want it, install the full package and enable the socket. That’s 32 packages on the AlmaLinux image, which ended up with Cockpit 356 listening on 9090:
sudo dnf install -y cockpit
sudo systemctl enable --now cockpit.socket
Cloud images create the login user without a password, so give yours one first with sudo passwd followed by the user name. Then log in at https://10.0.1.51:9090 and accept the self-signed certificate warning, which Cockpit shows until you install a real certificate. Our Cockpit dashboard guide walks through the panels, certificates and adding more hosts.
SELinux now treats /var/opt as /opt
Leave SELinux enforcing. Both images ship that way and nothing in this list needs it off. What changed is how you label /var/opt. Starting with the 10.2 point release, the policy declares /var/opt equivalent to /opt, and equivalency beats any rule written for the /var/opt path itself. Software that keeps state there (Red Hat names logrotate and monitoring agents) inherits usr_t unless you label the matching /opt path instead.
AlmaLinux does not ship semanage, so install it there first. Rocky already has it, along with setroubleshoot-server.
sudo dnf install -y policycoreutils-python-utils
Now try the rule the way you would have written it on version 9, for a log directory under /var/opt/myapp:
sudo mkdir -p /var/opt/myapp/logs
sudo semanage fcontext -a -t var_log_t "/var/opt/myapp/logs(/.*)?"
Both distros refuse it, and the error message already names the rule you should write instead:
ValueError: File spec /var/opt/myapp/logs(/.*)? conflicts with equivalency rule '/var/opt /opt'; Try adding '/opt/myapp/logs(/.*)?' instead
Write the rule against /opt, then run restorecon on the real /var/opt directory. That is the recipe Red Hat gives, and the files never move.
sudo semanage fcontext -a -t var_log_t "/opt/myapp/logs(/.*)?"
sudo restorecon -Rv /var/opt/myapp/logs
ls -Zd /var/opt/myapp/logs
The directory moves from usr_t to var_log_t. The rule names the /opt path, yet the label lands on /var/opt, which is the equivalency doing its job:

What happens on a machine you upgraded from an earlier 10.x with its own /var/opt rules? Those rules are ignored after the upgrade, so the service that used them starts collecting denials. Red Hat’s note points at these:
- rules you added with
semanagefor/var/optpaths - third-party packages that ship their own
/var/optcontexts - logrotate or monitoring agents writing under
/var/opt
Check with sudo ausearch -m avc -ts recent and re-create each rule on the /opt side. Our SELinux troubleshooting guide covers reading those denials.
Root can still log in by key
Before you change anything here, confirm you’re logged in as a non-root user with sudo and a working key, because this step removes root as a way back in. Password logins are already off on both images, because cloud-init writes PasswordAuthentication no into /etc/ssh/sshd_config.d/50-cloud-init.conf. Root login is not off. The effective setting is without-password, so a key in /root/.ssh/authorized_keys logs you straight in as root. sshd_config pulls in sshd_config.d/*.conf near its top, and sshd keeps the first value it reads for each option. Drop-ins load in name order, so a file you name to sort before 50-cloud-init.conf wins:
echo "PermitRootLogin no" | sudo tee /etc/ssh/sshd_config.d/10-root-login.conf
sudo sshd -t && sudo systemctl reload sshd
Keep your current session open and confirm the running values from a second terminal before you log out. If the reload broke something, the open session is how you fix it.
sudo sshd -T | grep -E "^(permitrootlogin|passwordauthentication) "
Both should read no. If permitrootlogin still shows without-password, another drop-in sorts ahead of yours and sets it first:
permitrootlogin no
passwordauthentication no
Moving SSH off port 22 needs an SELinux port label as well as a firewall rule, which our SSH port change guide covers.
AlmaLinux keeps the journal in memory
With the Storage=auto default of the systemd build these releases ship, journald stores logs on disk only when /var/log/journal exists. The Rocky image has that directory. The AlmaLinux image does not, so its journal lives in /run and is gone after every reboot. We noticed because journalctl --list-boots on Alma showed one boot after two, while Rocky kept all four of its boots. A kernel panic you can’t read afterwards is the classic cost. Create the directory with the right ownership, restart journald, then ask it to move what’s already in /run to disk:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
sudo journalctl --flush
Check the label and the size on disk. The tmpfiles step matters because it gives the directory the systemd-journal group and setgid bit that a bare mkdir leaves out:
ls -Zd /var/log/journal
sudo journalctl --disk-usage
The directory carries var_log_t, and after the flush the boot’s system.journal sat under /var/log/journal:
system_u:object_r:var_log_t:s0 /var/log/journal
Archived and active journals take up 16M in the file system.
There’s nothing to do on Rocky here, which is one of the few places it’s ahead of Alma. Leave the default size limits alone unless the disk is small, since journald caps itself at a share of the filesystem.
tuned ships only on AlmaLinux
AlmaLinux’s image runs tuned with the virtual-guest profile from the first boot. Rocky’s does not include it, and installing it pulled 70 packages on our VM, most of them Perl modules dragged in by its helper tools. That’s still worth it on a VM, because virtual-guest tunes dirty-page writeback and swappiness for a guest:
sudo dnf install -y tuned
sudo systemctl enable --now tuned
sudo tuned-adm recommend
sudo tuned-adm profile virtual-guest
tuned-adm active
recommend picked virtual-guest on the Proxmox VM, and active confirmed it. The profile builds on throughput-performance, then sets vm.swappiness to 30 and dirty memory to 30%:
Current active profile: virtual-guest
On bare metal, use whatever tuned-adm recommend prints rather than copying the VM profile.
Security updates install on a timer
dnf-automatic installs with every timer disabled on both distros, and its stock config downloads updates without ever applying them. Three lines in /etc/dnf/automatic.conf decide what it does:
upgrade_type = default, meaning every updatedownload_updates = yeswithapply_updates = noreboot = never
Limit it to security errata, then enable the timer variant that installs. dnf-automatic-install.timer passes --installupdates, which applies updates whatever apply_updates says, so you don’t need to edit that line.
sudo dnf install -y dnf-automatic
sudo sed -i "s/^upgrade_type = default/upgrade_type = security/" /etc/dnf/automatic.conf
sudo systemctl enable --now dnf-automatic-install.timer
systemctl list-timers dnf-automatic-install.timer --no-pager
The timer fires at 06:00 with up to an hour of random delay, so a fleet doesn’t hit the mirrors at once:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Fri 2026-10-23 06:44:46 EAT 19h - - dnf-automatic-install.timer dnf-automatic-install.service
Don’t wait a day to find out whether it works. Start the service once by hand with sudo systemctl start dnf-automatic-install.service, then read journalctl -u dnf-automatic-install.service. Ours finished in under a second of CPU because the box was already patched. reboot = never stays as it is, so pair the timer with dnf needs-restarting -r in your monitoring.
Where Rocky and AlmaLinux actually differ
So is the older claim wrong? Partly. Both rebuild the same RHEL sources, our two machines ran the same kernel and SELinux policy builds after updates, and every command above worked on both. The difference is in the defaults each project picks and in what AlmaLinux adds on top of RHEL. The AlmaLinux 10.2 release notes list those deviations, and the last two rows come from our own images.
| Item | Rocky Linux 10 | AlmaLinux 10 |
|---|---|---|
| CRB repository | Disabled until you enable it | Enabled by default |
| CPU baseline | x86-64-v3 only | x86-64-v3, plus a separate x86-64-v2 build for older hardware |
| EPEL for x86-64-v2 | Not applicable | Rebuilt by AlmaLinux |
| 32-bit userspace | No | i686 userspace, no i686 kernel or installer |
| Btrfs | No | Supported, including booting from Btrfs |
| Cloud image extras | Cockpit socket on, semanage, persistent journal | tuned on, volatile journal |
| Kernel release tag | 6.12.0-211.16.1.el10_2.0.1 in the image | 6.12.0-211.64.1.el10_2 in the image |
The CPU row is the one that can stop an install, so check what your hardware supports before you pick. The dynamic loader reports it without installing anything.
/usr/lib64/ld-linux-x86-64.so.2 --help | grep -A4 Subdirectories
Our Proxmox host, an 11th-gen Intel Core i7 passed through with cpu=host, supports every level up to v4:
Subdirectories of glibc-hwcaps directories, in priority order:
x86-64-v4 (supported, searched)
x86-64-v3 (supported, searched)
x86-64-v2 (supported, searched)
The loader lists every level whether or not the CPU has it, so read the suffix, not the name. If the x86-64-v3 line does not end in (supported, searched), the default builds of both distros won’t run, and AlmaLinux’s v2 build is the only one of the two that will. Run this check from whatever OS is already on the machine (any glibc 2.33 or newer prints the list), because a v3 build can’t boot there to run it. On a VM, check the CPU type first: a generic model hides features the host has, so set it to host before you blame the hardware. On anything newer, the choice comes down to defaults you’ve now changed anyway. Our Rocky vs AlmaLinux vs RHEL comparison covers governance, support terms and release timing.
Prove it from outside
Every check so far ran on the server itself, which only tells you what the server believes. A scan from another machine tells you what an attacker on your network sees. We ran nmap from a third VM against the Rocky box (Cockpit removed) at 10.0.1.50 and the Alma box (Cockpit kept) at 10.0.1.51:
sudo nmap -Pn -p 22,80,443,9090 10.0.1.50 10.0.1.51
Each host shows exactly the ports you chose to open, and nothing else answers. Ports 80 and 443 read filtered on both because firewalld rejects them with an ICMP admin-prohibited reply, which nmap --reason prints as admin-prohibited:

On Rocky, 9090 now reads filtered. Before this setup the same port returned a Cockpit login page to any machine on the LAN. On Alma, 9090 is open because you chose Cockpit there. Each state means something different for your firewall:
open: a service listens and the zone lets you infiltered: firewalld rejected or dropped the probeclosed: the probe got through, nothing listens
If a port you closed shows open, check firewall-cmd --list-all for the zone the interface is actually in. A closed result where you expected filtered means the firewall rule is the missing part, even though nothing answered this time.