Operating Systems Azure

Rocky Linux 9 Essential Eight Hardened on Azure User Guide

| Product: Rocky Linux 9 Essential Eight Hardened on Azure

Overview

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

Rocky Linux is a community enterprise operating system produced by the Rocky Enterprise Software Foundation, built 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: 100.0 against the Essential Eight profile, measured after a reboot, with no remediation flag
  • 91 rules evaluated: 87 pass, 0 fail, 3 not applicable, 1 not checked
  • 3 rules waived, all of them sudo rules, named in the image and in Step 3
  • 0 upstream conflicts. Nothing on this image is excused as unfixable
  • 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:

  • Rocky Linux 9.8, binary compatible with RHEL 9, hardened with Rocky's own product native SCAP content (ssg-rl9-ds.xml, scap-security-guide-0.1.82) from the distribution's free AppStream repository, with no subscription, no CIS membership and no licence key required
  • 584 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 security 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 "Rocky Linux 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-rocky-e8-rg --location eastus

az vm create \
  --resource-group my-rocky-e8-rg \
  --name my-rocky-e8-vm \
  --image cloudimg:rocky-linux:rocky9e8: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
echo "--- upstream conflicts (this file is empty on this image) ---"
cat /usr/share/cloudimg/hardened/upstream-conflicts.txt
echo "--- end ---"

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: empty on this image

This second list exists for rules that cannot pass on a given operating system, because the profile contradicts itself or the distribution's own packaging. That is a different thing from a waiver: a waiver is a choice cloudimg made, an upstream conflict is not a choice at all.

On this image the list is empty. Every selected rule that was evaluated passed. Nothing is excused as unfixable.

That is worth stating explicitly, because cloudimg's sibling AlmaLinux 9 Essential Eight image does carry one entry here. On AlmaLinux, which ships scap-security-guide 0.1.81, the profile selects both sysctl_kernel_yama_ptrace_scope and rpm_verify_hashes, and satisfying the first requires editing a package owned file that then makes the second fail. Rocky Linux 9 ships scap-security-guide 0.1.82, on which both rules pass: the control is set from /etc/sysctl.d/, which sorts last and wins on every boot, while the packaged file is left untouched. You can confirm both halves:

sysctl -n kernel.yama.ptrace_scope
rpm -V elfutils-default-yama-scope ; echo "no output above means the packaged file is untouched"
sudo rpm -Va --nodeps --nosignature | grep -E '^..5' || echo "no package file has a digest mismatch"

The image does not simply assert an empty list. The build ships a candidate list at /usr/share/cloudimg/hardened/upstream-conflict-candidates.txt naming the only rule that is ever allowed to be documented as a conflict, and a rule is written into upstream-conflicts.txt only if it is measured to fail. Anything else that failed would stop the build instead.

cat /usr/share/cloudimg/hardened/upstream-conflict-candidates.txt

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:

echo "run this from your workstation, not the VM:"
echo "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 = every rule passed, 2 = one or more failed)"

On the image as shipped this exits 0, because no rule fails. Exit code 2 means something on your VM has changed since capture, which is expected once you have configured it. Exit code 1 is a genuine tool error.

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.
  • A score of 100.0 means every rule this profile selects and evaluates passed on this machine, with the three sudo rules skipped. It does not mean the profile covers everything the Essential Eight asks of an organisation.
  • 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 --no-pager
systemctl list-timers hardened-verify.timer --no-pager

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) '

clientaliveinterval is 180. The SCAP rule is a ceiling ("terminate after at most N seconds"), so a shorter interval satisfies it more strictly, and 180 is also what Azure's own VM image certification asks for.

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:

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

The e8 profile present in Rocky'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 2>&1 | head -40 || true

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:

echo "run this once the VM is configured the way you want it:"
echo "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

upgrade_type is security, which the Essential Eight profile's own remediation sets. That mode only works on a distribution that publishes advisory metadata, and Rocky Linux does. You can see the advisory feed your VM is reading:

sudo dnf -q updateinfo summary 2>&1 | head -10

To patch immediately, including non security updates:

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. On this image, a fresh VM exits 0 because no rule fails. Exit 2 means at least one rule now fails, which is expected once you have changed the machine. Exit 1 is a genuine tool error.

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

Rocky Linux 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.

"Rocky Linux" is a trademark of the Rocky Enterprise Software Foundation, used here nominatively to identify the operating system this image contains. The Essential Eight is published by the Australian Cyber Security Centre; material describing it is licensed CC BY 4.0, Australian Signals Directorate (c) Commonwealth of Australia. cloudimg claims no affiliation with or endorsement by the Rocky Enterprise Software Foundation, the ACSC, the Australian Signals Directorate, Red Hat, ComplianceAsCode or CIS.