Applications AWS

Incus 7.6 on AWS User Guide

| Product: Incus 7.6 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Incus 7.6 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened MeshCentral & Helpdesk and Project Management AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Incus is a system container and virtual machine manager from the LinuxContainers project, the community fork of LXD. A system container is a full Linux system rather than a single process: it boots its own init, runs its own services and behaves like a small independent server, while sharing the host kernel so it starts in about a second. You manage instances from the Incus web UI in a browser or with the incus command line client.

The cloudimg image adds:

  • HTTPS and a password from the first boot. nginx serves the Incus web UI and API on port 443 with a certificate generated on your instance's first boot, behind HTTP Basic Auth with a password generated on your instance and written to /root/incus-credentials.txt. Port 80 redirects to HTTPS.
  • The Incus API is never exposed. Incus listens on 127.0.0.1:8443 only. nginx is the only way in, and it authenticates to the API with a TLS client certificate minted on your instance at first boot, the only entry in the Incus trust store. Its private key is readable by root only.
  • Fail closed. No password, certificate or key is baked into the image. nginx refuses to start until first boot has written your instance's own password, server certificate and client certificate.
  • Pinned to your instance's address. nginx answers only to the address first boot found for your instance (its public IPv4, else its private IPv4) and drops requests for any other host name. Cross site requests and WebSocket connections from another origin are refused. sudo incus-set-host moves it to an Elastic IP or a DNS name.
  • Metadata service blocked. The AMI requires IMDSv2, and Incus, nginx and every container you run are blocked from the instance metadata service, so a workload inside a container cannot read your instance's IAM role credentials.
  • Ready to use. A default storage pool and an incusbr0 NAT bridge are configured, so you can launch a container straight away.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on, a recorded licence notice and a dependency CVE audit on the image.

Containers, not virtual machines, on most instance types. Incus can also run full virtual machines, but that needs hardware virtualisation (/dev/kvm), and EC2 exposes it only on bare metal instance types (*.metal). On every other type, including the recommended m5.large, this image runs system containers; incus launch --vm is refused. See Step 9.

What is on the image: Incus 7.6 (the incus and incus-ui-canonical packages from the Zabbly repository published by the Incus maintainer, pinned and verified against the repository's signing key), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (containers download their images from the LinuxContainers image server).
  • A security group that allows TCP 22 (SSH), TCP 443 (the web UI and API) and TCP 80 (redirects to HTTPS) from your own networks only.

Never open port 8443. The Incus API is bound to 127.0.0.1 and is not reachable from outside the instance by design. nginx on 443 is the only door it needs.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Incus 7.6 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (2 vCPU, 8 GiB) or larger for more containers, your key pair and a security group as above.

Containers are stored on the root volume (16 GiB by default). Give the root volume more space at launch if you plan to run many containers, or add a second EBS volume as a separate storage pool (Step 7).

Step 2: Connect to your instance

Version SSH user
Incus 7.6 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot takes a few seconds. Then the four units are active, and the client and server both report 7.6:

systemctl is-active incus incus-firstboot nginx cloudimg-incus-imds-guard
sudo incus version
active
active
active
active
Client version: 7.6
Server version: 7.6

The incus, incus-firstboot, nginx and cloudimg-incus-imds-guard units active, incus version reporting client and server 7.6, and core.https_address set to 127.0.0.1:8443

The Incus API listens on loopback only; nginx serves ports 80 and 443:

sudo ss -tlnp | awk 'NR==1 || /incusd|nginx|sshd/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer
LISTEN 127.0.0.1:8443 users:(("incusd"))
LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN 0.0.0.0:22 users:(("sshd"),("systemd"))
LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx"))
LISTEN [::]:22 users:(("sshd"),("systemd"))
LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))

ss showing incusd listening only on 127.0.0.1:8443 while nginx listens on ports 80 and 443 and sshd on 22

Step 4: Get your URL and password

sudo cat /root/incus-credentials.txt

The file is readable by root only. It holds the address the web UI is served as (your instance's public IP address by default), the username, the password generated on this instance and the SHA-256 fingerprint of the instance's certificate:

INCUS_UI_URL=https://<public-ip>/
INCUS_UI_USERNAME=admin
INCUS_UI_PASSWORD=<24 letters and digits>
TLS_CERT_SHA256=<the fingerprint your browser should show>

The credentials file owned by root with mode 600, its URL, username and certificate fingerprint lines, and a count showing exactly one password line, which is not displayed

If the web UI does not answer, first boot has not finished. nginx cannot start until incus-firstboot.service has generated this instance's password and certificates. Check systemctl status incus-firstboot.

Step 5: Sign in from your browser

Open INCUS_UI_URL in your browser and sign in as admin with the password from the file. The certificate is self signed (generated on your instance), so your browser warns once; compare the fingerprint it shows with TLS_CERT_SHA256, or install your own certificate (Step 11).

The Incus UI opens on Instances. A new instance starts with none; here three containers have been created:

The Incus web UI Instances page listing three containers, app-01 and web-01 running and db-01 stopped, with their Debian bookworm and Alpine 3.24 images, and the version 7.6-ui-0.22 in the footer

Step 6: Launch your first container

From the instance, launch a system container. Incus downloads the image from the LinuxContainers image server the first time and caches it:

sudo incus launch images:debian/12 app-01
Launching app-01

Then list your instances:

sudo incus list -c ns4t
┌────────┬─────────┬───────────────────────┬───────────┐
│  NAME  │  STATE  │         IPV4          │   TYPE    │
├────────┼─────────┼───────────────────────┼───────────┤
│ app-01 │ RUNNING │ 10.138.219.166 (eth0) │ CONTAINER │
└────────┴─────────┴───────────────────────┴───────────┘

Why sudo. Incus gives full control to members of the incus-admin group. The image does not add ubuntu to that group, because that would give anyone who reaches the account control of every instance without a further step. Run incus with sudo, or add an account deliberately with sudo usermod -aG incus-admin <user>.

Run a command inside it:

sudo incus exec app-01 -- sed -n 1,2p /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"

For an interactive shell run sudo incus exec app-01 -- bash. The container appears in the web UI at once, with its image, type, address and resource use:

The Incus web UI overview of the app-01 container: base image Debian bookworm, type Container, its IPv4 address on eth0, MAC address, architecture x86_64, and CPU, memory and root filesystem usage

The Terminal tab opens a shell inside the container from your browser:

The Terminal tab of app-01 in the Incus web UI, showing a root shell where head /etc/os-release reports Debian 12 bookworm, hostname -I shows the container address and apt-get update completes

Alpine containers. The Terminal tab opens bash, which Alpine does not ship. Use the Console tab or sudo incus exec <name> -- sh for Alpine containers.

Other lifecycle commands: sudo incus stop app-01, sudo incus start app-01, sudo incus restart app-01, sudo incus info app-01, and sudo incus delete -f <instance-name> to remove an instance.

Step 7: Storage

The default storage pool uses the dir backend, which stores each instance as a directory on the root volume. It works on every instance type and needs no kernel module.

sudo incus storage list
┌─────────┬────────┬─────────────────────────────────────────────┬─────────┬─────────┐
│  NAME   │ DRIVER │                 DESCRIPTION                 │ USED BY │  STATE  │
├─────────┼────────┼─────────────────────────────────────────────┼─────────┼─────────┤
│ default │ dir    │ cloudimg default storage pool (dir backend) │ 2       │ CREATED │
└─────────┴────────┴─────────────────────────────────────────────┴─────────┴─────────┘

The Incus web UI Storage Pools page showing the default pool with the dir driver, its size and usage, and status Created

The dir backend has no pool level snapshots or quotas. For those, attach a second EBS volume to the instance and create a ZFS pool on it (the device name is the one lsblk shows for the new volume, such as /dev/nvme1n1):

lsblk -o NAME,SIZE,MOUNTPOINT
sudo apt-get install -y zfsutils-linux
sudo incus storage create fast zfs source=/dev/nvme1n1
sudo incus launch images:debian/12 db-01 --storage fast

Both pools coexist and you choose one per instance.

Step 8: Networking

Instances attach to the managed incusbr0 bridge and get a private address with NAT egress, so they can reach the internet while staying unreachable from outside the instance.

sudo incus network list

The Incus web UI Networks page showing the managed incusbr0 bridge with its IPv4 subnet and the description cloudimg default Incus NAT bridge, and the physical ens5 interface

To publish a service from a container, forward a port from the instance to it:

sudo incus config device add app-01 http proxy listen=tcp:0.0.0.0:8080 connect=tcp:127.0.0.1:80
sudo incus config device show app-01
Device http added to app-01
http:
  connect: tcp:127.0.0.1:80
  listen: tcp:0.0.0.0:8080
  type: proxy

Then allow that port in the instance's security group, from your own networks only, for example with the AWS CLI:

aws ec2 authorize-security-group-ingress --group-id <your-security-group-id> --protocol tcp --port 8080 --cidr <your-mgmt-cidr>

Open only the ports you mean to publish, and never 8443.

Step 9: Virtual machines on EC2

Incus runs full virtual machines with QEMU, which needs hardware virtualisation. EC2 exposes /dev/kvm only on bare metal instance types such as m5.metal or c5.metal. On every other instance type the image runs system containers only, and Incus reports the lxc driver alone:

ls /dev/kvm 2>&1; sudo incus info | grep -E '^\s+driver:'
ls: cannot access '/dev/kvm': No such file or directory
  driver: lxc

On a *.metal instance type, /dev/kvm is present and sudo incus launch images:debian/12 vm-01 --vm starts a virtual machine. On other types that command is refused; containers are unaffected.

Step 10: Security checks

You can confirm how the image is secured on your own instance. Without the password nginx answers 401, and the Incus API on 8443 trusts nothing that does not present the instance's client certificate:

H=$(sudo sed -n 's/^HOST=//p' /etc/cloudimg/incus.conf)
curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w 'no password: HTTP %{http_code}\n' "https://$H/ui/"
curl -sk https://127.0.0.1:8443/1.0 | jq -r '.metadata.auth'
sudo incus config trust list -f csv -c n
no password: HTTP 401
untrusted
cloudimg-nginx-ui-proxy

The client certificate's private key and the server's TLS key are readable by root only, and Incus and nginx are denied the instance metadata service:

sudo stat -c '%a %U:%G %n' /etc/cloudimg-incus/ui-client.key /etc/cloudimg-incus/nginx-selfsigned.key
systemctl show -p IPAddressDeny incus.service nginx.service
600 root:root /etc/cloudimg-incus/ui-client.key
600 root:root /etc/cloudimg-incus/nginx-selfsigned.key
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32

Security checks: HTTP 401 without the password, the API on 8443 reporting untrusted, cloudimg-nginx-ui-proxy as the only trusted certificate, both private keys root only with mode 600, and IPAddressDeny on incus and nginx

Containers are blocked from the metadata service too, by the cloudimg_imds_guard nftables table, while their normal internet access works:

sudo incus exec -n app-01 -- timeout 5 bash -c 'echo > /dev/tcp/169.254.169.254/80' && echo reachable || echo blocked
sudo incus exec -n app-01 -- timeout 5 bash -c 'echo > /dev/tcp/deb.debian.org/80' && echo reachable || echo blocked
blocked
reachable

incus list showing the containers, the metadata address blocked from inside app-01 while deb.debian.org is reachable, and no /dev/kvm with the lxc driver

To change the web UI password:

sudo htpasswd -B /etc/cloudimg-incus/htpasswd admin
sudo systemctl reload nginx

Two optional Incus features run helper programs that this image leaves switched off. Setting acme.domain makes Incus run the bundled lego ACME client, and adding an OCI image remote makes it run the bundled skopeo. Neither has a newer upstream release to move to if an advisory is published against it, so the image configures neither; the build records both binaries in the CVE audit (Step 12). Prefer Certbot on nginx (Step 11) to acme.domain.

Step 11: Your own DNS name and certificate

After you attach an Elastic IP, after a stop and start changes the public IP, or to use a DNS name, point the instance at its new address. With no argument it uses the current public IP; it writes a new certificate and updates INCUS_UI_URL:

sudo incus-set-host incus.example.com

Then, with a DNS A record for the name pointing at the instance, install a trusted certificate with Certbot:

sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d incus.example.com

Your password and the API binding are unchanged.

Step 12: Licence and dependency audit

The image records what it ships and under which licence, verified at build time against the upstream artifacts, and the result of a vulnerability scan of the shipped filesystem:

head -14 /usr/share/doc/incus-cloudimg/LICENCE-NOTICE.txt
sed -n 1,9p /usr/share/doc/incus-cloudimg/cve-audit.txt
cloudimg Incus appliance - licence notice
=========================================
Verified at build time against the real upstream artifacts, not package metadata or badges.

Incus core  (packages: incus, incus-base, incus-client)   Apache-2.0
  Source : https://github.com/lxc/incus  @ tag v7.6.0
  Artifact verified : COPYING, byte-identical to https://www.apache.org/licenses/LICENSE-2.0.txt
                      sha256 cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30

Incus web UI (package: incus-ui-canonical)                GPL-3.0
  Source : https://github.com/zabbly/incus-ui-canonical  @ tag incus-0.22.1
           (a fork of https://github.com/canonical/lxd-ui)
  Artifact verified : LICENSE, byte-identical to https://www.gnu.org/licenses/gpl-3.0.txt
                      sha256 3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986
cloudimg Incus appliance - dependency CVE audit
Scanner  : Version: 0.76.2
Scanned  : 2026-11-07T12:35:06Z - Trivy rootfs scan of the SHIPPED filesystem
OS       : ubuntu 24.04
Packages enumerated : 716   (positive control: a scan of nothing FAILS the build)
Total findings      : 24033
HIGH/CRITICAL, fix available : 0   <-- the build gate; must be 0
HIGH/CRITICAL in lego/skopeo  : 0   (d-320 residual: no fixed upstream release; recorded, see below)
HIGH/CRITICAL, no fix yet    : 2245   (recorded, not gated: no version resolves these)

The build fails on any HIGH or CRITICAL finding that has a fixed version available. The findings with no fix yet are all Linux kernel CVEs against the newest kernel Ubuntu publishes, counted once per kernel sub-package; the file lists them as distinct CVE ids.

Incus core is Apache-2.0. The Incus web UI is GPL-3.0, conveyed verbatim and unmodified; the notice on the image names the exact upstream tag its corresponding source is published at, with a written offer to supply it. Nothing in this image or cloudimg's support terms restricts the rights those licences grant you.

Step 13: Keeping Incus up to date

Ubuntu security updates install automatically through unattended-upgrades, and no package is held back. Incus comes from the Zabbly repository, which unattended-upgrades does not install from, so the instance stays on the Incus release it shipped with until you upgrade it deliberately:

sudo apt-get update && sudo apt-get install -y incus incus-ui-canonical

Read the Incus release notes first, and snapshot the root volume before a major upgrade.

Troubleshooting

The web UI does not load. Check systemctl is-active incus incus-firstboot nginx. If nginx is inactive, first boot has not completed; see journalctl -u incus-firstboot. If the instance's public IP changed after a stop and start, run sudo incus-set-host.

The browser connection is closed with no page. nginx drops requests for host names it does not serve. Browse to the exact INCUS_UI_URL, or run sudo incus-set-host <name-or-ip> for the address you use.

I get a 401 and my password is correct. Sign in as admin, and re-read the password with sudo cat /root/incus-credentials.txt.

incus launch says the image cannot be found. The LinuxContainers image server retires old releases. Run sudo incus image list images: | grep <distro> to see what is published and use a current alias.

incus launch --vm fails. Full virtual machines need /dev/kvm, which EC2 provides only on *.metal instance types. Use system containers on other types.

Support and licensing

Covered by cloudimg support: email support@cloudimg.co.uk.