Operating Systems Azure

CentOS Stream 10 Arm64 on Azure User Guide

| Product: CentOS Stream 10 Arm64 on Azure

Overview

This guide covers the deployment and configuration of CentOS Stream 10 on 64-bit Arm (AArch64/arm64) on Microsoft Azure using cloudimg's pre configured virtual machine image from the Azure Marketplace.

CentOS Stream is the continuously delivered distribution that sits immediately upstream of Red Hat Enterprise Linux. It is where the next RHEL 10 minor release is built in the open, so it carries the RHEL 10 package set and the same 6.12 kernel line, and it is the natural platform for anyone who wants to develop or test against RHEL before it ships. This is the native Arm build. It runs on Azure's Arm virtual machine sizes, which are powered by the Azure Cobalt 100 processor (the Dpsv6, Dplsv6 and Epsv6 families), where many workloads deliver the same performance at a lower cost than on x86.

What's included:

  • CentOS Stream 10, native 64-bit Arm (arm64 / AArch64) with the standard 4k page size kernel
  • Every available update applied at build time, and again immediately before capture
  • Automatic security updates installed, armed and enabled, so the machine keeps patching itself
  • Azure Linux Agent (waagent) and cloud-init for Azure integration
  • The Arm serial console corrected to ttyAMA0, so Azure boot diagnostics and the serial console work
  • Chronyd for NTP time synchronisation against the Azure host clock
  • SELinux in enforcing mode
  • BaseOS, AppStream and Extras repositories enabled, so your update path is intact
  • SSH as the only network listener, with no swap on the operating system disk
  • Gen2 Hyper V virtual machine support
  • 24/7 cloudimg support

Platform: Microsoft Azure (Gen2 Hyper V, Arm64, Azure Cobalt 100) Default user: azureuser

CentOS Stream is a rolling release, on Arm

CentOS Stream has no minor releases. It is continuously delivered: VERSION_ID stays 10 for the life of the stream, and every security fix and package update flows into the same stream as it is prepared for the next RHEL 10 minor. There is no frozen point release to pin to and no vault to fall off, so sudo dnf upgrade always moves the machine to the current CentOS Stream 10 package set. CentOS Stream 10 became generally available on 12 December 2024 and is supported until 31 May 2030, the end of the RHEL 10 full support phase.

Because this is the Arm build, it is a genuinely separate product from the x86 CentOS Stream 10 image: it carries aarch64 packages and an aarch64 kernel, and it only runs on Arm virtual machine sizes. It is the standard 4k page size build. The 64k page size kernel is an optional package on this release and is not installed here.

If you need a frozen minor release with a fixed lifecycle rather than a rolling stream, one of the RHEL rebuild images (AlmaLinux, Rocky Linux or Oracle Linux) is the better fit. CentOS Stream is for tracking what RHEL is about to become.

Security posture

This image ships with no known credential. There is no default password and no baked in SSH key:

  • The root account password is locked, so no one can log in as root with a password.
  • No account on the image has a usable password at all.
  • PasswordAuthentication and keyboard interactive authentication are disabled in the SSH daemon, so every login is by key.
  • PermitRootLogin is set to prohibit-password, which sshd -T reports under its older name without-password, and the root password is locked. The image itself ships no authorized_keys at all. cloud-init does copy your own public key into root's authorized_keys at first boot, but behind its standard forced command, so an ssh root@ attempt prints Please login as the user "azureuser" rather than the user "root". and disconnects. Use sudo from your administrator account instead.
  • SSH host keys are removed before capture and regenerated on each machine at first boot, so no two machines you launch share a host identity.
  • /etc/machine-id is cleared before capture and regenerated per machine.
  • The only key that works is the public key you supply when you create the virtual machine. Azure injects it into the administrator account you name at first boot.
  • SELinux is left in enforcing mode, the CentOS default.
  • The Cockpit web console, which the upstream CentOS Stream cloud image ships listening on port 9090, has been removed from this image. SSH on port 22 is the only network listener.

Prerequisites

Before deploying this image, ensure you have:

  1. An active Microsoft Azure subscription
  2. Access to the Azure Portal or Azure CLI
  3. An SSH key pair for Linux VM access
  4. Familiarity with Azure VM management

Recommended VM Size: Standard_D2ps_v6 (2 vCPU, 8 GB RAM, Azure Cobalt 100) or larger. This is an Arm64 image, so it must be launched on an Arm virtual machine size, such as the Dpsv6, Dplsv6 or Epsv6 families. An x86 size such as Standard_B2s will not accept this image.

Step 1: Deploy the Virtual Machine

Option A: Azure Portal

  1. Navigate to the Azure Marketplace and search for "CentOS Stream 10 Arm64 cloudimg"
  2. Select the image and click Create
  3. Configure the basics:
  4. Subscription: Select your Azure subscription
  5. Resource Group: Create new or select existing
  6. Virtual Machine Name: Enter a name for your VM
  7. Region: Select a region that offers Arm sizes
  8. Size: Standard_D2ps_v6 recommended (an Arm64 size)
  9. Under Administrator Account, select SSH public key and enter your key
  10. Under Inbound Port Rules, allow SSH (port 22)
  11. Click Review + Create, then Create

Option B: Azure CLI

Run this on your own workstation, substituting your resource group, VM name and preferred region. Accept the Marketplace terms once per subscription, then create the machine:

az vm image terms accept --urn cloudimg1647283583153:centos-stream-10-arm64:default:latest

az vm create \
  --resource-group myResourceGroup \
  --name my-centos-stream-10-arm64-vm \
  --image cloudimg1647283583153:centos-stream-10-arm64:default:latest \
  --size Standard_D2ps_v6 \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

The --size must be an Arm64 family (Standard_D2ps_v6 above). Azure rejects an x86 size for an Arm image at create time.

Step 2: Connect via SSH

Run these on your own workstation, not on the VM. Find the public IP, then connect:

az vm show --resource-group myResourceGroup --name my-centos-stream-10-arm64-vm --show-details --query publicIps -o tsv
ssh azureuser@<vm-ip>

There is no password to enter. If SSH asks you for one, the key you supplied at create time is not the key your client is offering.

Step 3: Confirm the release and the architecture

Check that the machine is CentOS Stream 10:

cat /etc/centos-release
CentOS Stream release 10 (Coughlan)

The os-release fields carry the same identity in machine readable form. Note that VERSION_ID is 10 with no minor component, because CentOS Stream does not have minor releases:

grep -E '^(NAME|VERSION|ID|VERSION_ID|PLATFORM_ID)=' /etc/os-release
NAME="CentOS Stream"
VERSION="10 (Coughlan)"
ID="centos"
VERSION_ID="10"
PLATFORM_ID="platform:el10"

/etc/redhat-release records the RHEL family lineage and reads the same on CentOS Stream:

cat /etc/redhat-release
CentOS Stream release 10 (Coughlan)

Confirm the kernel, the architecture and the page size. aarch64 is the proof this is the Arm build, and 4096 is the page size this release ships:

uname -srm
getconf PAGESIZE
Linux 6.12.0-267.el10.aarch64 aarch64
4096

The 64k page size kernel is an optional package on EL10 and is deliberately not installed, so the page size above will not change under you on a kernel update:

rpm -q kernel-64k || echo "(kernel-64k absent - this image is the standard 4k page size build)"
package kernel-64k is not installed
(kernel-64k absent - this image is the standard 4k page size build)

Confirm the processor. On an Azure Arm size the BIOS model name identifies the Cobalt 100 host and the core is an Arm Neoverse N2:

lscpu | grep -E '^(Architecture|Vendor ID|Model name|BIOS Model name):'
Architecture:                            aarch64
Vendor ID:                               ARM
Model name:                              Neoverse-N2
BIOS Model name:                         Cobalt 100 None CPU @ 3.4GHz

The terminal shows cat of etc centos-release reporting CentOS Stream release 10 Coughlan, the os-release fields NAME CentOS Stream, VERSION 10 Coughlan, ID centos, VERSION_ID 10 and PLATFORM_ID platform el10, the redhat-release lineage line, uname reporting the 6.12.0-267 el10 kernel on aarch64 with a 4096 byte page size, kernel-64k reported as not installed, and lscpu reporting an ARM Neoverse-N2 Azure Cobalt 100 processor

Step 4: Check the patch level and automatic updates

The image is fully patched at capture. dnf check-update exits 0 when nothing is pending and 100 when updates are waiting, so an exit code of 0 is the check you want:

