Deployment guide

Rocky 9 hardened E8

Overview

Rocky Linux is a community enterprise operating system produced by the Rocky Enterprise Software Foundation and built to be binary compatible with Red Hat Enterprise Linux 9. This guide covers the deployment and verification of Rocky Linux 9 Essential Eight Hardened, SecureAnyCloud's pre-configured virtual machine image on Microsoft Azure Marketplace.

At build time, this edition applies the SCAP Security Guide profile Australian Cyber Security Centre (ACSC) Essential Eight (xccdf_org.ssgproject.content_profile_e8) with oscap xccdf eval --remediate. It uses the same open source engine and the same freely available policy content that 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 that 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.
  • No upstream "pass score" exists for this profile. 90.0 is SecureAnyCloud's own threshold for shipping this image, and the build fails if the score falls below it.

What you get:

  • 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. No subscription, CIS membership or licence key is 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/sac/data/hardened/
  • A machine-readable manifest at /usr/share/sac/hardened/manifest.json that pins 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. It is audit only and never remediates.
  • Every available update applied at build time and verified with a zero-pending check, with 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 that it describes the image you actually receive
  • Passwordless sudo for your SSH-key-authenticated account. This is a deliberate, documented exception described in Step 3.
  • 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 SecureAnyCloud support

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

1

Deploy the Virtual Machine

Option A: Azure Portal

  • Open the Azure Marketplace and search for "Rocky Linux 9 Essential Eight Hardened SecureAnyCloud".
  • 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_B2s or larger
    • Authentication type: SSH public key. This image has no password on any account, so key authentication is the only way in.
    • Username: azureuser
  • Under Inbound port rules, allow SSH (22) only.
  • Review and create the VM.

Option B: Azure CLI

az group create --name my-rocky-e8-rg --location eastus
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:rocky-9-hardened-e8:default:latest
az vm create \
  --resource-group my-rocky-e8-rg \
  --name my-rocky-e8-vm \
  --image sac1647283583153:rocky-9-hardened-e8:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

When the VM is running, connect to it:

ssh azureuser@<public-ip>
2

Confirm the Image Is What It Claims

Every command in this step runs as azureuser on your own VM, and none of them needs the internet.

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

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

The manifest shows 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 that 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/sac/hardened/manifest.json'))['datastream'])")" python3 -c "import json;print(json.load(open('/usr/share/sac/hardened/manifest.json'))['sha256'])"

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

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

The Exclusions, In Full

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

cat /usr/share/sac/hardened/waivers.txt echo "--- upstream conflicts (this file is empty on this image) ---"
cat /usr/share/sac/hardened/upstream-conflicts.txt echo "--- end ---"

waivers.txt: three sudo rules, a deliberate SecureAnyCloud 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 rules strip NOPASSWD from every sudoers grant on the system. That includes /etc/sudoers.d/90-cloud-init-users, the file that cloud-init writes for your administrator account on every fresh boot.

This image, like every SecureAnyCloud OS image, has no password on any account, and SSH key authentication is the entire access model. On such an image, these three rules do not "require re-authentication". They remove 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.

For that reason, 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 prevent this, but it does not ship that way by default.

upstream-conflicts.txt: empty on this image

This second list holds rules that cannot pass on a given operating system, because the profile contradicts itself or the distribution's own packaging. It differs from a waiver. A waiver is a choice that SecureAnyCloud made, whereas an upstream conflict is not a choice at all.

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

This is worth stating explicitly, because SecureAnyCloud'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. Satisfying the first rule requires editing a package-owned file, which then makes the second rule 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/sac/hardened/upstream-conflict-candidates.txt, which names the only rule that may ever be documented as a conflict. A rule is written into upstream-conflicts.txt only if it is measured to fail. Any other failure stops the build.

cat /usr/share/sac/hardened/upstream-conflict-candidates.txt
4

Re-run the Evaluation Yourself

You do not have to take the score on trust, because the image ships its evidence:

ls -la /var/lib/sac/data/hardened/ cat /var/lib/sac/data/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/sac/data/hardened/report.html ."

To reproduce the measurement from scratch, run the identical command that 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/sac/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, the command exits with 0, because no rule fails. Exit code 2 means that 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. SecureAnyCloud 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 that a virtual machine image can carry. A hardened base image is a starting point for that work, not a substitute for it.
  • A score of 100.0 means that every rule this profile selects and evaluates passed on this machine, with the three sudo rules skipped. It does not mean that the profile covers everything the Essential Eight asks of an organisation.
  • 90.0 is SecureAnyCloud's own shipping threshold for this image. There is no published pass mark for this profile.
5

Ongoing Verification and Drift Detection

A systemd timer re-evaluates your VM shortly after first boot and weekly after that. The evaluation 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 results appear alongside the build-time evidence:

ls -la /var/lib/sac/data/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 instead of waiting for the timer, run:

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

hardened-verify.service is a Type=oneshot unit. When the audit finishes, the unit returns to inactive (dead), and systemctl status then exits non-zero. This is the expected state, not a failure. The hardened-verify.timer is the unit that stays active.

6

Verify the Rest of the Hardening

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

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

Confirm the SSH configuration. The image accepts keys only and refuses root login:

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. 180 is also the value that Azure's own VM image certification asks for.

Confirm that SSH is the only port listening off the VM:

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

Confirm that 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 that the firewall permits SSH:

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 that you received. To check what has changed on your VM since then, run:

sudo aide --check 2>&1 | head -40 || true

Expect differences under /var, /etc and /root after first boot, because cloud-init writes your SSH key, logs rotate, and the machine generates its own host keys. This is normal. After you finish configuring the VM, re-baseline the database so that 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"
7

Patching

Unattended security updates are already armed. Confirm the configuration:

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 works only on a distribution that publishes advisory metadata, and Rocky Linux does. To see the advisory feed that your VM reads, run:

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

To patch immediately, including non-security updates, run:

sudo dnf -y upgrade

If a kernel update is installed, reboot and then re-run the evaluation from Step 4 so that 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. This usually means that the VM was created with password authentication instead of an SSH key. Recreate the VM with --generate-ssh-keys or an explicit public key.

oscap exits 2. On a fresh VM from this image, the command exits 0 because no rule fails. Exit 2 means that 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. This is expected once you have changed the machine. The shipped number describes the image as captured, and any configuration you apply, packages you install or files you edit afterwards can change it. Use the shipped arf.xml as the baseline and diff against it.

aide --check reports many changes. This is normal on first boot, as described in Step 6. Re-baseline the database once you have configured the VM.

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

Support

SecureAnyCloud provides 24/7 support for this image. With any question about the hardening, include the output of cat /usr/share/sac/hardened/manifest.json and cat /var/lib/sac/data/hardened/score.txt. 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). You can read each package's terms 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/sac/HARDENED-IMAGE-LICENCE-NOTICE.txt.

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

Deploy on Azure

Launch Rocky Linux 9 Essential Eight Hardened on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure