Ubuntu 22.04 LTS STIG-Hardened on Azure User Guide
Overview
This guide covers the deployment and verification of Ubuntu 22.04 LTS STIG-Hardened on Microsoft Azure using cloudimg's pre configured virtual machine image from the Azure Marketplace.
Ubuntu 22.04 LTS is Canonical's widely deployed long term support release. This edition takes that base further: the DISA STIG (Security Technical Implementation Guide) profile for Ubuntu 22.04, from the open source SCAP Security Guide (ComplianceAsCode) 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 image deliberately does not use Canonical's own USG tool or an Ubuntu Pro subscription. Canonical's usg fix --profile disa_stig is the commercial path to the same benchmark, but it requires an active Ubuntu Pro subscription and token (sudo pro enable usg). This build uses the free, redistributable SCAP Security Guide content directly instead, so the compliance work ships without any extra subscription, token or support contract attached to it.
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 for Canonical Ubuntu 22.04 LTS, 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:
- Ubuntu 22.04 LTS with the DISA STIG profile (V2R7) applied, from the open source SCAP Security Guide
- 688 installed packages on a fresh build, including the SCAP Security Guide content and the auditd/apparmor tooling the STIG profile relies on
- No Ubuntu Pro subscription, USG or any commercial entitlement anywhere in the build or the image
- 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 upgrades already armed
- AppArmor enabled and enforcing, both at runtime and armed 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 (walinuxagent) 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
- Navigate to the Azure Marketplace and search for "Ubuntu 22.04 LTS STIG-Hardened cloudimg"
- Select the image and click Create
- Configure the basics:
- Subscription: Select your Azure subscription
- Resource Group: Create new or select existing
- Virtual Machine Name: Enter a name for your VM
- Region: Select your preferred Azure region
- Size:
Standard_B2srecommended - Under Administrator Account, select SSH public key and enter your key
- Under Inbound Port Rules, allow SSH (port 22)
- Click Review + Create, then Create
Option B: Azure CLI
az vm create \
--resource-group myResourceGroup \
--name my-ubuntu-22-04-stig-vm \
--image cloudimg:ubuntu-22-04-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-ubuntu-22-04-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 session timeouts and auth-try limits 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 rules (sudo_remove_nopasswd, sudo_remove_no_authenticate) that remove the NOPASSWD tag from every sudoers entry, so that sudo always demands re-authentication. This build deliberately excludes those rules 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 rules 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' /etc/sudoers.d/90-cloud-init-users
Expected output:
sudo works, no password prompt
# User rules for azureuser
azureuser ALL=(ALL) NOPASSWD:ALL

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_grub2_password
xccdf_org.ssgproject.content_rule_grub2_uefi_password
xccdf_org.ssgproject.content_rule_is_fips_mode_enabled
xccdf_org.ssgproject.content_rule_sudo_remove_no_authenticate
xccdf_org.ssgproject.content_rule_sudo_remove_nopasswd
Three exclusion groups, five rules total, every one of them explained here:
| Rule group | Why it is excluded |
|---|---|
sudo_remove_nopasswd, sudo_remove_no_authenticate |
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, grub2_uefi_password |
The upstream remediation script for these rules 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. |
is_fips_mode_enabled |
Full FIPS 140 kernel mode. Enabling it correctly needs a reboot-and-prove cycle this build does not perform, and on Ubuntu the certified FIPS kernel/crypto packages are themselves an Ubuntu Pro entitlement -- another reason this image does not enable it. FIPS is not enabled on this image. Stated plainly here rather than half-applied and claimed. |
Because this build does not use Canonical's USG tool, none of these exclusions required attaching an Ubuntu Pro subscription -- they are applied with the free, open source SCAP Security Guide tailoring mechanism described in Step 10.
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-ubuntu2204-ds.xml | grep -i 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):
96.7
build-remediation-report.html
build-remediation-results.xml
build-remediation-score.txt
xccdf_org.ssgproject.content_profile_stig:Canonical Ubuntu 22.04 LTS Security Technical Implementation Guide (STIG) V2R7

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 ."
What the remaining gap is made of, honestly, not hidden. This build measures 96.7%, out of 213 rules that were actually checked (pass or fail; the STIG profile selects 230 rules in total, the rest are informational, manual-review-only, or not applicable and are not counted against the score either way). Every rule that does not pass falls into one of these buckets:
| Bucket | Rules | Why it does not pass |
|---|---|---|
| Single-disk architecture | partition_for_dev_shm |
Wants /dev/shm on its own mount with dedicated options. This is a single OS disk cloud image; there is nowhere for a second mount to live. Genuinely not applicable, not a defect. |
| The excluded rule groups from Step 3 | (cascading checks only) | No separately-failing rule beyond the five explicitly skipped -- they show notselected, not fail, and are not counted in the 213. |
| Firewall not configured at the OS level | set_ufw_default_rule, ufw_rules_for_open_ports |
ufw ships installed but inactive by design -- the Azure Network Security Group is the network boundary for this image (see Step 9). Enable and configure ufw yourself if you want OS-level filtering as well. |
| Environment specific, no domain configured | sssd_certification_path_trust_anchor, sssd_offline_cred_expiration |
These need a site-specific answer -- your own certificate trust anchors and credential-caching policy for a real LDAP/AD domain. This image has no domain join. |
| Content limitation, verified false-negative | package_prelink_removed, chronyd_run_as_chrony_user |
Checked directly on this build: prelink is genuinely not installed, and chronyd genuinely runs as the _chrony system user (Debian/Ubuntu's naming convention, _chrony rather than chrony). Both controls are actually satisfied; the automated OVAL check in this content release does not correctly detect the Debian-family naming/state on Ubuntu 22.04. Not hidden, not skipped from the count -- shown here so you are not left guessing. |
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/os-release
uname -rm
sudo apt-get update -qq
sudo apt-get -s -o APT::Get::Always-Include-Phased-Updates=true dist-upgrade | grep -c '^Inst ' || echo 0
test -f /var/run/reboot-required && echo "reboot required" || echo "no reboot required"
Expected output:
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
ID=ubuntu
ID_LIKE=debian
UBUNTU_CODENAME=jammy
6.8.0-1064-azure x86_64
0
no reboot required
APT::Get::Always-Include-Phased-Updates=true matters: Ubuntu's phased update rollout can otherwise hold back a subset of updates -- including security-relevant ones -- on any individual machine without ever reporting a failure. This image is built and verified with phased updates always included, so 0 genuinely means fully patched, not "patched except whatever this specific VM was phased out of".
Step 6: Unattended upgrades are already armed
systemctl is-enabled unattended-upgrades.service
systemctl is-enabled apt-daily.timer
systemctl is-enabled apt-daily-upgrade.timer
grep -E '^(APT::Periodic)' /etc/apt/apt.conf.d/20auto-upgrades
Expected output:
enabled
enabled
enabled
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
unattended-upgrades applies security updates automatically on your VM going forward, exactly as a stock Ubuntu image does -- STIG hardening does not disable this, it reinforces it.
Step 7: Verify the hardening yourself
Confirm AppArmor, root, SSH policy and the STIG audit kernel argument:
aa-status --enabled && echo "AppArmor enabled"
systemctl is-enabled apparmor.service
sudo passwd -S root
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|maxauthtries|clientaliveinterval)'
grep -o 'audit=1' /proc/cmdline
Expected output:
AppArmor enabled
enabled
root L 02/09/2026 0 99999 7 -1
permitrootlogin without-password
passwordauthentication no
kbdinteractiveauthentication no
maxauthtries 6
clientaliveinterval 600
audit=1

audit=1 is also written into /etc/default/grub's GRUB_CMDLINE_LINUX_DEFAULT, not only applied to the current boot entry -- unlike RHEL-family grub tooling, Ubuntu's update-grub reads this file directly on every kernel update, so the setting carries forward automatically with no separate persistence step required.
grep CMDLINE_LINUX_DEFAULT /etc/default/grub
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 walinuxagent 2>/dev/null || systemctl is-active waagent
systemctl is-enabled walinuxagent 2>/dev/null || 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 -- the Azure NSG is the firewall boundary
Unlike some STIG builds, this image does not enable ufw by default -- it ships installed but inactive, because the Azure Network Security Group is already the network boundary for this VM, and a second default-deny layer that silently forgot to permit SSH would lock out every customer exactly like the sudo issue in Step 3 would have. Confirm what is actually listening:
ss -tlnH | awk '{print $1, $4}'
sudo postconf -h inet_interfaces
Expected output:
tcp 127.0.0.1:25
tcp 0.0.0.0:22
tcp 127.0.0.53%lo:53
tcp [::1]:25
tcp [::]:22
loopback-only
The only externally-bound listener is SSH on port 22 (IPv4 and IPv6). Postfix (local mail for cron and system tooling) and systemd-resolved's DNS stub are both loopback-only -- postfix ships on the base Ubuntu image bound to all interfaces by default; this build restricts it to loopback-only so it cannot accept mail from the internet, while local mail delivery for system tools keeps working.
If you want OS-level filtering in addition to the NSG:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw enable
That block is deliberately not something you run blindly -- enabling ufw without first allowing SSH can lock you out exactly as an NSG misconfiguration would; review it before running it, and always test SSH access from a second existing session before disconnecting your only one.
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-ubuntu2204-ds.xml && echo "datastream valid"
systemctl list-timers stig-verify.timer --no-legend
test -f /var/lib/cloudimg/stig-firstboot.done && echo "firstboot completed"
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, using the same tailored profile the build used (jammy's oscap predates --skip-rule, so this image ships an XCCDF tailoring file that excludes the five rules from Step 3 the standards-based way):
sudo oscap xccdf eval \
--tailoring-file /usr/share/cloudimg/stig/tailoring.xml \
--profile xccdf_cloudimg_profile_stig_automation_safe \
--results /tmp/my-scan-results.xml --report /tmp/my-scan-report.html \
/usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml
echo "exit code: $? (0 = every rule passed, 2 = some rules failed -- normal, 1 = a genuine scanner error)"
To scan against the raw, untailored DISA STIG profile instead (this will also evaluate the five rules from Step 3 -- read what they do before running their generated fixes):
sudo oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_stig \
--results /tmp/my-raw-results.xml --report /tmp/my-raw-report.html \
/usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml
echo "exit code: $? (0 = every rule passed, 2 = some rules failed -- normal, especially against the raw profile, 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 this profile ships alongside the scanner, so you can review and apply further hardening deliberately:
ls /usr/share/cloudimg/stig/remediation/bash | head -5
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-ubuntu2204-ds.xml > /tmp/my-fixes.sh
less /tmp/my-fixes.sh
# review every line before running anything
Common Use Cases
- A pre-hardened, familiar Ubuntu LTS 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, and no Ubuntu Pro subscription to buy
- A lean, patchable foundation for production servers where both security posture and provable evidence of it matter
Troubleshooting
Cannot connect via SSH
- Verify the VM is in Running state in the Azure Portal
- Check that port 22 is allowed in the Network Security Group
- Ensure you are using the correct username:
azureuser - 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 walinuxagent 2>/dev/null || systemctl is-active waagent
sudo systemctl enable --now walinuxagent 2>/dev/null || sudo systemctl enable --now waagent
A service is blocked and you suspect AppArmor
sudo aa-status | grep -i complain
sudo journalctl -k | grep -i apparmor | tail -20
Prefer adjusting the specific profile in /etc/apparmor.d/ and running sudo apparmor_parser -r over disabling AppArmor.
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 if no domain is configured. SSSD is not needed for SSH key access; enable it yourself once you have a real domain/LDAP config in /etc/sssd/sssd.conf.
I want OS-level firewalling in addition to the NSG
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status
Always allow SSH before enabling ufw, and test from a second session before disconnecting your only one.
Important Notes
Ubuntu is free and open source, and it is a collection of licences rather than a single one -- 688 installed packages on this image declare dozens of distinct licence terms, most commonly GPL-2, MIT, GPL-3, LGPL-2.1, ISC, Artistic and Apache-2.0:
for p in $(dpkg-query -W -f='${Package}\n'); do
f="/usr/share/doc/$p/copyright"
[ -s "$f" ] || continue
grep -m1 -oE '(GPL-[0-9.]+|LGPL-[0-9.]+|MIT|BSD-[0-9]-Clause|Apache-2\.0|MPL-[0-9.]+|Artistic|ISC)' "$f"
done | sort | uniq -c | sort -rn | head -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 (shipped inside Ubuntu's libopenscap8 package) is LGPL-2.1-or-later. Full text: /usr/share/doc/cloudimg/SSG-BSD-3-CLAUSE-LICENSE.txt and /usr/share/doc/libopenscap8/copyright.
No subscription, licence key, Ubuntu Pro entitlement or support contract is required for any of it. "Ubuntu" 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 Canonical, 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:
- Website: www.cloudimg.co.uk
- Product Catalogue: www.cloudimg.co.uk/products
- User Guides: www.cloudimg.co.uk/guides
- SLA: 24 hour response guaranteed