sudo dnf -q check-update >/dev/null 2>&1; echo "exit=$?"
exit=0

Confirm nothing is waiting on a reboot:

sudo dnf needs-restarting -r
No core libraries or services have been updated since boot-up.
Reboot should not be necessary.

The update path is intact. BaseOS carries the core operating system, AppStream carries the application packages and the security updates that reach them, and Extras carries the CentOS specific additions:

sudo dnf repolist --enabled
repo id                       repo name
appstream                     CentOS Stream 10 - AppStream
baseos                        CentOS Stream 10 - BaseOS
extras-common                 CentOS Stream 10 - Extras packages

A single kernel is installed, the one the machine is running:

rpm -q kernel-core
kernel-core-6.12.0-267.el10.aarch64

Automatic updates

dnf-automatic is installed and its timer is enabled, so the machine applies security updates on its own without any action from you:

systemctl is-enabled dnf-automatic.timer
enabled

It runs daily at 06:00 with up to an hour of random delay, so a fleet does not stampede the mirrors at the same second:

systemctl cat dnf-automatic.timer | grep -E 'OnCalendar|RandomizedDelaySec'
OnCalendar=*-*-* 6:00
RandomizedDelaySec=60m

The policy downloads and applies security updates, and never reboots the machine by itself. An unannounced reboot on a production machine is worse than a deferred kernel, so rebooting is left to you:

grep -E '^(apply_updates|upgrade_type|download_updates|reboot) ' /etc/dnf/automatic.conf
upgrade_type = security
download_updates = yes
apply_updates = yes
reboot = never

To apply everything rather than just security fixes, edit upgrade_type to default in /etc/dnf/automatic.conf. To take all pending updates by hand at any time, run sudo dnf -y upgrade.

The terminal shows dnf check-update returning exit code 0 meaning no updates are pending, needs-restarting reporting no reboot necessary, dnf repolist listing appstream, baseos and extras-common as enabled, dnf-automatic.timer reported as enabled with its six o clock schedule and sixty minute randomised delay, the automatic.conf policy showing upgrade_type security, download_updates yes, apply_updates yes and reboot never, and a single installed kernel-core package

Step 5: Verify the security posture

SELinux is enforcing, the CentOS default, and is persisted in /etc/selinux/config so it survives your reboots:

getenforce
Enforcing

The root account password is locked. L in the second field is the proof:

sudo passwd -S root
root L 2009-12-22 -1 -1 -1 -1

No account on the image ships a usable password. This command prints the name of every account that has one, and prints nothing:

sudo awk -F: '($2 !~ /^[!*]/) && ($2 != "") {print $1}' /etc/shadow

Read the SSH policy from the daemon's own effective configuration rather than from the file, since drop-ins and includes can override what you see in /etc/ssh/sshd_config:

sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractiveauthentication) '
permitrootlogin without-password
pubkeyauthentication yes
passwordauthentication no
kbdinteractiveauthentication no

without-password is the older name for prohibit-password: root may not log in with a password. Since root's password is locked and root has no authorized_keys, there is no root login on a machine you launch.

There is no swap on the operating system disk, so nothing pages onto the OS disk and nothing writes secrets to it:

swapon --show

SSH is the only thing listening off box. Port 22 is the entire TCP listener set, and the only UDP socket is chrony's on the loopback address, which is not reachable from off the machine:

ss -tln
State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0      128          0.0.0.0:22        0.0.0.0:*
LISTEN 0      128             [::]:22           [::]:*
ss -uln
State  Recv-Q Send-Q Local Address:Port Peer Address:Port
UNCONN 0      0          127.0.0.1:323       0.0.0.0:*
UNCONN 0      0              [::1]:323          [::]:*

The upstream CentOS Stream cloud image ships the Cockpit web console enabled and listening on port 9090. It is removed from this image at the package level, so it cannot be re-enabled by accident:

rpm -q cockpit-ws || echo "(cockpit-ws absent - nothing listens on 9090)"
package cockpit-ws is not installed
(cockpit-ws absent - nothing listens on 9090)

rpcbind arrives as a dependency of the NFS client tooling. Both its socket and its service are disabled, so nothing binds port 111:

systemctl is-enabled rpcbind.socket rpcbind.service || true
disabled
disabled

The terminal shows getenforce reporting Enforcing, passwd -S root reporting the root account as L meaning locked, no accounts holding a usable password, the effective sshd policy reporting permitrootlogin without-password, pubkeyauthentication yes, passwordauthentication no and kbdinteractiveauthentication no, swapon showing no swap, ss listing sshd on port 22 as the only TCP listener and chronyd on the loopback address as the only UDP socket, cockpit-ws reported as not installed, and rpcbind socket and service both disabled

Step 6: Verify Azure integration

The Azure Linux Agent is running and enabled at boot. It is what makes VM extensions, the serial console, password reset and boot diagnostics work:

systemctl is-active waagent
systemctl is-enabled waagent
active
enabled

The Azure guest stack is installed from the CentOS repositories:

rpm -q WALinuxAgent cloud-init chrony
WALinuxAgent-2.14.0.1-3.el10.noarch
cloud-init-24.4-7.el10.1.noarch
chrony-4.8-2.el10.aarch64

cloud-init ran to completion on this boot. It is what injected your SSH key, set the hostname and grew the root filesystem:

cloud-init status
status: done

The serial console is on ttyAMA0, the Arm PL011 UART. This matters on Arm: the upstream cloud image inherits the x86 default of ttyS0, a 16550 UART that does not exist on Cobalt 100, and left alone the Azure serial console and boot diagnostics would show nothing at all. This image corrects it, and the fix persists across kernel upgrades:

grep -o 'console=ttyAMA0[^ ]*' /proc/cmdline
console=ttyAMA0,115200n8

Time is synchronised against the Azure host clock by chronyd, and the machine is on UTC:

systemctl is-active chronyd
active
timedatectl | grep -E 'Time zone|System clock|NTP service'
                Time zone: UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active

The disk layout is flat: a 200 MB EFI system partition and an XFS root that cloud-init grows to the full size of the operating system disk on first boot. There is no LVM to unpick. sr0 is the small read-only ISO Azure attaches to pass provisioning data to the machine; it is not storage you use:

lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
NAME    SIZE TYPE FSTYPE MOUNTPOINT
sda      30G disk
├─sda1  200M part vfat   /boot/efi
└─sda2 29.8G part xfs    /
sr0     630K rom
df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda2        30G  2.2G   28G   8% /

To use a larger operating system disk, set the size when you create the VM, or resize the disk in the portal and reboot. The root filesystem follows it automatically.

The terminal shows waagent active and enabled at boot, the WALinuxAgent, cloud-init and chrony package versions from the CentOS repositories, cloud-init status reporting done, the kernel command line carrying console equals ttyAMA0 for the Arm PL011 serial console, chronyd active with the clock synchronised on UTC, and lsblk and df showing a flat layout with a 200 megabyte EFI partition and an XFS root grown to the full operating system disk

Step 7: Networking and the firewall

There is no host firewall in this image. firewalld is not installed:

rpm -q firewalld || echo "(firewalld absent - the Azure NSG is your only firewall)"
package firewalld is not installed
(firewalld absent - the Azure NSG is your only firewall)

The only nftables rules on the machine are the three the Azure Linux Agent installs to protect the Azure wire server endpoint 168.63.129.16. They are present on every Azure Linux image, they only affect traffic to that one address, and they do not filter anything you serve. sudo nft list ruleset shows them:

table ip security {
    chain OUTPUT {
        type filter hook output priority 150; policy accept;
        ip daddr 168.63.129.16 tcp dport 53 counter packets 0 bytes 0 accept
        ip daddr 168.63.129.16 ip protocol tcp skuid 0 counter packets 1133 bytes 353881 accept
        ip daddr 168.63.129.16 ip protocol tcp ct state invalid,new counter packets 0 bytes 0 drop
    }
}

Your Azure Network Security Group is the firewall. It sits in front of the network interface and is the right place to control inbound access, because it drops traffic before it reaches the machine at all. Open only the ports you actually serve, and restrict the source range on SSH to the addresses you administer from.

Add a rule from your own workstation like this:

az network nsg rule create \
  --resource-group myResourceGroup \
  --nsg-name my-centos-stream-10-arm64-vmNSG \
  --name allow-https \
  --priority 1010 \
  --destination-port-ranges 443 \
  --access Allow --protocol Tcp

If you prefer a host firewall as well, install one in the usual way with sudo dnf install -y firewalld and enable it with sudo systemctl enable --now firewalld. Open SSH before you start it, or you will lock yourself out.

Step 8: Install software

AppStream carries the application packages. Everything in the repositories is built for Arm, so dnf resolves aarch64 packages automatically with nothing extra to configure:

sudo dnf -q list --available nginx 2>/dev/null | tail -2
Available Packages
nginx.aarch64                     2:1.26.3-15.el10                     appstream

Install a package and enable it in the usual way, for example sudo dnf install -y nginx followed by sudo systemctl enable --now nginx, then open the port in your Network Security Group.

To add another administrator, create the account with sudo adduser <name>, add it to the wheel group with sudo usermod -aG wheel <name>, then copy your public key into /home/<name>/.ssh/authorized_keys with mode 600 and the .ssh directory mode 700, owned by that user. The image ships with no human accounts at all, so the administrator you named at create time is the only login until you add one.

Common Use Cases

  • Developing and testing against the next RHEL 10 minor release before it ships, on Arm
  • Building and validating packages, containers and RPM builds for the RHEL 10 ecosystem on Arm
  • Arm build and CI runners on cost efficient Azure Cobalt 100 virtual machines
  • Container hosts and cloud native workloads that track a current, continuously delivered EL10 userspace
  • Upstream contribution and QA work on the CentOS Stream distribution itself

Troubleshooting

Cannot connect via SSH

Check the Network Security Group allows inbound TCP 22 from your address, confirm the machine is running, and make sure your client is offering the private key that matches the public key you supplied at create time. Password authentication is disabled, so a password prompt means key negotiation already failed. Root cannot log in over SSH, so connect as the administrator you named at create time.

az vm show --resource-group myResourceGroup --name my-centos-stream-10-arm64-vm --show-details --query powerState -o tsv

The VM will not create or will not boot

This is an Arm64 image. It only runs on Arm virtual machine sizes (the Dpsv6, Dplsv6 and Epsv6 Azure Cobalt 100 families and other p sizes). If Azure rejects the size at create time, confirm you selected an Arm size and not an x86 one such as Standard_B2s or Standard_D2s_v5. If the machine never becomes reachable, open Boot diagnostics and the Serial console in the portal, which read from ttyAMA0 on this image.

Azure agent not running

Check whether the agent is running and read its recent log:

systemctl is-active waagent
sudo journalctl -u waagent -n 20 --no-pager

If it reports anything other than active, start it:

sudo systemctl enable --now waagent

Package manager issues

Clear the metadata cache and rebuild it:

sudo dnf clean all
sudo dnf makecache

If a repository is unreachable, confirm the machine has outbound HTTPS and that DNS resolves, since the CentOS mirror system is reached over both.

An update changed behaviour unexpectedly

CentOS Stream is a rolling distribution: packages move forward continuously as the next RHEL 10 minor is prepared, so a dnf upgrade can bring a newer component than you had yesterday. This is the point of the distribution, not a fault. Pin production workloads that need a frozen package set to a RHEL rebuild image instead, and use dnf history to inspect or undo a transaction:

sudo dnf history list 2>/dev/null | awk 'NR<=5'

A service is blocked and the logs mention SELinux

SELinux is enforcing, which is the secure default and should stay that way. Look at the denial rather than disabling SELinux:

sudo ausearch -m AVC -ts recent </dev/null 2>/dev/null || echo "no recent AVC denials"

Most denials on a fresh machine come from software installed into a non standard path. Relabel with sudo restorecon -Rv /path before considering any policy change.

Important Notes

CentOS Stream is free and open source. It is not covered by a single licence: the distribution is a collection of free and open source licences across its package set, predominantly GPL and LGPL, with MIT, BSD, Apache and others. No subscription, entitlement or licence key is required, and no per seat cost applies.

CentOS is a trademark of Red Hat, Inc. cloudimg is not affiliated with, endorsed by or sponsored by Red Hat, and uses the name only to identify the distribution this image contains. Every package in this image comes from the CentOS Project's own repositories.

CentOS Stream 10 became generally available on 12 December 2024 and is supported until 31 May 2030, the end of the RHEL 10 full support phase. It is continuously delivered and has no minor releases, so there is nothing to pin and nothing to migrate between within the stream.

Support

For assistance with this image, contact cloudimg support: