Ubuntu 24.04 Minimal
Overview
This guide covers the deployment and configuration of Ubuntu Server 24.04 LTS Minimal on Microsoft Azure using SecureAnyCloud's pre-configured virtual machine image from the Azure Marketplace.
Ubuntu is a widely used Linux distribution produced by Canonical. Version 24.04 is a long-term support (LTS) release, with standard security maintenance to 2029.
What makes this image different from the standard Ubuntu Server 24.04 LTS image is the package profile. The installed set is trimmed to Canonical's own published Ubuntu Minimal cloud image seed, which is the package set Canonical defines as Minimal, plus the Azure provisioning stack. Measured at capture on this exact image, the standard image has 664 packages and this one has 322 (342 fewer). The dpkg payload is 902 MiB smaller, and the root filesystem ships over a gigabyte leaner. Fewer installed packages means a smaller attack surface, fewer packages to patch and scan, and faster update runs.
The image describes itself. A manifest generated from its own package database ships at /usr/share/sac/ubuntu-24-04-minimal.manifest. It states exactly what is installed, what was removed and what that means in practice. Anything you miss is one apt install away from the standard Ubuntu archives.
What you get:
- Ubuntu Server 24.04 LTS with the Minimal package profile: 322 packages, cut to Canonical's published Minimal seed from SecureAnyCloud's own hardened standard image
- The full Azure provisioning path, protected from the trim by construction: cloud-init, the Azure Linux Agent (walinuxagent), OpenSSH, netplan, the Azure kernel flavour and its initramfs boot chain
- A shipped manifest and trim ledger derived from the image's own package database, so the delta is measured, never asserted
- Every available security update applied at build time, with
unattended-upgradesarmed so the machine keeps patching itself (the update path is part of the Minimal seed) - Automatic root filesystem growth at first boot on larger disks (cloud-init growpart, proven below)
- chrony for NTP time synchronisation
- AppArmor in enforcing mode
- Gen2 Hyper-V virtual machine support
- 24/7 SecureAnyCloud support
What's deliberately NOT included (matching Canonical's Minimal seed, each is one apt install away): the man command, full screen editors (vim, nano), ping and network diagnostic tools, classic cron (systemd timers remain), rsyslog (journald remains), an NFS client, ufw, and hundreds of libraries and utilities a cloud VM managed by automation never touches.
Platform: Microsoft Azure (Gen2 Hyper-V, x64)
Default user: azureuser
Security posture
This image ships with no known credential. There is no default password and no baked-in SSH key:
- The root account password is locked, so no one can log in as root with a password.
PasswordAuthenticationis disabled in the SSH daemon, so every login is by key.PermitRootLoginis set toprohibit-password, so root cannot be reached with a password even if one were later set.- The only key that works is the public key you supply when you create the virtual machine. Azure injects it into
azureuserat first boot, so no two machines you launch ever share a secret. - The machine identity and the SSH host keys are regenerated uniquely on every instance, so no two machines share a host key either.
- AppArmor is left enabled and enforcing, the Ubuntu default.
- The trim itself is a security feature: 342 fewer installed packages is 342 fewer things to carry vulnerabilities, patch and audit.
Prerequisites
Before deploying this image, ensure you have:
- An active Microsoft Azure subscription
- Access to the Azure Portal or Azure CLI
- An SSH key pair for Linux VM access
- Familiarity with Azure VM management
Recommended VM Size: Standard_B2s (2 vCPU, 4 GB RAM) or larger. The Minimal profile is also comfortable on Standard_B1s for light duties.
Disk size: the image ships as a 30 GiB OS disk. If you deploy onto a larger disk, the root filesystem grows itself at first boot with no manual steps (see Step 7).
Deploy the Virtual Machine
Option A: Azure Portal
- Navigate to the Azure Marketplace and search for "Ubuntu 24.04 LTS Minimal SecureAnyCloud"
- 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)
- On the Disks tab, set the OS disk size you want. The root filesystem will fill it automatically at first boot
- Click Review + Create, then Create
Option B: Azure CLI
az vm image terms accept --urn sac1647283583153:ubuntu-24-04-minimal:default:latest
az vm create \
--resource-group myResourceGroup \
--name my-ubuntu-minimal-vm \
--image sac1647283583153:ubuntu-24-04-minimal:default:latest \
--size Standard_B2s \
--os-disk-size-gb 64 \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard--os-disk-size-gb 64 deploys the 30 GiB image onto a 64 GiB disk. Unlike an LVM layout, nothing further is needed: cloud-init grows the partition and the filesystem to fill the disk during first boot.
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-minimal-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.
Confirm the release and the Minimal profile
Check that the machine is the Ubuntu 24.04 LTS release you expect:
grep -E '^(NAME|VERSION|ID|VERSION_ID|PRETTY_NAME)=' /etc/os-release uname -srm dpkg --print-architectureExpected output:
PRETTY_NAME="Ubuntu 24.04.5 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04.5 LTS (Noble Numbat)" ID=ubuntu Linux 6.17.0-1022-azure x86_64 amd64The -azure kernel matters: this image keeps the Azure kernel flavour of the standard image, with the Hyper-V drivers built in, the same arrangement Canonical uses for its own Azure images.
Next, confirm the defining property of this image, the Minimal package profile:
dpkg-query -Wf '${Package}\n' | sed 's/:.*$//' | sort -u | wc -l df -h /Expected output on a fresh VM deployed with the default 30 GiB disk:
322 Filesystem Size Used Avail Use% Mounted on /dev/root 29G 1.3G 27G 5% /That is 322 packages against the standard image's 664. The trim was performed as a single guarded, simulated first package transaction against Canonical's published Minimal manifest. The provisioning path (cloud-init, the Azure agent, OpenSSH, netplan, the boot chain) is on the guard's protected list, so it cannot be trimmed by construction.
Read the shipped manifest
The image states what it is. The manifest is generated from the package database at capture and is never written by hand, so it cannot disagree with the machine you are on:
head -8 /usr/share/sac/ubuntu-24-04-minimal.manifestExpected output:
# Ubuntu 24.04 LTS Minimal — sac shipped manifest
# DERIVED from the dpkg database on this image at 2026-09-11T21:03:26Z (capture time) — never hand-written.
# Baseline: the sac 'Ubuntu Server 24.04 LTS' (std) image this Minimal is cut from.
#
# Measured trim delta vs the std baseline: 664 -> 322 packages (-342), dpkg payload -902 MiB.
# Trim target: Canonical's published Ubuntu 24.04 Minimal cloud-image package seed (manifest sha256 c65cc54232807f02...). # Guard refusals (candidates KEPT because their removal would cascade a keeper): dracut-install hwdata klibc-utils libdw1t64 libip4tc2 libip6tc2 libklibc libnetfilter-conntrack3 libnfnetlink0 libnftnl11 libnl-3-200 libnl-genl-3-200 libnuma1 libpopt0 libpython3.12t64 libslang2 libtraceevent1 python3-passlib python3-setuptools tzdata-legacy usb.ids # Installed now: 322 packages, dpkg payload 614 MiB, rootfs used 1288 MiB.The capability notes further down tell you, in plain terms, what is not on board and how to restore each piece:
sed -n '/capability notes/,/^#$/p' /usr/share/sac/ubuntu-24-04-minimal.manifestExpected output:
# --- capability notes, each DERIVED from a probe on this image --- NFS: this image has NO NFS client. nfs-common is not part of the Ubuntu Minimal seed, NFS: so 'mount -t nfs' will not work as shipped. Restore it with:
sudo apt install nfs-common MAN: no man command (man-db is not part of the Minimal seed). Restore with:
sudo apt install man-db CRON: no classic cron daemon (cron is not part of the Minimal seed); systemd timers CRON: provide scheduling. Restore classic cron with:
sudo apt install cron EDITOR: no full-screen editor ships (matching Canonical's Minimal seed). EDITOR: Install one with: sudo apt install nano (or vim) PROVISIONING: cloud-init 26.1 + walinuxagent 2.15.0.1 are installed — the full Azure provisioning path is retained. KERNEL: 6.17.0-1022-azure (the Azure kernel flavour — Hyper-V drivers built in, matching Canonical's own Azure minimal SKUs). UPDATES: unattended-upgrades is installed and armed — security updates auto-apply, exactly as on the std image. DOCS: unlike Canonical's minimal cloud image, this image does NOT strip documentation of DOCS: RETAINED packages at the dpkg level — the trim is package-level; /usr/share/doc of the kept set is intact.The full installed package list follows the notes in the same file, one package per line with its version. It is useful for compliance snapshots and drift comparisons.
Check the patch level
The image is fully updated at build time. Confirm that no upgrades are pending and that the update path is intact:
apt-get -s -o APT::Get::Always-Include-Phased-Updates=true dist-upgrade | grep -c '^Inst ' | sed 's/^/pending upgrades: /' apt-mark showhold dpkg -l | awk '/^ii linux-image-[0-9]/{print $2, $3}'Expected output shows pending upgrades: 0, no held packages, and exactly one installed Azure kernel:
pending upgrades: 0 linux-image-6.17.0-1022-azure 6.17.0-1022.22The Always-Include-Phased-Updates=true flag matters on Ubuntu: without it, a plain check can silently hide phased security updates held back on this particular machine. This image also arms unattended-upgrades, which is part of Canonical's Minimal seed, so automatic security updates survive the trim by design.
You can still run updates on your own schedule:
sudo apt-get update && sudo apt-get -y dist-upgradeReboot if a new kernel was installed:
sudo systemctl rebootVerify the security posture
Confirm AppArmor is enforcing, root is locked, and SSH is key only:
sudo aa-status --enabled && echo "apparmor: enabled"
sudo passwd -S root
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication) 'Expected output:
apparmor: enabled root L 2009-12-22 -1 -1 -1 -1 permitrootlogin without-password pubkeyauthentication yes passwordauthentication nowithout-password is how OpenSSH reports the prohibit-password setting. Both names mean the same thing: root may not authenticate with a password. The L from passwd -S means the root account is locked.
Disk growth: automatic on this image
If you deployed onto a disk larger than 30 GiB (as in the Step 1 CLI example), there is nothing to do: this image keeps the standard flat root layout, and cloud-init's growpart module extends the root partition and filesystem during first boot. Confirm it:
df -h / lsblk -o NAME,SIZE,TYPE,MOUNTPOINT "$(readlink -f /dev/disk/azure/root)"Expected output on a 64 GiB disk, exactly as produced by this image:
Filesystem Size Used Avail Use% Mounted on /dev/root 61G 1.3G 60G 3% / NAME SIZE TYPE MOUNTPOINT sda 64G disk |-sda1 63G part / |-sda14 4M part |-sda15 106M part /boot/efi `-sda16 913M part /bootThe proof of the automatic grow is in cloud-init's own log:
grep 'resized' /var/log/cloud-init.log | tail -1Expected output (byte counts vary with your disk size):
2026-09-11 19:23:20,360 - cc_growpart.py[INFO]: '/' resized: changed (/dev/disk/by-partuuid/054c3354-42bf-4a6d-8c6f-2cf42bd83e63) from 31138496000 to 67644669440Growing an existing VM later: enlarge the OS disk (deallocate the VM, az disk update --resource-group <rg> --name <os-disk-name> --size-gb <new-size>, start the VM, or use the portal's Disks blade). cloud-init grows the root again on the next boot; no manual partitioning is ever needed on this layout.
Networking and the firewall
The Azure Network Security Group is the control plane for inbound and outbound traffic, and it is the first and usually the only place you need to open a port. The image advertises a single inbound port, SSH on 22. Confirm what is listening:
sudo ss -tlnpExpected output shows sshd on port 22 (IPv4 and IPv6), and systemd-resolved on the loopback-only stub addresses:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1098,fd=3),("systemd",pid=1,fd=98)) LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=564,fd=17)) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=564,fd=15)) LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=1098,fd=4),("systemd",pid=1,fd=99))ufw is not installed on this image, because it is not part of Canonical's Minimal seed. The NSG is the enforced firewall, which is the normal arrangement on Azure. If your policy requires a host firewall as well, install and enable one, adding the SSH rule first so you do not lock yourself out:
sudo apt install -y ufw
sudo ufw allow OpenSSH
sudo ufw enableOnce ufw is running, a port must be open in both the Azure NSG and ufw before traffic reaches your service. (The iptables layer itself is present. The Azure agent uses it to protect the platform metadata endpoint.)
Install software
The trim removes nothing from your reach: the machine points at the standard Ubuntu archives, and every removed package, like anything else in the archive, installs normally. Search, then install what you need:
apt-cache search --names-only '^nginx' 2>/dev/null | sed -n '1,3p'Install a package and enable it in the usual way, for example sudo apt-get install -y nginx followed by sudo systemctl enable --now nginx. The everyday absences and their one-line restores are in the shipped manifest (Step 4): man-db for manual pages, nano or vim for an editor, iputils-ping for ping, cron for classic cron jobs, nfs-common for NFS mounts.
To add another administrator, create the account with sudo adduser <name>, add it to the sudo group with sudo usermod -aG sudo <name>, then copy your public key into /home/<name>/.ssh/authorized_keys with mode 600 and the .ssh directory mode 700, owned by that user. The image ships with no human accounts at all, so azureuser is the only login until you add one:
getent group sudoExpected output:
sudo:x:27:azureuserCommon Use Cases
- Container and automation hosts that want the smallest practical Ubuntu base under their workloads
- Security-conscious deployments: fewer installed packages to patch, scan and audit
- Fleets managed entirely by SSH and configuration management, where interactive niceties are dead weight
- A fast-booting, quick-patching Ubuntu LTS base for ephemeral and scale-out workloads
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, so a wrong key cannot fall back to a password prompt.
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.
"command not found" for man, vim, nano, ping or crontab
This is expected. Those tools are not part of Canonical's Minimal seed, which this image is cut to. Each is one command away (see the shipped manifest, Step 4): sudo apt install man-db, sudo apt install nano, sudo apt install iputils-ping, sudo apt install cron. Until an editor is installed, remember that sed -i and here-documents cover most quick edits, and systemctl edit opens its own fallback editor.
df shows 29G on a bigger disk
The automatic grow runs during first boot. If you resized the disk while the VM was running, the VM has not rebooted since the disk grew. Reboot once (sudo systemctl reboot) and cloud-init claims the space. Check the evidence trail in /var/log/cloud-init.log: a cc_growpart line reading '/' resized: changed means the grow ran; '/' NOCHANGE: no change necessary means the partition already fills the disk.
Azure agent not running
sudo systemctl status walinuxagentIf it is not running, start it. Run this yourself on the VM:
sudo systemctl enable --now walinuxagentPackage manager issues
Refresh the metadata cache first:
sudo apt-get updateIf repositories are unreachable, check DNS resolution. getent is always present and resolves through the same system resolver:
getent hosts archive.ubuntu.comA service is blocked and the logs mention AppArmor
AppArmor is enforcing on this image. A healthy machine has no denials, as measured on this exact build:
sudo journalctl -k --no-pager | awk '/apparmor/ && /DENIED/{n++} END{print "apparmor denials: " n+0}'Expected output:
apparmor denials: 0If you see denials after installing software, the denied profile's name appears in the log line. Adjust or disable that one profile rather than turning AppArmor off.
Important Notes
Ubuntu is free and open-source software. It is not covered by a single licence: it is a distribution assembled from thousands of independently packaged components, each carrying its own terms, including the GPL, LGPL, MIT, BSD, Apache 2.0 and MPL. Every package's terms are readable with dpkg -s <package> and under /usr/share/doc/<package>/copyright. No subscription or licence key is required to run this image; Ubuntu Pro is an optional Canonical subscription that is neither included nor required here.
Ubuntu 24.04 LTS receives standard security maintenance until 2029. Plan a move to a newer LTS image before that date for workloads that need to outlive it.
This image's Minimal profile is package level: the installed set matches Canonical's published Minimal cloud image seed (plus the Azure provisioning stack), while documentation belonging to the packages that remain is kept on disk. Canonical's own minimal cloud image additionally strips documentation files at the dpkg level; if you want that behaviour you are better served installing their image outside the Marketplace. This image chooses the less surprising default.
Ubuntu is a trademark of Canonical Ltd. The name is used here nominatively, only to identify the distribution this image contains. SecureAnyCloud is not affiliated with, endorsed by or sponsored by Canonical Ltd.
Support
For assistance with this image, contact SecureAnyCloud support:
- Website: secureanycloud.com
- Product Catalogue: secureanycloud.com/products
- User Guides: secureanycloud.com/guides
- SLA: 24 hour response guaranteed
Deploy on Azure
Launch Ubuntu 24.04 LTS Minimal on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
This guide covers launching and verifying the Ubuntu 24.04 LTS Minimal AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud Ubuntu listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Ubuntu 24.04 is a long term support (LTS) release from Canonical, with standard security maintenance to 2029. This image starts from Ubuntu Server 24.04 LTS for EC2 and adds four things:
- A trimmed package set. 23 packages a cloud server does not need are removed (the release upgrader, boot splash and recovery menu, crash reporters, firmware updaters, iSCSI and multipath, LXD helpers, telnet and ftp), and snapd is purged with every snap. Each removal is simulated first and refused if it would take a package the instance needs to boot, network or be reached. The list ships on the image.
- CIS Level 1 Server remediation. The image is remediated with the OpenSCAP CIS Level 1 Server profile from SCAP Security Guide 0.1.82, then rebooted and evaluated. On this build 347 of 351 scored rules pass (98.9%). Six rules are waived because they cannot work on an EC2 image, each with its reason. The HTML report, the machine-readable results, the waiver list and a summary ship in
/usr/share/doc/sac/. - EC2 defaults that keep you in control. Key-only SSH as
ubuntu, root locked, no password on any account, IMDSv2 required, a host firewall (ufw) that allows SSH in and everything out, the AWS Systems Manager agent installed as a normal package, and time from the Amazon Time Sync Service. - Fully patched at build time, with unattended security upgrades switched on for your instance.
What is on the image: Ubuntu Server 24.04 LTS (AWS kernel), cloud-init, OpenSSH, ufw, chrony, the AWS Systems Manager agent, AppArmor, AIDE file integrity checking, OpenSCAP and the pinned SCAP Security Guide content used for the evaluation.
Default user: ubuntu; Architecture: x86_64; Root volume: 8 GiB gp3 (grows to the size you choose at launch)
These images are remediated with an OpenSCAP profile and ship their own evaluation report. They are not CIS Hardened Images, and CIS has not reviewed or certified them.
Security posture
- No known credential. There is no default password and no baked in SSH key. The only key that works is the EC2 key pair you choose at launch, which cloud-init installs for
ubuntuon first boot. - Key-only SSH.
PasswordAuthentication no,PermitRootLogin no, keyboard-interactive off. The root password is locked. - Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance, so no two instances share a host key.
- IMDSv2 required. The AMI is registered with
ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests by default. - Host firewall. ufw denies inbound traffic except SSH on 22 and allows outbound traffic, so the SSM agent, metadata service, DNS, time and package updates keep working.
- Only SSH listens on the network. Everything else on the image listens on loopback only.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- A security group that allows TCP 22 from your own address (not from the whole internet)
Recommended instance type: m5.large. The image runs on any current x86_64 instance type.
Launch the instance
Option A: AWS Marketplace console
- Open the SecureAnyCloud Ubuntu listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard), then in Version choose Ubuntu 24.04 LTS Minimal - YYYY-MM-DD (the date is when the version was added) and your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: allow SSH (port 22) from your own IP address only.
- Storage: the root volume is 8 GiB gp3 by default. Choose a larger size if you need one; the root filesystem grows to fill it on first boot.
- Metadata: leave the defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the key pair, security group and subnet with your own:
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids sg-0123456789abcdef0 \
--subnet-id subnet-0123456789abcdef0 \
--block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=20,VolumeType=gp3}' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=ubuntu-minimal}]'You do not need --metadata-options: the AMI already makes IMDSv2 required.
Connect via SSH
From your workstation, connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>There is no password to enter. If SSH asks for one, or says Too many authentication failures, see Troubleshooting below.
You can also connect without opening port 22 at all through AWS Systems Manager Session Manager, if the instance has an instance profile that allows Systems Manager. The agent is installed and running on this image.
Confirm the release
grep -E '^(NAME|VERSION|ID|VERSION_ID|PRETTY_NAME)=' /etc/os-release uname -srm dpkg --print-architectureExpected output:
PRETTY_NAME="Ubuntu 24.04.5 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04.5 LTS (Noble Numbat)" ID=ubuntu Linux 7.0.0-1014-aws x86_64 amd64The -aws kernel is Canonical's kernel flavour for EC2, with the Nitro network and NVMe drivers built in. Your kernel version may be newer if updates have installed since this image was built.
See what the Minimal trim removed
The image lists exactly what was removed, and anything that a removal would have taken with it is kept and listed instead:
cat /usr/share/doc/sac/minimal-manifest.txt dpkg-query -W -f='${Package}\n' | wc -lExpected output:
Minimal version: packages 685 -> 569 Removed: landscape-common ubuntu-release-upgrader-core apport apport-symptoms friendly-recovery ntfs-3g open-iscsi multipath-tools plymouth fwupd fwupd-signed modemmanager bolt udisks2 usb-modeswitch pollinate byobu motd-news-config lxd-agent-loader lxd-installer sosreport telnet ftp Kept (a removal would have taken a protected package, or it failed): 585The trim takes the package count from 685 to 569, including the dependencies that left with those 23 packages. The CIS remediation then adds the tools it needs (OpenSCAP, AIDE and their libraries), which is why the installed count is a little higher. The count on your instance moves by a few as kernels and updates install.
Anything removed is one sudo apt-get install away from the standard Ubuntu archives.
Read the compliance evidence
The evaluation the build ran after its reboot ships on the image:
ls -lh /usr/share/doc/sac/ python3 -c "import json; s=json.load(open('/usr/share/doc/sac/cis-l1-summary.json')); print('profile: ', s['profileLabel'], '(SCAP Security Guide', s['ssgVersion'] + ')'); print('scored rules: ', s['scored'], ' pass:', s['pass'], ' fail:', s['fail'], ' rate:', str(s['ratePct']) + '%'); print('waived: ', s['waived'], ' not applicable:', s['notapplicable']); print('evaluated at: ', s['evaluatedAt'])"Expected output:
total 21M -rw-r--r-- 1 root root 18M Oct 19 07:47 cis-l1-arf.xml -rw-r--r-- 1 root root 3.4M Oct 19 07:47 cis-l1-report.html -rw-r--r-- 1 root root 1.3K Oct 19 07:47 cis-l1-summary.json -rw-r--r-- 1 root root 2.1K Oct 19 07:47 cis-l1-tailoring.xml -rw-r--r-- 1 root root 2.3K Oct 19 07:47 cis-l1-waivers.txt -rw-r--r-- 1 root root 437 Oct 19 07:38 minimal-manifest.txt -rw-r--r-- 1 root root 1.6K Oct 19 07:38 scap-security-guide-LICENSE.txt profile: CIS Level 1 Server (SCAP Security Guide 0.1.82) scored rules: 351 pass: 347 fail: 4 rate: 98.9% waived: 6 not applicable: 51 evaluated at: 2026-10-19T07:47:15Zcis-l1-report.htmlis the full OpenSCAP report, rule by rule. Copy it to your workstation and open it in a browser.cis-l1-arf.xmlis the same evaluation in the machine-readable ARF format, for compliance tooling.cis-l1-tailoring.xmlis the exact tailoring used, so you can re-run the same evaluation (Step 6).- The pass rate is pass / (pass + fail + error + unknown). Rules that are not applicable to this machine, not selected, or waived are not counted.
To copy the report to your workstation, run this on the workstation:
scp -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>:/usr/share/doc/sac/cis-l1-report.html .The six waived rules
grep -v '^#' /usr/share/doc/sac/cis-l1-waivers.txt | cut -f1,2Expected output:
partition_for_tmp [single-volume-partitions] grub2_password [serial-console-and-boot] grub2_uefi_password [serial-console-and-boot] accounts_maximum_age_login_defs [key-only-account-aging] account_disable_post_pw_expiration [key-only-account-aging] sshd_limit_user_access [ssh-access-list]The full reason for each is the third column of the same file. In short:
- Separate /tmp partition: this version boots from one EBS root volume. (The LVM versions of this listing provide separate volumes for /tmp, /var, /var/log, /var/log/audit, /var/tmp and /home.)
- Boot loader passwords: a GRUB password would have to be baked into the image, giving every instance the same secret. Set your own if you need one (see Step 11).
- Password ageing defaults: the accounts cloud-init creates are key-only. An ageing limit would expire them and lock the SSH key out.
- SSH AllowUsers/AllowGroups: a list fixed at build time would refuse accounts you create later. Your security group restricts who can reach port 22.
The four rules that do not pass
python3 -c "import json; [print(r) for r in json.load(open('/usr/share/doc/sac/cis-l1-summary.json'))['failingRules']]"Expected output:
all_apparmor_profiles_in_enforce_complain_mode set_nftables_loopback_traffic set_nftables_table set_ufw_default_ruleWhat each one means on this image:
all_apparmor_profiles_in_enforce_complain_mode: Ubuntu 24.04 loads some AppArmor profiles in its own "unconfined" mode. AppArmor is enabled and the enforcing profiles are enforced, as in stock Ubuntu.set_nftables_loopback_trafficandset_nftables_table: these check a hand-written nftables configuration. The firewall on this image is ufw, so they do not describe it.set_ufw_default_rule: the benchmark wants outbound traffic denied by default. This image allows outbound so the SSM agent, metadata service, DNS, time sync and updates work out of the box.
Re-run the evaluation yourself
You can repeat the evaluation at any time with the same content and tailoring. It takes about a minute:
sudo oscap xccdf eval \
--tailoring-file /usr/share/doc/sac/cis-l1-tailoring.xml \
--profile xccdf_co.sac_profile_cis-l1_ec2_single \
--results /tmp/cis-check-results.xml --report /tmp/cis-check-report.html \
/usr/share/xml/scap/ssg/content/ssg-ubuntu2404-ds.xml > /tmp/cis-check.log 2>&1 echo "oscap exit code: $? (2 = at least one rule did not pass)"
sudo python3 -c "import re,collections; c=collections.Counter(re.findall(r'<result>(\w+)</result>', open('/tmp/cis-check-results.xml').read())); s=sum(c[k] for k in ('pass','fail','error','unknown')); print(dict(sorted(c.items()))); print('pass rate: %.1f%%' % (100.0*c['pass']/s))"Expected output on a freshly launched instance:
oscap exit code: 2 (2 = at least one rule did not pass) {'fail': 4, 'notapplicable': 51, 'notselected': 246, 'pass': 347} pass rate: 98.9%Your own report is at /tmp/cis-check-report.html. As you install software and change settings, re-run this to see how your changes affect the result.
Confirm IMDSv2 is required
curl -s -o /dev/null -w 'IMDSv1 request without a token: HTTP %{http_code}\n' http://169.254.169.254/latest/meta-data/instance-id
TOKEN=$(curl -s -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token)
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" -w '\n' http://169.254.169.254/latest/meta-data/placement/regionExpected output (your Region may differ):
IMDSv1 request without a token: HTTP 401 us-east-1A request without a session token is refused with HTTP 401, and the same request with a token works. Scripts and SDKs on the instance must use IMDSv2; every current AWS SDK and the AWS CLI do this automatically.
The host firewall and listening ports
sudo ufw status verbose
sudo ss -tlnpExpected output:
Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- Anywhere on lo ALLOW IN Anywhere 22/tcp ALLOW IN Anywhere 68/udp ALLOW IN 67/udp
# DHCP client: replies from a DHCP server (port 67) 53 on lo ALLOW IN Anywhere
# systemd-resolved loopback stub 127.0.0.54:53 (lo only) Anywhere DENY IN 127.0.0.0/8 Anywhere (v6) on lo ALLOW IN Anywhere (v6) 22/tcp (v6) ALLOW IN Anywhere (v6) 53 (v6) on lo ALLOW IN Anywhere (v6)
# systemd-resolved loopback stub 127.0.0.54:53 (lo only) Anywhere (v6) DENY IN ::1 Anywhere ALLOW OUT Anywhere on lo Anywhere (v6) ALLOW OUT Anywhere (v6) on lo State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=338,fd=17)) LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1171,fd=3),("systemd",pid=1,fd=48)) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=338,fd=15)) LISTEN 0 100 127.0.0.1:25 0.0.0.0:* users:(("master",pid=1110,fd=13)) LISTEN 0 100 [::1]:25 [::]:* users:(("master",pid=1110,fd=14)) LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=1171,fd=4),("systemd",pid=1,fd=49))Only sshd listens on the network. The DNS stub resolver (53) and the local mail relay (25, for system mail) listen on loopback only.
The 68/udp rule admits only the replies of the VPC's DHCP server to the instance's own DHCP client (the client that keeps the instance's private address), and the 53 on lo rules cover the systemd-resolved stub resolver on the loopback interface only. ufw already accepts both; the explicit rules open nothing new to the network.
Opening a port for your own service takes two steps: allow it in the instance's security group, and allow it in ufw. For example, for a web server on 443, run on the instance:
sudo ufw allow 443/tcpIf a port is open in the security group but not in ufw (or the other way round), the traffic will not arrive.
Time sync and the Systems Manager agent
systemctl is-active chrony amazon-ssm-agent chronyc -n sources dpkg -s snapd >/dev/null 2>&1 && echo "snapd: installed" || echo "snapd: not installed"Expected output (the pool servers and timings vary):
active active MS Name/IP address Stratum Poll Reach LastRx Last sample =============================================================================== ^* 169.254.169.123 3 4 177 15 -9626ns[ -93us] +/- 570us ^- 91.189.91.157 2 6 17 63 +286us[ +261us] +/- 29ms ^- 185.125.190.58 2 6 17 63 +328us[ +299us] +/- 42ms ^- 185.125.190.56 2 6 17 63 +4262us[+4238us] +/- 46ms ^- 185.125.190.57 2 6 17 62 +5040us[+5013us] +/- 46ms ^- 209.253.210.112 1 6 17 63 +1812us[+1782us] +/- 9227us ^- 192.231.84.116 2 6 17 62 -331us[ -357us] +/- 38ms ^- 45.127.113.2 2 6 17 64 +193us[ +165us] +/- 11ms ^- 23.186.168.126 2 6 17 64 +147us[ +120us] +/- 54ms snapd: not installedThe ^* marks the source chrony is using: 169.254.169.123, the Amazon Time Sync Service. It is reachable from every instance without internet access, so time stays correct in a private subnet too. The public Ubuntu pool servers are fallbacks.
The AWS Systems Manager agent is installed from Amazon's Debian package rather than as a snap, and snapd is removed.
Patch level and automatic updates
The image is fully patched at build time. Check what is pending now:
sudo apt-get update -qq apt-get -s -o APT::Get::Always-Include-Phased-Updates=true dist-upgrade | awk '/^Inst /{n++} END{print "pending upgrades: " n+0}' apt-mark showhold uname -rExpected output on an instance launched soon after the image was built (later launches show the updates released since):
pending upgrades: 0 7.0.0-1014-awsunattended-upgrades is on, so security updates install automatically every day. To update everything now, run:
sudo apt-get update && sudo apt-get -y dist-upgradeReboot if a new kernel was installed:
sudo systemctl rebootSecurity settings on the instance
sudo aa-status --enabled && echo "apparmor: enabled"
sudo passwd -S root
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries|clientaliveinterval) ' cat /etc/issue.netExpected output:
apparmor: enabled root L 2024-04-23 0 99999 7 -1 maxauthtries 4 clientaliveinterval 300 permitrootlogin no pubkeyauthentication yes passwordauthentication no Authorized users only. All activity may be monitored and reported.Lmeans the root password is locked.maxauthtries 4andclientaliveinterval 300come from the CIS profile: SSH allows four authentication attempts per connection and checks idle sessions every five minutes.- The banner text is shown before login.
EC2 Serial Console and recovery. No account on this image has a password, so the serial console cannot log in until you set one. If you want serial console access, set a password for your own user on your running instance (the password is yours and is not on the image):
sudo passwd ubuntuDisk size, swap and file integrity
df -h /
sudo awk '/cc_growpart/ && /(resized|NOCHANGE)/' /var/log/cloud-init.log | tail -1 swapon --noheadings --show | wc -l systemctl list-timers --all --no-pager | awk '/dailyaidecheck/'Expected output with the default 8 GiB root volume (on a larger volume, Size is larger and the cloud-init line says resized):
Filesystem Size Used Avail Use% Mounted on /dev/root 6.8G 2.1G 4.7G 32% / 2026-10-19 08:08:06,777 - cc_growpart.py[DEBUG]: '/' NOCHANGE: no change necessary (/dev/nvme0n1, 1) 0 Tue 2026-10-20 03:34:20 UTC 19h Mon 2026-10-19 07:39:59 UTC - dailyaidecheck.timer dailyaidecheck.service- Disk growth. cloud-init grows the root partition and filesystem to the volume size on first boot. To grow later, increase the EBS volume size in the console or with
aws ec2 modify-volume, then reboot (or runsudo growpart /dev/nvme0n1 1 && sudo resize2fs /dev/nvme0n1p1). - Swap. The image has no swap. Add a swap file yourself if your workload needs one.
- AIDE. The file integrity database is built from the image's own files, and a daily check runs from the
dailyaidechecktimer.
Install software and add users
The instance uses the standard Ubuntu archives:
sudo apt-get update -qq apt-cache policy nginx | sed -n '1,3p'Expected output (the version changes over time):
nginx: Installed: (none) Candidate: 1.24.0-2ubuntu7.18Install and start software in the usual way, for example sudo apt-get install -y nginx then sudo systemctl enable --now nginx, and open its port in the security group and ufw (Step 8).
To add another administrator, create the account, add it to the sudo group and give it an SSH key:
sudo adduser --disabled-password --gecos "" alice
sudo usermod -aG sudo alice
sudo install -d -m 700 -o alice -g alice /home/alice/.ssh echo "ssh-ed25519 AAAA... alice@laptop" | sudo tee /home/alice/.ssh/authorized_keys
sudo chown alice:alice /home/alice/.ssh/authorized_keys && sudo chmod 600 /home/alice/.ssh/authorized_keysBecause SSH is key-only, a new account needs its key in authorized_keys. The new account has no password, so sudo cannot authenticate it until you give it one with sudo passwd alice. SSH stays key-only; the password is used by sudo. (Only ubuntu, created by cloud-init, has password-free sudo.)
Common use cases
- A base for workloads that must show CIS Level 1 alignment, with the evaluation report already on the machine
- Container hosts and automation targets that want a small Ubuntu LTS base with no snaps
- Fleets managed through Systems Manager, with IMDSv2 required from the first boot
- A starting point for your own golden image: add your software, re-run Step 6, and keep the report with your build
Troubleshooting
SSH says "Too many authentication failures"
The SSH server allows four authentication attempts per connection. If your SSH agent holds several keys, it can use them up before it offers the right one. Tell SSH to offer only the key you name:
ssh -o IdentitiesOnly=yes -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>Cannot connect via SSH
- Check the instance is running and has passed its status checks.
- Check the security group allows TCP 22 from your current public address.
- Use the user name
ubuntuand the key pair chosen at launch. Password login is disabled, so a wrong key never falls back to a password prompt. - If you changed ufw, make sure 22/tcp is still allowed. Session Manager (Step 2) still works if SSH is blocked, provided the instance has a Systems Manager instance profile.
My service is running but not reachable
Allow the port in both the security group and ufw (Step 8). sudo ufw status shows the ufw rules; sudo ss -tlnp shows whether the service listens on all addresses or only on loopback.
A tool that uses instance metadata fails
The image requires IMDSv2. Old scripts that call http://169.254.169.254/ without a token get HTTP 401. Update them to request a token first (Step 7), or use the AWS CLI or a current SDK.
Package manager issues
Refresh the package lists first:
sudo apt-get updateIf the archives are unreachable, check that the instance has outbound internet access (a public IP or a NAT gateway) and that DNS resolves:
getent hosts archive.ubuntu.comImportant notes
Ubuntu is free and open source software. It is a distribution of thousands of packages, each under its own licence (GPL, LGPL, MIT, BSD, Apache 2.0 and others); each package's terms are in /usr/share/doc/<package>/copyright. No subscription or licence key is needed to run this image. The compliance tooling is OpenSCAP (LGPL 2.1 or later) and SCAP Security Guide content (BSD 3-Clause, licence text in /usr/share/doc/sac/scap-security-guide-LICENSE.txt).
Ubuntu 24.04 LTS receives standard security maintenance until 2029.
The CIS Level 1 Server profile used here is the OpenSCAP profile published in SCAP Security Guide. These images are not CIS Hardened Images and are not certified by the Center for Internet Security. A pass rate measures this image as built; your own changes affect it, which is why the evaluation can be re-run (Step 6).
Ubuntu is a trademark of Canonical Ltd. CIS is a trademark of the Center for Internet Security. Both names are used only to identify the software and the profile. SecureAnyCloud is not affiliated with or endorsed by Canonical Ltd or the Center for Internet Security.
Support
For help with this image, contact SecureAnyCloud support:
- Website: secureanycloud.com
- User Guides: secureanycloud.com/guides
- Ubuntu on AWS hub: secureanycloud.com/guides/ubuntu-server-aws
Deploy on AWS
Launch Ubuntu 24.04 LTS Minimal on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com