Operating Systems Azure

AlmaLinux 9 STIG-Hardened on Azure User Guide

| Product: AlmaLinux 9 STIG-Hardened on Azure

Overview

This guide covers the deployment and verification of AlmaLinux 9 STIG-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 takes that base further: the DISA STIG (Security Technical Implementation Guide) profile from the SCAP Security Guide project is applied at build time with oscap xccdf eval --remediate, the same open source engine you can run yourself, then the machine is rebooted and re-scanned cleanly, with no remediation flag, to produce a real measured compliance score. That score, the full HTML report and the machine readable results all ship on the image.

This is a compliance scanner AND a pre-hardened image, not a compliance certificate. A passing score is a measurement made with the ComplianceAsCode community implementation of the DISA STIG benchmark, not a certification issued by DISA or any accreditation body. Read Step 4 before you rely on the number for an audit.

What's included:

  • AlmaLinux 9, binary compatible with RHEL 9, with the DISA STIG profile applied using AlmaLinux's own genuinely product-native SCAP Security Guide content (ssg-almalinux9-ds.xml) -- not a substitute or a RHEL 9 stand-in
  • 571 installed packages on a fresh build, including the SCAP Security Guide content, aide, audit and the crypto-policy tooling the STIG profile pulls in
  • The build time compliance report, results and measured score shipped at /var/log/cloudimg/stig/
  • A second, independent re-verification that runs on your own VM shortly after first boot, and weekly after that
  • 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
  • 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 for Azure integration, proven to still work under the hardening
  • 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 STIG-Hardened 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 your preferred Azure region
  8. Size: Standard_B2s recommended
  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

az vm create \
  --resource-group myResourceGroup \
  --name my-almalinux-9-stig-vm \
  --image cloudimg:almalinux-9-stig:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

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-almalinux-9-stig-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. STIG's own sshd hardening tightens the ciphers, MACs and session timeouts below, but the SSH connection model is exactly the same as every other cloudimg image: key only, injected by Azure at first boot.

Step 3: Sudo stays passwordless -- read this before anything else

The raw DISA STIG profile includes a rule that removes the NOPASSWD tag from every sudoers entry, so that sudo always demands re-authentication. This build deliberately excludes that rule, and its two closest siblings, from automated remediation. Here is why, stated plainly rather than buried in a changelog:

This image, like every cloudimg image, has no password on any account, ever. Access is entirely by SSH key. If the NOPASSWD-removal rule had been applied, sudo would have demanded a password that does not exist and never will, on the very account Azure creates for you -- permanently locking you out of root with no recovery path at all, not even a console password. That is not "requiring re-authentication", it is removing administrative access outright. A STIG image that locks every customer out of root on first boot is not hardened, it is broken.

Confirm it for yourself:

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

Expected output:

sudo works, no password prompt
# User rules for azureuser
azureuser ALL=(ALL) NOPASSWD:ALL

The terminal shows sudo -n true succeeding with no password prompt, and the sudoers.d file granting azureuser NOPASSWD ALL, confirming administrative access works over key based SSH exactly as on every other cloudimg image

The full list of excluded rules, and the reasoning for each, ships on the image:

cat /usr/share/cloudimg/stig/skip-rules.txt

Expected output:

xccdf_org.ssgproject.content_rule_aide_use_fips_hashes
xccdf_org.ssgproject.content_rule_disallow_bypass_password_sudo
xccdf_org.ssgproject.content_rule_fips_crypto_subpolicy
xccdf_org.ssgproject.content_rule_fips_custom_stig_sub_policy
xccdf_org.ssgproject.content_rule_grub2_password
xccdf_org.ssgproject.content_rule_sudo_remove_no_authenticate
xccdf_org.ssgproject.content_rule_sudo_remove_nopasswd
xccdf_org.ssgproject.content_rule_sysctl_crypto_fips_enabled

Three exclusion groups, eight rules total, every one of them explained here:

Rule group Why it is excluded
sudo_remove_nopasswd, sudo_remove_no_authenticate, disallow_bypass_password_sudo Would strip passwordless sudo from your SSH key authenticated account, as above. There is no password anywhere on this image to re-authenticate with.
grub2_password The upstream remediation script for this rule reads a boot loader password interactively from a terminal. Run non-interactively during an automated build, it would hang the build indefinitely rather than fail cleanly. It has no effect on normal boot either way -- GRUB password protection guards editing boot entries at the console, not booting the default one. Set it yourself with grub2-setpassword if you want that protection; see Step 7.
fips_crypto_subpolicy, fips_custom_stig_sub_policy, sysctl_crypto_fips_enabled, aide_use_fips_hashes Full FIPS 140 mode. Enabling it correctly needs fips-mode-setup --enable, a dracut -f initramfs rebuild and a further reboot to prove it actually boots that way -- a cycle this build does not perform. FIPS is not enabled on this image. Stated plainly here rather than half-applied and claimed.

