Operating Systems Azure

AlmaLinux 9 Essential Eight Hardened on Azure User Guide

| Product: AlmaLinux 9 Essential Eight Hardened on Azure

Overview

This guide covers the deployment and verification of AlmaLinux 9 Essential Eight Hardened on Microsoft Azure using cloudimg's pre configured virtual machine image from the Azure Marketplace.

AlmaLinux is a community driven enterprise Linux distribution, built by the AlmaLinux OS Foundation to be binary compatible with Red Hat Enterprise Linux 9. This edition applies the SCAP Security Guide profile Australian Cyber Security Centre (ACSC) Essential Eight (xccdf_org.ssgproject.content_profile_e8) at build time with oscap xccdf eval --remediate, using the same open source engine and the same freely available policy content you can run yourself. The machine is then rebooted and re evaluated cleanly, with no remediation flag, to produce a measured score that reflects what the machine actually boots into.

This is a hardened image that ships its own evaluation. It is not a certification, an accreditation, or a compliance guarantee. Compliance with the Essential Eight is an organisational determination about a whole environment, and no machine image can confer it. What this image gives you is a measurement you can reproduce: the exact policy content, its checksum, the profile identifier, the scanner version, the rules that were excluded and why, and the score. Read Step 4 before you rely on the number for an audit.

Measured on the shipped image (October 2026 build, scap-security-guide 0.1.82):

  • XCCDF score: 100.0 against the Essential Eight profile, measured with no remediation flag and re-measured on a freshly deployed VM before release
  • 91 rules evaluated: 87 pass, 3 not applicable, 1 not checked, none failing
  • 3 rules waived, all of them sudo rules, named in the image and in Step 3
  • There is no upstream "pass score" for this profile. 90.0 is cloudimg's own threshold for shipping this image, and the build fails if the score falls below it.

Manifest and shipped evidence: the score, ARF, HTML report and machine readable manifest that ship on every VM

What's included:

  • AlmaLinux 9.8, binary compatible with RHEL 9, hardened with AlmaLinux's own product native SCAP content (ssg-almalinux9-ds.xml) from the distribution's free AppStream repository, with no subscription, no CIS membership and no licence key required
  • 550 installed packages on a fresh build, including scap-security-guide, openscap-scanner, audit and aide
  • The build time ARF results, HTML report and measured score shipped at /var/lib/cloudimg/hardened/
  • A machine readable manifest at /usr/share/cloudimg/hardened/manifest.json pinning the datastream, its SHA-256, the profile id, the content package version, the scanner version, both exclusion lists and the score
  • A second, independent re evaluation that runs on your own VM shortly after first boot, and weekly after that, audit only and never remediating
  • Every available update applied at build time, verified with a zero pending check, and unattended updates already armed
  • SELinux in enforcing mode, both at runtime and persisted for the next boot
  • auditd enabled with the profile's ruleset loaded, and an AIDE database built immediately before capture, so it describes the image you actually receive
  • Sudo stays passwordless for your SSH key authenticated account. See Step 3, this is a deliberate, documented exception
  • Azure Linux Agent (waagent) and cloud-init, proven to still work under the hardening on a freshly provisioned VM
  • Gen2 Hyper-V virtual machine support
  • 24/7 cloudimg support

Platform: Microsoft Azure (Gen2 Hyper-V) Default user: azureuser

Step 1: Deploy the Virtual Machine

Option A: Azure Portal

  1. Navigate to the Azure Marketplace and search for "AlmaLinux 9 Essential Eight Hardened cloudimg"
  2. Select the image and click Create
  3. Configure the basics:
  4. Subscription and Resource group: your own
  5. Virtual machine name: your choice
  6. Region: any region you deploy into
  7. Size: Standard_B2s or larger
  8. Authentication type: SSH public key (this image has no password on any account, key authentication is the only way in)
  9. Username: azureuser
  10. Under Inbound port rules, allow SSH (22) only
  11. Review and create

Option B: Azure CLI

az group create --name my-alma-e8-rg --location eastus

az vm create \
  --resource-group my-alma-e8-rg \
  --name my-alma-e8-vm \
  --image cloudimg:almalinux:alma9e8:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Connect once the VM is running:

ssh azureuser@<public-ip>

Step 2: Confirm the Image Is What It Claims

Everything below runs as azureuser on your own VM. Nothing here needs the internet.

Read the manifest first. It is the single machine readable record of what was evaluated:

cat /usr/share/cloudimg/hardened/manifest.json

You should see the datastream path and its SHA-256, the profile id xccdf_org.ssgproject.content_profile_e8, the scap-security-guide package version, the oscap version, the waiver list, the upstream conflict list, the score and the timestamp of the evaluation.

Confirm the policy content on your VM is byte for byte the content that was evaluated:

sha256sum "$(python3 -c "import json;print(json.load(open('/usr/share/cloudimg/hardened/manifest.json'))['datastream'])")"
python3 -c "import json;print(json.load(open('/usr/share/cloudimg/hardened/manifest.json'))['sha256'])"

The two hashes must match. Then confirm the content is a valid SCAP datastream and really carries the profile:

DS=$(python3 -c "import json;print(json.load(open('/usr/share/cloudimg/hardened/manifest.json'))['datastream'])")
sudo oscap ds sds-validate "$DS" && echo "datastream is valid"
sudo oscap info --profiles "$DS" | grep e8

Step 3: The Exclusions, In Full

Two lists ship on the image, in /usr/share/cloudimg/hardened/. They are deliberately separate because they mean different things.

cat /usr/share/cloudimg/hardened/waivers.txt
cat /usr/share/cloudimg/hardened/upstream-conflicts.txt

waivers.txt: three sudo rules, a deliberate cloudimg choice

xccdf_org.ssgproject.content_rule_sudo_remove_nopasswd
xccdf_org.ssgproject.content_rule_sudo_remove_no_authenticate
xccdf_org.ssgproject.content_rule_sudo_require_authentication

All three strip NOPASSWD from every sudoers grant on the system, including /etc/sudoers.d/90-cloud-init-users, the file cloud-init writes for your administrator account on every fresh boot.

This image, like every cloudimg OS image, has no password on any account. SSH key authentication is the entire access model. Applying these three rules on such an image does not "require re-authentication", it removes sudo entirely and permanently, with no recovery path: no root password, no console password, nothing. An image that locks every customer out of root on first boot is broken, not hardened.

So sudo stays passwordless for your key authenticated account. You can verify both halves yourself:

sudo -n true && echo "sudo works with no password prompt"
sudo grep -A1 'User rules for' /etc/sudoers.d/90-cloud-init-users

The complete three rule waiver set, and the passwordless sudo grant it exists to protect

If your policy requires password protected sudo, set a password on your account (sudo passwd azureuser) and then apply those three rules yourself with oscap. The image does not stop you, it just does not ship that way by default.

upstream-conflicts.txt: re-measured on every build, empty today

This file is for a different kind of exclusion from a waiver: a rule that cannot pass on this OS because the profile contradicts itself or the distribution's packaging. cloudimg never carries an entry forward from an earlier build. Each build evaluates the profile again, and a rule is listed only if it was measured to fail for that reason on that build.

On the current image the file is empty. Earlier builds, made with scap-security-guide 0.1.81, listed xccdf_org.ssgproject.content_rule_sysctl_kernel_yama_ptrace_scope, because that content also required a package owned file (/usr/lib/sysctl.d/10-default-yama-scope.conf, which ships the value 0) to carry the setting, and editing that file would have failed rpm_verify_hashes, which the same profile selects. Version 0.1.82 passes the rule with the packaged file untouched, so the conflict no longer exists and is no longer listed.

The ptrace protection itself is in force, and package integrity is intact. You can confirm both:

sysctl -n kernel.yama.ptrace_scope
rpm -V elfutils-default-yama-scope ; echo "no output above means the packaged file is untouched"
grep -H . /etc/sysctl.d/kernel_yama_ptrace_scope.conf

The first command returns 1. The file in /etc/sysctl.d/ sorts after 10-default-yama-scope.conf, so it wins on every boot.

Step 4: Re-run the Evaluation Yourself

The whole point of this image is that you do not have to take the score on trust. The shipped evidence is here:

ls -la /var/lib/cloudimg/hardened/
cat /var/lib/cloudimg/hardened/score.txt

arf.xml is the full ARF results file and report.html is the rendered report. Copy the report to your workstation to read it in a browser:

scp azureuser@<public-ip>:/var/lib/cloudimg/hardened/report.html .

To reproduce the measurement from scratch, run the identical command the build ran. The three --skip-rule arguments are the waivers from Step 3, and nothing else is excluded:

DS=$(python3 -c "import json;print(json.load(open('/usr/share/cloudimg/hardened/manifest.json'))['datastream'])")
sudo oscap xccdf eval \
  --profile xccdf_org.ssgproject.content_profile_e8 \
  --skip-rule xccdf_org.ssgproject.content_rule_sudo_remove_nopasswd \
  --skip-rule xccdf_org.ssgproject.content_rule_sudo_remove_no_authenticate \
  --skip-rule xccdf_org.ssgproject.content_rule_sudo_require_authentication \
  --results-arf /tmp/my-arf.xml \
  --report /tmp/my-report.html \
  "$DS" ; echo "oscap exit code: $? (0 = all pass, 2 = one or more fail, both are normal)"

On the current image no selected rule fails, so oscap exits 0. An exit code of 2 means at least one rule failed. Any failing rule should be listed in upstream-conflicts.txt; if it is not, the VM has drifted from the image as shipped, which is what the comparison in Step 5 is for.

What the score does and does not mean

  • It is a measurement produced by the ComplianceAsCode community implementation of the ACSC Essential Eight, applied to this machine.
  • It is not a certification, an accreditation, or an assessment by the ACSC, the Australian Signals Directorate, or any other body. cloudimg claims no affiliation with or endorsement by any of them.
  • The Essential Eight is a set of mitigation strategies for an organisation. Several of them, such as application control maturity, user application hardening and staff training, are not properties a virtual machine image can carry at all. A hardened base image is a starting point for that work, not a substitute for it.
  • 90.0 is cloudimg's own shipping threshold for this image. There is no published pass mark for this profile.

Step 5: Ongoing Verification and Drift Detection

A systemd timer re evaluates your VM shortly after first boot and weekly after that. It is audit only and never passes --remediate, so a scheduled run can never change your running system.

systemctl status hardened-verify.timer
systemctl list-timers hardened-verify.timer

After the first run, its own results appear alongside the build time evidence:

ls -la /var/lib/cloudimg/hardened/verify-* 2>/dev/null || echo "the first scheduled verify has not run yet, it fires about 3 minutes after boot"

Compare verify-score.txt against score.txt to see whether your VM has drifted from the image as shipped.

To run the audit immediately rather than waiting:

sudo systemctl start hardened-verify.service
systemctl is-active hardened-verify.service || echo "inactive is NORMAL for a oneshot unit once the audit has finished"
sudo journalctl -u hardened-verify.service --no-pager | tail -5

Note that hardened-verify.service is a Type=oneshot unit: once the audit finishes it goes back to inactive (dead), and systemctl status then exits non-zero. That is the expected state, not a failure. The hardened-verify.timer is what stays active.

Step 6: Verify the Rest of the Hardening

Confirm SELinux is enforcing now and will be after a reboot:

getenforce
grep -E '^[[:space:]]*SELINUX=' /etc/selinux/config

Confirm the SSH posture. This image is key only, with root login refused:

sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication|clientaliveinterval) '

Confirm the only port listening off box is SSH:

ss -ltnH | awk '{print $4}' | grep -vE '^(127\.|\[::1\])' | sort -u

Azure guest agent, cloud-init, auditd and AIDE all healthy under the hardening, with :22 the only off box listener

Confirm auditd is running with its ruleset loaded, and that AIDE has its database:

systemctl is-active auditd
sudo auditctl -l | wc -l
sudo ls -la /var/lib/aide/aide.db.gz

Confirm the firewall permits SSH and nothing unexpected:

sudo firewall-cmd --state
sudo firewall-cmd --list-services

The e8 profile present in AlmaLinux's own SCAP content, SELinux enforcing, and the shipped sshd posture

AIDE: checking for file changes

The AIDE database is rebuilt immediately before the image is captured, so it describes the image you received. To check what has changed on your VM since then:

sudo aide --check | head -40

Expect differences under /var, /etc and /root after first boot: cloud-init writes your SSH key, logs rotate, and the machine generates its own host keys. That is normal. After you have finished configuring the VM, re baseline it so future checks are meaningful:

sudo aide --init && sudo mv -f /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz

Step 7: Patching

Unattended security updates are already armed:

systemctl is-enabled dnf-automatic.timer
grep -E '^(upgrade_type|apply_updates|download_updates)' /etc/dnf/automatic.conf

To patch immediately:

sudo dnf -y upgrade

If a kernel update lands, reboot and then re run the evaluation from Step 4 so your evidence matches the running kernel.

Troubleshooting

sudo asks for a password. It should not on a fresh VM. If it does, cloud-init did not write /etc/sudoers.d/90-cloud-init-users for your account, which usually means the VM was created with password authentication rather than an SSH key. Recreate it with --generate-ssh-keys or an explicit public key.

oscap exits 2. That is not a tool error. oscap exits 2 when any rule fails; on the image as shipped none does, so a failing rule that is not in upstream-conflicts.txt means the VM has changed since deployment (Step 5). Exit code 1 is a genuine tool error, exit 0 means every rule passed.

The score I measure differs from score.txt. Expected once you have changed the machine. The shipped number describes the image as captured. Configuration you apply afterwards, packages you install, and files you edit all move it. Use the shipped arf.xml as the baseline and diff against it.

aide --check reports many changes. Normal on first boot, see Step 6. Re baseline once configured.

The scheduled verify has not produced results. It first fires about three minutes after boot, and the full evaluation takes a minute or two. Check sudo journalctl -u hardened-verify.service --no-pager | tail for what it did. Do not read systemctl status exiting non-zero as a failure: the unit is Type=oneshot, so inactive (dead) is its normal resting state once the audit has completed.

Support

cloudimg provides 24/7 support for this image. Include the output of cat /usr/share/cloudimg/hardened/manifest.json and cat /var/lib/cloudimg/hardened/score.txt with any query about the hardening, since together they identify exactly which policy content your VM was built and measured against.

Licence

AlmaLinux is assembled from thousands of independently packaged components under their own terms (GPL, LGPL, MIT, BSD, Apache-2.0, MPL and more); each package's terms are readable with rpm -qi <package> and under /usr/share/licenses/. No paid subscription, licence key or support contract is required.

The oscap scanner is LGPL-2.1-or-later. The SCAP Security Guide content is BSD-3-Clause, Copyright (c) 2012-2017 Red Hat, Inc. and the ComplianceAsCode contributors. AIDE and audit are GPL-2.0-or-later. The full notice ships at /usr/share/doc/cloudimg/HARDENED-IMAGE-LICENCE-NOTICE.txt.

"AlmaLinux" is a mark of the AlmaLinux OS Foundation, used here nominatively to identify the operating system this image contains. The Essential Eight is published by the Australian Cyber Security Centre. cloudimg claims no affiliation with or endorsement by the AlmaLinux OS Foundation, the ACSC, the Australian Signals Directorate, Red Hat, ComplianceAsCode or CIS.