Author, validate, and debug cloud-init configuration for Ubuntu Server LTS 24.04 and 26.04, focused on on-premise and air-gapped hosts via the NoCloud datasource — `#cloud-config` user-data, the cloud-config modules (users, ssh, write_files, runcmd, apt with local mirrors, ca_certs for internal CAs, ntp, disk_setup), NoCloud seeding (seed dir, `cidata` ISO, `ds=nocloud;s=...` kernel cmdline, SMBIOS serial), boot stages, and pinning `datasource_list`. Public-cloud datasources (EC2/Azure/GCE…) ...
Scanned 9/3/2026
Install to Claude Code
npx -y skills add air-gapped/skills --skill ubuntu-cloud-init --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ubuntu Cloud Init?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/air-gapped-ubuntu-cloud-init)More formats (shields.io, HTML) on the badges page.
---
name: ubuntu-cloud-init
description: >-
Author, validate, and debug cloud-init configuration for Ubuntu Server LTS 24.04
and 26.04, focused on on-premise and air-gapped hosts via the NoCloud datasource —
`#cloud-config` user-data, the cloud-config modules (users, ssh, write_files,
runcmd, apt with local mirrors, ca_certs for internal CAs, ntp, disk_setup),
NoCloud seeding (seed dir, `cidata` ISO, `ds=nocloud;s=...` kernel cmdline, SMBIOS
serial), boot stages, and pinning `datasource_list`. Public-cloud datasources
(EC2/Azure/GCE…) are pointers only. For the `network:` block use the ubuntu-netplan
skill; for the installer schema use the ubuntu-autoinstall skill.
when_to_use: >-
Use whenever the task mentions cloud-init, `#cloud-config`, user-data / meta-data /
vendor-data, NoCloud, a seed ISO, first-boot provisioning,
`cloud-init schema|status|clean|query`, or what an Ubuntu host does on first boot —
even when the user just says "set up users and packages on first boot" or "seed a
VM with cloud-init".
---
# ubuntu-cloud-init
cloud-init runs early in boot, reads configuration from a **datasource**, and applies
it through **modules** across four boot stages. This skill is authoring-led (produce
correct cloud-config from a description) with a strong validation/debug path, aimed
at **on-prem / air-gapped Ubuntu Server LTS** using the **NoCloud** datasource.
Mapping: 24.04 LTS ≈ cloud-init 24.x; 26.04 LTS ≈ 26.1.
## Authoring workflow
1. **Identify the goal & datasource.** On-prem/air-gapped → **NoCloud** (seed dir,
`cidata` ISO/USB, HTTP seed, or SMBIOS serial). Pin
`datasource_list: [NoCloud]` to stop cloud-init probing cloud metadata services
and timing out.
2. **Choose the user-data format** — almost always `#cloud-config`. Use MIME multipart
only to combine cloud-config with scripts. (Formats table below.)
3. **Write the `#cloud-config`** using the modules needed (see Quick reference).
4. **Validate:** `cloud-init schema -c user-data --annotate`.
5. **Seed it** (NoCloud) and set a unique `instance-id` in `meta-data`.
6. **Test on a throwaway boot**, then `cloud-init clean --logs` to re-run as if
first boot.
## user-data formats
First line determines the format:
| Header (line 1) | Format |
|---|---|
| `#cloud-config` | YAML config processed by modules (the usual choice). |
| `#!/bin/sh` (shebang) | A script, run once per instance in the Final stage. |
| `#cloud-boothook` | Runs very early, **every boot** (guard with `cloud-init-per`). |
| `#include` | List of URLs, each fetched as user-data. |
| `Content-Type: multipart/mixed` | MIME — combine cloud-config + scripts. Build with `cloud-init devel make-mime`. |
| `## template: jinja` | Jinja template (line 1); real header on line 2. Variables = instance-data keys, e.g. `{{ v1.instance_id }}`. |
Any of these may be gzipped.
## Quick reference — the modules that matter on-prem
**users / ssh** (created in the Network stage):
```yaml
#cloud-config
ssh_pwauth: false
users:
- default # KEEP this first to retain the distro user + cloud keys
- name: deploy
groups: [sudo]
sudo: "ALL=(ALL) NOPASSWD:ALL"
lock_passwd: true
shell: /bin/bash
ssh_authorized_keys:
- ssh-ed25519 AAAA... deploy@site
```
`passwd` (a hash) applies only to *new* users; `hashed_passwd` applies even to
existing ones. Generate a hash with `mkpasswd --method=SHA-512 --rounds=4096`.
**write_files:**
```yaml
write_files:
- path: /etc/myapp/app.conf
owner: root:root
permissions: '0640'
content: |
backend = db.internal:5432
- encoding: b64 # also gz, gz+b64
content: aGVsbG8K
path: /etc/motd.d/banner
defer: true # write in Final stage (after users/packages)
```
**runcmd vs bootcmd:** `runcmd` runs once per instance in the Config stage (later);
`bootcmd` runs early on *every* boot (guard once-only work with
`cloud-init-per once <name> ...`).
```yaml
runcmd:
- [systemctl, enable, --now, myapp]
```
**apt — local mirror + internal repo** (the air-gapped essential):
```yaml
apt:
preserve_sources_list: false
primary:
- arches: [default]
uri: http://mirror.internal/ubuntu
security:
- arches: [default]
uri: http://mirror.internal/ubuntu
sources:
internal-app.list:
source: "deb [signed-by=$KEY_FILE] http://mirror.internal/app stable main"
key: |
-----BEGIN PGP PUBLIC KEY BLOCK-----
...embed the key inline; do NOT rely on a keyserver air-gapped...
-----END PGP PUBLIC KEY BLOCK-----
conf: |
Acquire::http::Proxy "http://apt-cache.internal:3142";
```
**ca_certs — trust an internal CA** (runs in the Network stage, *before* apt, so
HTTPS to the internal mirror works):
```yaml
ca_certs:
trusted:
- |
-----BEGIN CERTIFICATE-----
<internal root CA>
-----END CERTIFICATE-----
```
**ntp — point at local time source:**
```yaml
ntp:
enabled: true
servers: [ntp.internal]
pools: []
```
Other on-prem-relevant modules: `packages`/`package_update`/`package_upgrade`
(avoid `package_upgrade` without a full mirror), `disk_setup`+`fs_setup`+`mounts`,
`growpart`/`resize_rootfs`, `timezone`/`locale`/`keyboard`, `set_passwords`/`chpasswd`,
`seed_random`, `power_state`, hostname (`hostname`/`fqdn`/`manage_etc_hosts`). **Avoid
`phone_home`** air-gapped — it only calls out to a URL and retries on failure. Full
per-module detail (keys, snippets, stage, frequency) in `references/modules.md`.
## NoCloud seeding (the air-gapped core)
cloud-init finds NoCloud config from (in precedence order) SMBIOS serial → kernel
cmdline → seed dirs → labeled block device. The common methods:
- **Seed directory:** drop `meta-data` + `user-data` (and optionally `vendor-data`,
`network-config`) into `/var/lib/cloud/seed/nocloud/`.
- **`cidata` ISO/USB:** a `vfat`/`iso9660` filesystem labeled `CIDATA` (case-insensitive)
containing those files in its root.
`genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data network-config`
(or `cloud-localds seed.iso user-data meta-data`).
- **HTTP seed:** kernel cmdline `ds=nocloud;s=http://10.0.0.1:8000/` (the scheme
decides local vs network; **trailing slash required** — files are appended as
`<uri>/user-data` etc.).
- **SMBIOS serial** (QEMU/libvirt): `-smbios type=1,serial=ds=nocloud;s=http://10.0.0.1:8000/`.
**Required files:** `meta-data` and `user-data`. **Optional:** `vendor-data`,
`network-config`. Since cloud-init 24.3, an HTTP/seed source that *omits*
`network-config` triggers a boot retry/timeout — ship an **empty `network-config`**
for back-compat, or a real netplan-v2 one.
**`instance-id` re-run rule:** cloud-init only re-applies user-data when the
`instance-id` in `meta-data` *changes* (or after `cloud-init clean`). Bump it
whenever the config changes.
Full seeding detail (cmdline grammar + aliases, DMI variable expansion, FTP, GRUB
escaping, `seedfrom`, dsname deprecations) is in `references/nocloud-and-airgapped.md`.
## Network config (hand-off to netplan)
cloud-init network-config has v1 (its own schema) and **v2 (which IS netplan
format)**. On Ubuntu the renderer is netplan; cloud-init writes
`/etc/netplan/50-cloud-init.yaml`. **user-data cannot set network config** —
networking comes from the datasource, system config, or kernel cmdline.
To author the network block, or to hand network control back to netplan entirely
(`network: {config: disabled}` in `/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg`),
use the **ubuntu-netplan** skill.
## Validation & debugging (fast path)
```bash
cloud-init schema -c user-data --annotate # validate a file, errors inline
sudo cloud-init schema --system --annotate # validate the live system's user-data
cloud-init status --long --wait # 0=ok, 1=error, 2=recoverable error
cloud-init query --all # inspect instance-data
sudo cloud-init clean --logs # wipe state → next boot is "first boot"
```
Logs: `/var/log/cloud-init.log`, `/var/log/cloud-init-output.log`,
`/run/cloud-init/` (incl. `ds-identify.log`, `result.json`, `instance-data.json`).
Config: `/etc/cloud/cloud.cfg` + `/etc/cloud/cloud.cfg.d/*.cfg`. Full CLI, the boot
stages, and re-run/golden-image notes in `references/cli-and-debugging.md`.
## Air-gapped checklist
- Pin `datasource_list: [NoCloud]` (or `[NoCloud, None]`) in a `cloud.cfg.d` drop-in
— stops cloud probing/timeouts.
- Local apt mirror via the `apt` module; embed repo signing keys inline.
- Internal CA via `ca_certs.trusted`.
- Local NTP via `ntp.servers`.
- Omit `phone_home`; don't `package_upgrade` without a full mirror; avoid `snap`
unless a local store / pre-acked assertions are available.
- Use `vendor-data` for site-wide defaults (mirror, CA, NTP, base users); per-host
`user-data` overrides it.
## Upstream breaking changes worth knowing (`doc/rtd/reference/breaking_changes.rst`)
Verified against `main` on 2026-07-21. Ubuntu vendors patch some of these out —
the doc says so itself — so confirm against the image before assuming.
**25.1.4 — strict datasource identity before network. The one most likely to
bite an on-prem/ARM fleet.** `ds-identify` now requires strict identification
from **DMI platform data, the kernel command line, or an explicit
`datasource_list:`** in `/etc/cloud/cloud.cfg.d`. Previously, a platform without
clear identifying data fell into a *late discovery mode* that brought networking
up and reached out to well-known link-local IPs to fetch config — the hardening
exists to stop a bad actor on the local network answering those requests.
- **Most affected: Ec2, OpenStack and AltCloud on non-x86**, where the kernel
may not expose DMI data.
- **Failure mode is silent-ish:** if no datasource is identified, cloud-init
**stays disabled and performs no configuration at all during boot** — the
machine simply comes up unconfigured.
- **Mitigations:** launch with `openstack server create … --config-drive true`,
or pin `datasource_list:` explicitly. This is a second, stronger reason for
the `datasource_list: [NoCloud]` pin already recommended in the air-gapped
checklist — it is no longer only about avoiding probe timeouts.
Also recorded, lower operational impact for this skill's use cases:
| Version | Change |
|---|---|
| **26.1** | OpenStack bond names are no longer hard-coded to `bond0`/`bond1`… — they now take whatever `network_data.json` provides |
| **25.3** | Systemd **socket protocol changed** for compatibility with `openbsd-netcat` alternatives (e.g. `nmap`'s `ncat -U`) — **downstreams shipping a custom `ExecStart=` must update**. Build backend moved **setuptools/distutils → meson** (PEP-0632); packagers should diff the generated package |
| **25.1** | `/usr` merge — packaging installs nothing to `/lib` any more, everything to `/usr/lib`. Affects non-systemd / older / non-Linux distros |
| **24.4** | `cloud-final.service` ordering standardized — changed the systemd boot order on some distributions |
| **24.3** | `cloud-init.service` → `cloud-init-network.service` (already covered above) |
| **24.1** | `ds-identify` no longer auto-appends `None` to a single-entry `datasource_list` (already covered above) |
## Boundaries (sibling skills)
- **`network:`** → **ubuntu-netplan** skill (v2 is netplan format).
- **Ubuntu Server install automation** → **ubuntu-autoinstall** skill. The installer
is itself driven by a NoCloud `user-data` carrying a top-level `autoinstall:` key;
cloud-init *ignores* `autoinstall` and passes it to Subiquity. Inside that document,
the nested `user-data:` is cloud-config for the *installed* system (first boot).
## Reference files
- `references/nocloud-and-airgapped.md` — every NoCloud seeding method, cmdline
grammar, SMBIOS/DMI, `instance-id`, `datasource_list` pinning, DataSourceNone,
vendor-data, deprecations.
- `references/modules.md` — full cloud-config module reference (on-prem modules in
depth; the rest listed by stage). Has a TOC.
- `references/cli-and-debugging.md` — CLI, boot stages & module ordering, re-run /
golden-image, version notes (24.x vs 26.1).
- `references/examples.md` — minimal and realistic air-gapped configs with matching
`meta-data`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!