Ubuntu Server 22.04 on AWS User Guide
Overview
This image is Ubuntu Server 22.04 LTS (Jammy Jellyfish), one software version of the cloudimg Hardened Ubuntu 22.04 & 24.04 LTS AMIs: Minimal, LVM, STIG listing on AWS Marketplace. Every release and variant in the listing is described at /guides/ubuntu-server-aws/.
Host firewall enabled (ufw, inbound SSH only): open app ports in both the security group and ufw. Opening a port in the security group alone is not enough.
What cloudimg does to the image:
- Remediated with the OpenSCAP CIS Level 1 Server profile. The build applies the SCAP Security Guide 0.1.82 profile
cis_level1_serverwithoscap --remediate, reboots, then runs a scored evaluation. The build fails if fewer than 95% of the scored rules pass; this version scored 338 of 343 rules (98.5%). The report ships in the image at/usr/share/doc/cloudimg/cis-l1-report.html. This is a measurement made by open-source tooling, not a certification or accreditation by the benchmark's publisher or any other body, and these images are not CIS Hardened Images. - 6 rules waived, each with its reason (listed below and in
/usr/share/doc/cloudimg/cis-l1-waivers.txt): rules that would lock you out of an EC2 instance or need a password baked into every copy of the image. - Fully patched at build time. The build fails if any update is outstanding or a reboot is pending.
- Automatic security updates stay on (unattended-upgrades).
- Key-only SSH as
ubuntu. Password login is off, root login is refused, the root account is locked and no account has a password. - IMDSv2 only. The AMI is marked IMDSv2-only, so instances launched from it require session tokens for instance metadata.
- Host firewall on (ufw): inbound SSH only, outbound open so the SSM agent, updates, DNS and NTP work.
- AppArmor enforcing, chrony on the Amazon Time Sync Service (169.254.169.123, no internet access needed), and the AWS Systems Manager (SSM) agent installed from its Debian package (snapd and every snap are removed).
- Nothing shared between instances. No SSH keys, host keys, machine ID or credentials are baked in; cloud-init creates them on each instance's first boot.
- No swap, and SSH is the only service listening on the network (the DNS stub resolver, chrony and the local mail transfer agent listen on loopback only).
Ubuntu 22.04 LTS receives standard security maintenance from Canonical until 2027. For a longer runway on a new deployment, the Ubuntu 24.04 LTS versions of the same listing are maintained until 2029.
Connecting to your instance
Connect over SSH as the default login user for the operating system variant you launched:
| Operating system variant | SSH login user |
|---|---|
| Ubuntu Server 22.04 LTS | ubuntu |
Launch from AWS Marketplace
- Open the 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 full control), then in Version choose Ubuntu Server 22.04 LTS - YYYY-MM-DD (the date is when the version was added) and your Region.
- Instance type:
m5.largeis recommended; any current x86_64 type works. The software charge is $0.04 per vCPU per hour. - Key pair: choose your EC2 key pair. It is the only way in. RSA and ED25519 keys both work.
- Security group: allow inbound TCP 22 from your own IP address or network only.
- Storage: the root volume is 8 GiB gp3 by default. Choose a larger size if you need one; cloud-init grows the root filesystem to fill it on first boot.
- Metadata: leave the defaults. The image already requires IMDSv2. Then choose Launch.
From the AWS CLI (use the AMI ID the Marketplace launch page shows for your Region):
aws ec2 run-instances --image-id ami-xxxxxxxxxxxxxxxxx --instance-type m5.large \
--key-name my-key --security-group-ids sg-xxxxxxxx --subnet-id subnet-xxxxxxxx
Connect over SSH
ssh -i ~/.ssh/my-key.pem ubuntu@<public-ip>
ubuntu has passwordless sudo. There is no password on any account. AWS Systems Manager Session Manager also works: the SSM agent is installed and running (attach an instance profile with AmazonSSMManagedInstanceCore).
The SSH server allows four authentication attempts per connection. If your SSH agent holds several keys and the connection fails with Too many authentication failures, add -o IdentitiesOnly=yes so SSH offers only the key you name.
Confirm the release and kernel:
grep -E '^(PRETTY_NAME|VERSION_ID|VERSION_CODENAME)=' /etc/os-release
uname -r
PRETTY_NAME="Ubuntu 22.04.5 LTS"
VERSION_ID="22.04"
VERSION_CODENAME=jammy
6.8.0-1066-aws
The -aws kernel is Canonical's kernel flavour for EC2. Its version moves forward as Ubuntu publishes updates; the image ships the kernel current on its build date.

IMDSv2
The AMI carries ImdsSupport=v2.0, so every instance launched from it gets HttpTokens=required. Check it on the instance:
imds_token=$(curl -s -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 300' http://169.254.169.254/latest/api/token)
curl -s -H "X-aws-ec2-metadata-token: $imds_token" http://169.254.169.254/latest/meta-data/instance-id; echo
curl -s -o /dev/null -w '%{http_code}\n' http://169.254.169.254/latest/meta-data/instance-id
The first request (with a token) returns your instance ID; the second (IMDSv1, no token) is refused with 401:
i-0123456789abcdef0
401
Software that still uses IMDSv1 must be updated to request a token (current AWS SDKs and CLIs already do). If you must allow IMDSv1 temporarily, change it per instance with aws ec2 modify-instance-metadata-options --http-tokens optional; we do not recommend it.
View the compliance report
The evaluation the build ran is in /usr/share/doc/cloudimg/cis-l1-report.html (HTML), /usr/share/doc/cloudimg/cis-l1-arf.xml (machine-readable ARF) and /usr/share/doc/cloudimg/cis-l1-summary.json (counts, pass rate, threshold, SSG version and checksums). The build host's name and addresses are replaced in the shipped copy.
ls /usr/share/doc/cloudimg/
python3 -c "import json; d=json.load(open('/usr/share/doc/cloudimg/cis-l1-summary.json')); print(d['profileLabel'], d['ssgVersion'], '-', d['pass'], 'of', d['scored'], 'scored rules pass', '(' + str(d['ratePct']) + '%)', '- waived', d['waived'])"
cis-l1-arf.xml
cis-l1-report.html
cis-l1-summary.json
cis-l1-tailoring.xml
cis-l1-waivers.txt
scap-security-guide-LICENSE.txt
CIS Level 1 Server 0.1.82 - 338 of 343 scored rules pass (98.5%) - waived 6

Copy the HTML report to your workstation and open it in a browser:
scp -i ~/.ssh/my-key.pem ubuntu@<public-ip>:/usr/share/doc/cloudimg/cis-l1-report.html .
The pass rate is pass / (pass + fail + error + unknown). Rules that are not applicable to this machine, not selected, or waived are not counted.
The five rules that do not pass
python3 -c "import json; [print(r) for r in json.load(open('/usr/share/doc/cloudimg/cis-l1-summary.json'))['failingRules']]"
ensure_root_access_controlled
service_nftables_enabled
set_nftables_loopback_traffic
set_nftables_table
set_ufw_default_rule
What each one means on this image:
ensure_root_access_controlled: the Ubuntu 22.04 version of this check passes only when root has a password hash. A root password on the image would be the same secret on every instance, so root has no password at all and is locked instead (sudo passwd -S rootshowsL, below). Nobody can log in as root; administrators usesudo.service_nftables_enabled,set_nftables_loopback_trafficandset_nftables_table: these belong to the profile's nftables firewall option. The firewall on this image is ufw, andnftables.serviceis masked so it can never start and flush ufw's rules at boot. Running two firewall front ends at once is what the benchmark itself warns against.set_ufw_default_rule: the benchmark wants outbound traffic denied by default. This image allows outbound on purpose (see the deliberate deviation below).
Re-run the same evaluation on your instance at any time (a few minutes; nothing is changed):
sudo oscap xccdf eval --tailoring-file /usr/share/doc/cloudimg/cis-l1-tailoring.xml \
--profile xccdf_co.cloudimg_profile_cis-l1_ec2_single \
--report ~/cis-l1-rescan.html /usr/share/xml/scap/ssg/content/ssg-ubuntu2204-ds.xml
oscap exits with status 2 when any rule fails; that is expected and the HTML report is still written. As you install software and change settings, re-run it to see how your changes affect the result.
Waived rules
These rules of the profile are deselected through the tailoring file (/usr/share/doc/cloudimg/cis-l1-tailoring.xml) and are not scored. Everything else the profile selects is remediated and scored. The tailoring also selects ufw as the profile's firewall option and chrony as its time service.
grep -v '^#' /usr/share/doc/cloudimg/cis-l1-waivers.txt | cut -f1,2
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]
| Rule | Category | Why it is waived on EC2 |
|---|---|---|
partition_for_tmp |
single-volume-partitions | The standard and Minimal versions boot from one EBS root volume, so /tmp, /var, /var/log, /var/log/audit, /var/tmp and /home live on the root filesystem. The LVM versions provide every one of these as a separate volume with the benchmark's mount options. |
grub2_password |
serial-console-and-boot | A boot-loader or root password would have to be baked into the image, giving every instance the same credential. Root stays locked and no password exists. EC2 Serial Console and single-user recovery need a password you set yourself on your own instance (see Emergency access). |
grub2_uefi_password |
serial-console-and-boot | As above, for the UEFI boot loader. |
accounts_maximum_age_login_defs |
key-only-account-aging | Password-age and inactivity defaults apply to every account cloud-init creates at launch. Those accounts are key-only and have no password, so an age limit expires them and locks the SSH key out. Accounts that do have a password are still aged. |
account_disable_post_pw_expiration |
key-only-account-aging | As above, for the inactivity default. |
sshd_limit_user_access |
ssh-access-list | An AllowUsers/AllowGroups list fixed at build time would refuse SSH to accounts you create with cloud-init user-data. SSH is key-only, root login is refused, and your security group restricts the source addresses. |
Deliberate deviation (scored as a failure): ufw denies all inbound traffic except SSH (22/tcp) but allows outbound traffic, so set_ufw_default_rule reports fail and counts against the score. A default-deny outbound policy would cut the SSM agent off from its endpoints, cloud-init and the instance from the metadata service (169.254.169.254), and the instance from DNS, NTP and package updates.
Host firewall and SSH
ufw is active and allows inbound SSH only; SSH accepts keys only:
sudo ufw status verbose
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|pubkeyauthentication) '
sudo passwd -S root
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)
Anywhere DENY IN 127.0.0.0/8
Anywhere (v6) on lo ALLOW IN Anywhere (v6)
22/tcp (v6) ALLOW IN Anywhere (v6)
Anywhere (v6) DENY IN ::1
Anywhere ALLOW OUT Anywhere on lo
Anywhere (v6) ALLOW OUT Anywhere (v6) on lo
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
root L 10/14/2026 0 99999 7 -1
root L means the root password is locked. 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); ufw already accepts those replies, and the explicit rule opens nothing new.

To serve anything else, open the port in both your security group and the host firewall, for example HTTPS:
sudo ufw allow 443/tcp
sudo ufw status verbose
Listening ports
sudo ss -tlnu
Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 172.31.83.113%ens5:68 0.0.0.0:*
udp UNCONN 0 0 127.0.0.1:323 0.0.0.0:*
udp UNCONN 0 0 [::1]:323 [::]:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 100 127.0.0.1:25 0.0.0.0:*
tcp LISTEN 0 128 [::]:22 [::]:*
tcp LISTEN 0 100 [::1]:25 [::]:*
Only sshd (22/tcp) listens on the network. The DNS stub resolver (53), chrony's control port (323) and the local mail relay (25, for system mail) listen on loopback only, and UDP 68 on the private address is the DHCP client. Your own address appears in place of 172.31.83.113.
Services
systemctl is-active ssh ufw apparmor amazon-ssm-agent chrony
systemctl is-enabled unattended-upgrades
chronyc -n sources | head -3
active
active
active
active
active
enabled
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================================
^* 169.254.169.123 3 4 377 21 -4333ns[-4210ns] +/- 537us
^* marks the source chrony is synchronised to: the Amazon Time Sync Service at 169.254.169.123, which every EC2 instance reaches without internet access, so time stays correct in a private subnet too. The Ubuntu NTP pool servers in /etc/chrony/chrony.conf are kept as fallbacks.
Check that the system booted cleanly and that snapd is gone:
systemctl is-system-running
dpkg -s snapd >/dev/null 2>&1 && echo "snapd: installed" || echo "snapd: not installed"
running
snapd: not installed
Patching
unattended-upgrades installs security updates automatically. To see how many updates are waiting:
apt list --upgradable 2>/dev/null | grep -c upgradable || true
0
An instance launched soon after the image was built shows 0; later launches show the updates released since. To install everything now:
sudo apt-get update && sudo apt-get -y upgrade
If /var/run/reboot-required exists afterwards, a new kernel or core library was installed: reboot when convenient with sudo reboot.
Disk size and swap
df -h /
swapon --noheadings --show | wc -l
With the default 8 GiB root volume:
Filesystem Size Used Avail Use% Mounted on
/dev/root 7.6G 2.0G 5.6G 27% /
0
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 run sudo growpart /dev/nvme0n1 1 && sudo resize2fs /dev/nvme0n1p1). The image has no swap; add a swap file yourself if your workload needs one.
Emergency access
There is no password on any account, by design. EC2 Serial Console and single-user recovery need a local password: set one for root on your own instance before you need it, and keep it in your secrets store:
sudo passwd root
Session Manager (SSM) is the other way in if SSH is unavailable.
Licensing
Ubuntu is free and open source software: 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. The compliance tooling is OpenSCAP (LGPL 2.1 or later) and SCAP Security Guide content (BSD 3-Clause, licence text in /usr/share/doc/cloudimg/scap-security-guide-LICENSE.txt).
Support
cloudimg provides 24/7 technical support for this image by email and live chat: support@cloudimg.co.uk. Ubuntu is a registered trademark of Canonical Ltd. CIS and CIS Benchmarks are trademarks of the Center for Internet Security, Inc. cloudimg is not affiliated with or endorsed by Canonical or CIS.