AlmaLinux 9 Essential Eight Hardened on Azure User Guide
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:
- XCCDF score: 99.7 against the Essential Eight profile, measured after a reboot, with no remediation flag
- 91 rules evaluated: 86 pass, 1 documented upstream conflict, 3 not applicable, 1 not checked
- 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.

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,auditandaide - 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.jsonpinning 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
auditdenabled 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
- Navigate to the Azure Marketplace and search for "AlmaLinux 9 Essential Eight Hardened cloudimg"
- Select the image and click Create
- Configure the basics:
- Subscription and Resource group: your own
- Virtual machine name: your choice
- Region: any region you deploy into
- Size:
Standard_B2sor larger - Authentication type: SSH public key (this image has no password on any account, key authentication is the only way in)
- Username:
azureuser - Under Inbound port rules, allow SSH (22) only
- 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

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: one rule that cannot pass on this OS
xccdf_org.ssgproject.content_rule_sysctl_kernel_yama_ptrace_scope
This is not a choice cloudimg made. It is a contradiction between two rules the Essential Eight profile itself selects, and it cannot be resolved on AlmaLinux 9.
The control is active on your VM. Check it:
sysctl -n kernel.yama.ptrace_scope
That returns 1, which is what the rule asks for, and SSG's own runtime test for this rule passes. The rule still reports fail because it has a second, separate sub test that additionally requires /usr/lib/sysctl.d/*.conf to carry the value. The AlmaLinux package elfutils-default-yama-scope ships that file with kernel.yama.ptrace_scope = 0, and the profile's own remediation edits it.
cloudimg puts that file back, deliberately, because:
- the file is owned by a package and is not marked
%config, so editing it makesrpm -Vareport a digest mismatch; xccdf_org.ssgproject.content_rule_rpm_verify_hashesis also selected by this profile, so the edit trades one failing rule for another;- the package cannot simply be removed, because
dnfitself depends on it transitively.
Exactly one of the 94 selected rules fails either way. cloudimg chose to keep package integrity verification intact, since the ptrace protection is already in force at runtime. You can confirm both facts:
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 file in /etc/sysctl.d/ sorts after 10-default-yama-scope.conf, so it wins on every boot. That is why the runtime value is 1.
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)"
oscap exits 2 whenever any rule fails, and on this image one rule does: the documented upstream conflict from Step 3. That is expected. What matters is that the only failing rule is that one.
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

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

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 an error. oscap exits 2 when any rule fails, and one rule does on this image by design (Step 3). 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.