Step 4: Read the compliance evidence

The build time compliance report ships on the image, generated after a reboot, so it reflects a real running boot, not a snapshot taken mid-build:

cat /var/log/cloudimg/stig/build-remediation-score.txt
ls -la /var/log/cloudimg/stig/
oscap info --profiles /usr/share/xml/scap/ssg/content/ssg-almalinux9-ds.xml | grep stig

Expected output (the exact score varies build to build as upstream STIG content and the base image evolve; this is the real measurement from this build, not a fixed target):

94.4
build-remediation-report.html
build-remediation-results.xml
build-remediation-score.txt
xccdf_org.ssgproject.content_profile_stig:DISA STIG for AlmaLinux OS 9

The terminal shows the measured build time STIG score of 94.4 percent, the three evidence files present under var log cloudimg stig, and the DISA STIG profile confirmed present in the shipped AlmaLinux-native SCAP Security Guide datastream

Copy the HTML report to your workstation to read it properly:

echo "from your workstation: scp azureuser@<vm-ip>:/var/log/cloudimg/stig/build-remediation-report.html ."

This profile is genuinely AlmaLinux-native, not a RHEL 9 substitute. AlmaLinux's own scap-security-guide package ships ssg-almalinux9-ds.xml with a real DISA STIG for AlmaLinux OS 9 profile built in -- this image resolves and probes that datastream directly rather than assuming any particular filename or falling back to a different distribution's content.

What the remaining gap is made of, honestly, not hidden. This build measures in the mid-90s percent, out of 428 evaluated rules. Every rule that does not pass falls into one of these buckets:

Bucket Example rules Why it does not pass
Single-disk architecture partition_for_tmp, partition_for_var, partition_for_var_log, partition_for_var_log_audit, partition_for_var_tmp, partition_for_home These require /tmp, /var, /var/log, /var/log/audit, /var/tmp and /home on their own separate partitions. This is a single OS disk cloud image; there is nowhere for a second partition to live. Genuinely not applicable, not a defect.
The three excluded rule groups from Step 3, plus their crypto-policy cascades grub2_admin_username, selinux_context_elevation_for_sudo, configure_crypto_policy, harden_sshd_ciphers_openssh_conf_crypto_policy, harden_sshd_ciphers_opensshserver_conf_crypto_policy, harden_sshd_macs_openssh_conf_crypto_policy, harden_sshd_macs_opensshserver_conf_crypto_policy Deliberately excluded (grub2/sudo), or tied to the deferred FIPS crypto sub-policy -- the system-wide crypto policy only reaches its strictest FIPS-aligned state once FIPS mode itself is enabled, which this build does not do (see Step 3).
The zero-password, key-only design require_singleuser_auth, accounts_password_all_shadowed_sha512 Rescue/single-user mode authentication and password-hash checks both assume an account has a password. No account on this image ever has one -- access is SSH key only, by design.
Environment specific, no automated fix, or a package this image does not carry network_configure_name_resolution, chronyd_configure_local_socket, accounts_authorized_local_users, sysctl_user_max_user_namespaces_no_remediation, package_cron_installed, postfix_prevent_unrestricted_relay, sssd_enable_certmap, sssd_enable_smartcards, configure_usbguard_auditbackend These need a site-specific answer (your own DNS servers, your own authorized-user list), SSG itself ships no automated fix and expects manual review, or the rule hardens a package (cron, postfix, usbguard, domain-joined sssd) that is not installed on this lean base image at all.

None of these are hidden or remediated-away. Read the HTML report for the full rule-by-rule detail.

Step 5: Confirm the release and patch level

cat /etc/almalinux-release
grep -E '^(NAME|VERSION|ID|PLATFORM_ID)=' /etc/os-release
uname -rm
sudo dnf check-update; echo "check-update exit code: $?"
sudo dnf needs-restarting -r; echo "needs-restarting exit code: $?"

Expected output:

AlmaLinux release 9.8 (Olive Jaguar)
NAME="AlmaLinux"
VERSION="9.8 (Olive Jaguar)"
ID="almalinux"
PLATFORM_ID="platform:el9"
5.14.0-687.36.1.el9_8.x86_64 x86_64
check-update exit code: 0
needs-restarting exit code: 0

dnf check-update returns exit code 100 when updates are pending and 0 when none are. needs-restarting -r returns 0 when nothing requires a restart.

Step 6: Unattended updates are already armed

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

Expected output:

enabled
active
upgrade_type = default
download_updates = yes
apply_updates = yes

upgrade_type is deliberately default rather than security. The security-only mode resolves against errata metadata; where that metadata is not published a security-only policy silently resolves to nothing and applies no updates at all. default applies every available update, which cannot quietly do nothing.

Step 7: Verify the hardening yourself

Confirm SELinux, root, SSH ciphers and the boot loader kernel arguments STIG requires:

getenforce
grep -E '^[[:space:]]*SELINUX=' /etc/selinux/config
sudo passwd -S root
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|maxauthtries|clientaliveinterval)'
grep -o 'init_on_free=1\|pti=on\|vsyscall=none\|page_poison=1\|audit=1\|audit_backlog_limit=[0-9]*' /proc/cmdline

Expected output:

Enforcing
SELINUX=enforcing
root LK 2009-12-22 -1 -1 -1 -1 (Alternate authentication scheme in use.)
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
maxauthtries 6
clientaliveinterval 600
init_on_free=1
pti=on
vsyscall=none
page_poison=1
audit=1
audit_backlog_limit=8192

The terminal shows SELinux Enforcing both at runtime and persisted, root locked, the STIG tightened sshd policy with MaxAuthTries 6 and a 600 second idle timeout, and the STIG kernel hardening arguments -- init_on_free, page table isolation, no vsyscalls, page allocator poisoning and audit from early boot -- all active on the running kernel

Every one of these kernel arguments is also written into /etc/default/grub, not only applied to the current boot entry, so they carry forward the next time a kernel update installs a new boot entry:

grep CMDLINE_LINUX /etc/default/grub

If you want GRUB boot-menu-edit password protection (Step 3 explains why this build does not set one automatically):

sudo grub2-setpassword
sudo grub2-mkconfig -o /boot/grub2/grub.cfg

That block is deliberately not something you run blindly -- it sets an interactive password, review it first.

Step 8: Verify Azure integration survived the hardening

The single most important property of this image: STIG hardening must not break the very cloud-init and Azure agent paths that provision your VM. Confirm it:

systemctl is-active waagent
systemctl is-enabled waagent
cloud-init --version
cloud-init status --long
df -h /
free -h

free -h reports 0B of swap, deliberately. Azure manages swap on the ephemeral resource disk through waagent rather than on the OS disk, and a swap file baked into a Marketplace image fails certification.

Step 9: Networking and the STIG-configured firewall

Unlike the plain AlmaLinux 9 image, STIG remediation installs and enables firewalld. Confirm what it allows before you rely on it:

firewall-cmd --state
firewall-cmd --get-default-zone
firewall-cmd --list-services
ss -tlnu

Expected output:

running
public
cockpit dhcpv6-client ssh
Netid State  Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp   LISTEN 0      128          0.0.0.0:22        0.0.0.0:*
tcp   LISTEN 0      128             [::]:22           [::]:*

ssh is explicitly permitted in the default zone -- this is asserted on every build, not assumed, precisely because a default-deny firewall that forgot to keep SSH open would lock out every customer exactly like the sudo issue in Step 3 would have. cockpit and dhcpv6-client are the stock public zone's other pre-authorised services; neither is installed or listening, so they grant nothing extra.

The terminal shows the Azure Linux Agent active and enabled, the STIG-installed firewalld running with the ssh service explicitly permitted in the public zone, and the only TCP listener on the machine is SSH on port 22 for both IPv4 and IPv6

The Azure Network Security Group is still the first boundary your traffic crosses. With firewalld also active, a port must be open in both the NSG and firewalld before traffic reaches your service:

sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --reload

rpcbind is disabled on this image, exactly as on the plain AlmaLinux 9 build. NFSv4, the default in AlmaLinux 9, does not need it; re-enable it yourself with sudo systemctl enable --now rpcbind.socket if you specifically need NFSv3. STIG's own package_nfs-utils_removed rule additionally removes the nfs-utils package outright to reduce attack surface -- install it again with sudo dnf install -y nfs-utils if you need any NFS client capability at all.

Step 10: Re-scan on demand, and where the scheduled re-verification lives

The scanner and policy content ship on the image, so you are never dependent on cloudimg's own report:

oscap ds sds-validate /usr/share/xml/scap/ssg/content/ssg-almalinux9-ds.xml && echo "datastream valid"
systemctl list-timers stig-verify.timer --no-legend
test -f /var/lib/cloudimg/stig-firstboot.done && echo "firstboot completed: $(stat -c %y /var/lib/cloudimg/stig-firstboot.done)"

stig-verify.timer runs an audit-only re-scan of your own instance a few minutes after every boot, and weekly after that, writing a fresh report to /var/log/cloudimg/stig/verify-report.html, verify-results.xml and verify-score.txt. It never remediates -- a scheduled job that could silently change your running configuration is not something this image will ever do to you.

To run a full scan by hand at any time:

sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_stig \
  --results /tmp/my-scan-results.xml --report /tmp/my-scan-report.html \
  /usr/share/xml/scap/ssg/content/ssg-almalinux9-ds.xml
echo "exit code: $? (0 = every rule passed, 2 = some rules failed -- normal, 1 = a genuine scanner error)"

Step 11: Remediation is reviewable, never automatic, and never something the walk-through runs for you

The upstream bash and Ansible remediation content for every profile ships alongside the scanner, so you can review and apply further hardening deliberately:

ls /usr/share/xml/scap/ssg/content/*.xml | head -3

Generating and applying additional fixes is destructive and must be reviewed before use -- it can disable services, tighten sshd further, or change mount options. This is intentionally shown as a reference block, not something to paste and run:

sudo oscap xccdf generate fix --profile xccdf_org.ssgproject.content_profile_stig \
  --fix-type bash /usr/share/xml/scap/ssg/content/ssg-almalinux9-ds.xml > /tmp/my-fixes.sh
less /tmp/my-fixes.sh
# review every line before running anything

Common Use Cases

  • A pre-hardened RHEL compatible base for regulated or government-adjacent workloads that need DISA STIG controls applied and evidenced
  • A measured starting point for a compliance programme, with a real report instead of a checklist claim
  • A lean, patchable foundation for production servers where both security posture and provable evidence of it matter

Troubleshooting

Cannot connect via SSH

  1. Verify the VM is in Running state in the Azure Portal
  2. Check that port 22 is allowed in the Network Security Group
  3. Ensure you are using the correct username: azureuser
  4. Verify your SSH key matches the one you supplied at create time. Password login is disabled by design.

To see which key your client is actually offering, run ssh -v azureuser@<vm-ip> from your workstation and look for the Offering public key lines.

sudo asks for a password

It should not. If it does, something on your own VM changed the sudoers configuration after boot -- this image ships with azureuser ALL=(ALL) NOPASSWD:ALL and asserts it on every build (Step 3). Check sudo -l and /etc/sudoers.d/90-cloud-init-users.

Azure agent not running

systemctl is-active waagent
sudo systemctl enable --now waagent

A service is blocked and the logs mention SELinux

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

Prefer semanage fcontext + restorecon, or setsebool -P for the specific boolean, over disabling SELinux.

Checking for failed services

systemctl is-system-running
systemctl list-units --state=failed --no-legend

If you see sssd.service was ever enabled: it is disabled on this image. STIG's certificate-mapping rule writes a partial SSSD config with no domain configured, which would otherwise crash-loop sssd.service on every boot. SSSD is not needed for SSH key access; re-enable it yourself once you have a real domain/LDAP config in /etc/sssd/sssd.conf.

I need NFS

sudo dnf install -y nfs-utils
sudo systemctl enable --now rpcbind.socket   # only if you specifically need NFSv3

Important Notes

AlmaLinux is free and open source, and it is a collection of licences rather than a single one -- 571 installed packages on this image declare a wide spread of licence strings, most commonly GPLv2+, LGPLv2+, MIT, GPL+ or Artistic, BSD and GPLv3+:

rpm -qa --qf '%{LICENSE}\n' | sort | uniq -c | sort -rn | awk 'NR<=8'

The SCAP Security Guide content that ships on this image (the DISA STIG profile and every other bundled profile) is licensed BSD-3-Clause, Copyright (c) 2012-2017 Red Hat, Inc. and the ComplianceAsCode contributors -- fully permissive. The oscap scanner itself is LGPL-2.1-or-later. Full text: /usr/share/doc/cloudimg/SSG-BSD-3-CLAUSE-LICENSE.txt and /usr/share/doc/openscap-scanner/copyright.

No subscription, licence key or support contract is required for any of it. "AlmaLinux" and "DISA STIG" are used here to identify the distribution and the benchmark this image implements; this listing is not affiliated with, endorsed by, or certified by the AlmaLinux OS Foundation, Red Hat, the ComplianceAsCode project, or the US Defense Information Systems Agency. A passing measured score is not a compliance certification.

Support

For assistance with this image, contact cloudimg support: