Cloud-init Template Images¶
netbox-packer can store a cloud-init user-data document in
PackerInstallerConfig and bake it into a Proxmox VM template through
proxbox-api. The plugin does not run local Packer for this path. It creates a
PackerBuild, queues PackerBuildJob, and delegates the real Proxmox work to
POST /cloud/templates/images on proxbox-api.
Flow¶
- Create or select a
PackerInstallerConfigwhereinstaller_type = "cloud_config". - Create or select a
PackerTemplatethat references the installer config. - Trigger the template build from NetBox, the API, or the NMS page at
/virtualization/packer. PackerBuildJobresolves the build node, base cloud image URL, storage, and SSH host.- The client sends the exact build body with
execute=false; proxbox-api renders it and returns the server-authoredrecipe_digest. - The client submits that digest and the endpoint, node, VMID, provider, and
storage target to
POST /cloud/templates/images/preflightcontract1.0. - Only when preflight reports ready and returns an unexpired signed
plan_token, the client resends the unchanged build fields withexecute=trueandpreflight_plan_tokenset. proxbox-apidownloads the base image, creates the VM, writes the cloud-init snippet as Proxmoxcicustomuser-data, converts the VM to a template, and returns a completed, executed, artifact-verified response.
The PackerPluginSettings singleton row must include proxbox_api_url and an
encrypted proxbox-api key (see Configuration — today this
is set via the Python shell only, not a UI page or REST endpoint). The target
ProxmoxEndpoint in netbox-proxbox must allow writes, and the selected Proxmox
storage must support snippets, import, and images.
This requires the signed-preflight contract available in
proxbox-api >= 0.0.19.post5. If the preflight endpoint returns 404, the service
is incompatible: netbox-packer fails the build with an upgrade message and does
not fall back to the unsafe legacy one-step execute call. Unreachable/not-ready
preflight, returned findings, missing or expired plan tokens, plan mismatch, and
unverified execution all remain visible in the build log and fail closed.
Creating a template from the web form¶
The template add/edit form (/plugins/packer/templates/add/) is optimised for
creating cloud-init images:
- OS family and OS version are both dropdowns. OS version is grouped by OS family (Ubuntu / Debian / RHEL / Proxmox) and, with JavaScript enabled, narrows to the family you pick. It works without JavaScript too — every version stays listed under its optgroup. An existing template whose stored version is not in the offered list keeps that value selectable, so editing an older template never fails validation.
- Machine-managed fields are hidden.
built_at,packer_template_ref,installer_config_checksum_at_build,base_image_url_at_build, andbase_image_sha256_at_buildare written byPackerBuildJob, so the form no longer asks operators to fill them in. They remain available read-only via the REST API. - Guidance help text is shown on
os_version,proxmox_template_id,storage_pool,cloud_init_ready, andinstaller_configto make picking the right values for a cloud-init bake obvious.
Adding a new OS version to the dropdown¶
Offered versions live in a single mapping,
OS_VERSIONS_BY_FAMILY, in netbox_packer/choices.py. Add a
("<version>", "<label>") tuple to the relevant family list — no database
migration is required, because the model field stays a free-form CharField
(the REST API keeps accepting any version string for automation).
OS_VERSIONS_BY_FAMILY = {
OSFamilyChoices.CHOICE_UBUNTU: [
("26.04", "Ubuntu 26.04 LTS"),
("24.04", "Ubuntu 24.04 LTS (Noble)"),
# add new Ubuntu releases here
],
# ...
}
Zabbix 7.4 Monitoring Stack Template¶
Migration 0006_seed_zabbix_cloud_init.py seeds the Zabbix 7.4 monitoring
server image.
| Field | Value |
|---|---|
| Template name | zabbix-7.4-ubuntu-2604-pgsql-nginx |
| Installer config | zabbix-7.4-ubuntu-2604-pgsql-nginx |
| OS | Ubuntu 26.04 |
| Template VMID | 9010 |
| Proxmox endpoint | https://10.0.30.139:8006 |
| Proxmox node / SSH host | 10.0.30.139 |
| Storage | local |
Guardrail: https://10.0.30.9:8006 / 10.0.30.9 is the production
netbox.nmulti.cloud Proxmox cluster. Do not seed, bake, or retarget this
monitoring-server template there. The seeded build target is the development
endpoint https://10.0.30.139:8006 only.
The cloud-init payload installs Zabbix Server 7.4, the PHP frontend, and Agent
2 on Ubuntu 26.04, backed by a local PostgreSQL database and nginx (PHP 8.5),
and initializes the Zabbix database schema on first boot. Because the seed
content already contains "zabbix-agent2", the build-time monitoring-agent
injection in jobs.py skips adding a second Zabbix Agent 2 install for this
template.
Kubernetes 1.31 Node Templates¶
Migrations 0009_seed_kubernetes_cloud_init.py and
0011_seed_k8s_role_templates.py seed three Kubernetes 1.31 node images on
CLUSTER01-DC01.
| Field | Base node | Control plane | Worker |
|---|---|---|---|
| Template name | k8s-1.31-ubuntu-2404-node |
k8s-1.31-control-plane-ubuntu-2404 |
k8s-1.31-worker-node-ubuntu-2404 |
| Template VMID | 9012 |
9013 |
9014 |
| OS | Ubuntu 24.04 |
Ubuntu 24.04 |
Ubuntu 24.04 |
| Proxmox endpoint | https://10.0.30.71:8006 |
https://10.0.30.71:8006 |
https://10.0.30.71:8006 |
| Proxmox node / SSH host | 10.0.30.71 |
10.0.30.71 |
10.0.30.71 |
| Storage | local |
local |
local |
All three cloud-init payloads install containerd and
kubelet/kubeadm/kubectl pinned to 1.31. The base node and control-plane
images additionally run kubeadm config images pull to pre-pull the
control-plane container images for a faster kubeadm init; the worker image
skips that pre-pull. qemu-guest-agent is enabled on all three.
These are pre-staged node images, not a running cluster: an operator still
runs kubeadm init on the control-plane clone and kubeadm join on worker
clones after provisioning.
InfluxDB OSS 2 and Core 3 Profiles¶
Migration 0020_seed_influxdb_profiles.py seeds two current, endpoint-agnostic
profiles and hardens the legacy collector row without deleting its Proxmox
artifact.
| Profile | Version | VMID | Port | Intended workload |
|---|---|---|---|---|
influxdb-oss-2.9.1-ubuntu-2404-proxmox-metrics |
OSS 2.9.1 |
9050 |
8086 |
Proxmox external metrics and Flux |
influxdb-core-3.11.0-ubuntu-2404 |
Core 3.11.0 |
9051 |
8181 |
SQL, InfluxQL, and processing-engine workloads |
influxdb-core-3.11.0-debian-13 |
Core 3.11.0 |
9052 |
8181 |
SQL and InfluxQL on Debian 13, with the production posture baked in |
Both rows store an empty proxmox_endpoint and select-at-build as the model
placeholder node. Each build request must provide a positive proxbox-api
variable_overrides.endpoint_id and a validated
variable_overrides.target_node. Optional validated template_vmid and
storage overrides select the destination identifiers. When an endpoint ID is present,
netbox-packer suppresses all legacy ssh_host values so proxbox-api derives
transport from the same selected endpoint it authorizes.
Hardened by migration
0026. Both profiles below (and the legacy9011row) originally shared the Debian 13 profile's three defects: a keyring trust boundary that admitted any extra key bundled with the genuine one, a version match that accepted~prereleases as the pinned release, and unbounded downloads and readiness probes that could hang first boot indefinitely.0026applies the same fixes made in0025. A row whose content no longer matches the exact0020baseline is not rewritten (that would discard an operator's edit) and not silently skipped — the migration fails with the offending rows named, so nobody deploys believing the vector was removed everywhere.0026also refuses to run while a build is queued or running against a linked template, and invalidates rebake state by installer-config relationship rather than by the editable template name. Both profiles additionally record a durable failure marker at/var/lib/nms/influxdb-install-failed, because cloud-init'sruncmdwrapper has noset -eand the injected Zabbix bootstrap runs after the installer, so a failure would otherwise be masked and reported as success.
The cloud-init profiles:
- trust exactly one repository key: the downloaded key is imported into an isolated
GNUPGHOMEand only fingerprint24C975CBA61A024EE1B631787C3D57159FC2F927is exported, with the exported keyring then asserted to hold a singlepubrecord; - select only final APT package versions matching
2.9.1or3.11.0— any~prerelease is refused in both candidate selection and post-install verification — then verify the installed version and applyapt-mark hold; - enable the correct fixed systemd service and wait for
/healthor/readywith per-probe connect/total timeouts and an overall deadline, so a socket that accepts a connection but never answers cannot hang first boot; - contain no user, password, token, organization, bucket/database, setup API request, private key, or endpoint-specific Proxmox data.
After a clone completes cloud-init, use the typed
service.influxdb.1.bootstrap, database_create, and token_create RPC
procedures through NMS or nms-cli. The backend generates/resolves one-time
plaintext only in memory and stores it through netbox-nms as nms-secret:
references. Routine config, managed/plugin files, service state, health, and
journal operations use the remaining typed InfluxDB RPC procedures.
The immutable historical influxdb-2-ubuntu-2404-proxmox-collector / VMID
9011 seed remains development-only on 10.0.30.139. Additive migration
0020 replaces the database row's former credential-generating installer
content with the safe OSS profile and marks it pending. Existing artifacts are
never deleted automatically.
InfluxDB 3 Core on Debian 13 (0025)¶
0025_seed_influxdb3_core_debian13_cloud_init.py adds
influxdb-core-3.11.0-debian-13 at VMID 9052. It shares the endpoint-agnostic
and credential-free contract above and adds the production posture of the
operator installer rather than leaving the server at package defaults.
| Field | Value |
|---|---|
| Template name | influxdb-core-3.11.0-debian-13 |
| Installer config | influxdb-core-3.11.0-debian-13-cloud-config version 3.11.0 |
| OS | Debian 13 (Trixie) |
| Template VMID | 9052 |
| Proxmox endpoint / node | selected at build (endpoint_id + target_node) |
| Storage | local |
| Listener | 127.0.0.1:8181 (loopback only) |
| Managed config | /etc/influxdb3/influxdb3-core.conf (root:influxdb3, 0640) |
| Data directory | /var/lib/influxdb3/data (influxdb3:influxdb3, 0750) |
| Systemd unit | influxdb3-core.service + drop-in 20-production.conf |
| Service marker | provisions_service = "influxdb3-core" |
The verbatim cloud-config source of truth is
netbox_packer/seeds/influxdb-core-3.11.0-debian-13.cloud-config.yaml; the
migration constant must stay byte-identical to it, which
tests/test_cloud_config_build_static.py asserts.
Beyond the shared behaviours listed above, first boot:
- refuses any release other than Debian 13 — it reads
/etc/os-releaseand exits non-zero unlessID=debianandVERSION_IDis13, and it also requires anamd64/arm64architecture and systemd as PID 1, rather than half-configuring an unverified platform; - writes the managed configuration with
object-store = "file", an explicitdata-dir,http-bind = "127.0.0.1:8181",log-filter,wal-flush-interval, anddisable-telemetry-upload = true, deliberately omittingplugin-dirso the Python Processing Engine stays disabled; - installs a
influxdb3-core.servicedrop-in withRestart=on-failure,RestartSec=5s, andTimeoutStopSec=120s; - derives
node-idfrom the per-VM SMBIOS UUID (/sys/class/dmi/id/product_uuid, falling back to/etc/machine-id) and fails closed if neither is readable — the hostname is not usable for this, because the clone pipeline reuses the template's cicustom meta-data and clones can therefore share a hostname; - holds the package with
apt-mark holdand waits on the unauthenticatedhttp://127.0.0.1:8181/readyendpoint with per-probe--connect-timeout/--max-timeplus an overall deadline, dumping unit status and journal tail before failing the boot script if readiness never arrives.
Two build-path details are specific to this profile:
- Base image.
os_version="13"resolves to the Trixie Debian 13 cloud image. The resolver previously returned Bookworm for every Debian row; since acloud_configbake never executes cloud-init, that would have produced an artifact markedreadywhose own OS gate fails at clone time. - Monitoring injection.
install_qemu_guest_agentis on;install_zabbix_agent2andinstall_nms_agentare off, because the shared injectors build an Ubuntu Zabbix package name fromVERSION_IDand the NMS agent bootstrap requires amd64. That also leaves this installer as the lastruncmdentry, so its failure is not masked by a later command in cloud-init'sset -e-less wrapper.
Token authentication stays enabled for every data and admin route: the readiness probe is the only unauthenticated call the image makes. Because a remote listener would then expose bearer tokens over plaintext HTTP, the baked posture is loopback-only — put a TLS reverse proxy in front of it, or use the audited RPC installer (below) with explicit TLS material.
For hosts that already exist, do not re-bake: netbox-rpc seeds
os.linux.debian.13.preflight_influxdb3_core (read) and
os.linux.debian.13.install_influxdb3_core (write, approval required), which
apply the same posture over audited SSH and accept the installer's parameters
(node_id, data_dir, http_bind, tls_cert/tls_key, enable_plugins,
disable_telemetry, wal_flush_interval, log_filter, package_version,
hold_package, upgrade_package, force_reconfigure,
allow_plaintext_remote). Those procedures also create no credential; the
sanctioned onboarding sequence remains preflight -> install ->
service.influxdb.1.bootstrap.
Known accepted risk (tracked in issue #96). Like every other seeded profile,
this one resolves the vendor's mutable latest image directory and passes no
content digest to proxbox-api, so a rebuild is not guaranteed to reproduce the same
root filesystem. Pinning a dated image plus a reviewed sha256 — and failing closed
when a pinned profile lacks one — is deliberately deferred to that issue rather than
half-implemented, because an unverified digest would look like provenance while
proving nothing.
Per the estate destructive-operation guardrail this migration seeds catalog rows
only and builds nothing. Confirm VMID 9052 is free on the destination cluster
before baking, and supersede a bad artifact by baking a new VMID rather than
deleting the previous one.
InfluxDB 3 Explorer on Debian 13 (0030)¶
0030_seed_influxdb3_explorer_debian13_cloud_init.py adds the independent
influxdb3-explorer-1.9.0-debian-13 template at VMID 9053. A separate
template gives Core and Explorer separate build runs and artifacts. Like the
Debian Core profile it is endpoint-agnostic, so each build supplies proxbox-api
endpoint_id and target_node.
| Field | Value |
|---|---|
| Template name | influxdb3-explorer-1.9.0-debian-13 |
| Installer config | influxdb3-explorer-1.9.0-debian-13-cloud-config version 1.9.0 |
| OS | Debian 13 (Trixie) |
| Template VMID | 9053 |
| Proxmox endpoint / node | selected at build (endpoint_id + target_node) |
| Storage | local |
| Container image | influxdata/influxdb3-ui@sha256:7df00684199c4b983b05b109e72e89aa23a0d6a9a9460d6b90cfd70f979023cc |
| Listener | configurable host IP, default 127.0.0.1:8080 |
| Persistent data | /var/lib/influxdb3-explorer mounted at /db |
| Provisioned config | /etc/influxdb3-explorer mounted read-only at /app-root/config |
| Systemd unit | influxdb3-explorer.service |
| Service marker | provisions_service = "influxdb3-explorer" |
The verbatim cloud-config source of truth is
netbox_packer/seeds/influxdb3-explorer-1.9.0-debian-13.cloud-config.yaml;
the migration constant must stay byte-identical to it. First boot refuses a
non-Debian-13 or unsupported-architecture guest, installs docker.io only from
Debian's signed repository, pulls the digest-addressed multi-architecture
manifest under an overall deadline, applies a maximum accepted image size,
and starts the container through the single systemd lifecycle owner. The local
readiness probe has per-attempt and overall deadlines. An EXIT trap plus
TERM/INT/HUP conversion records failures durably at
/var/lib/nms/influxdb-install-failed.
Explorer 1.9.0 runs as non-root uid/gid 1500. The installer therefore creates
the writable /var/lib/influxdb3-explorer data directory as 1500:1500 mode
0700, while /etc/influxdb3-explorer stays root:1500 mode 0750 and is
mounted read-only into the container. Provisioned config.json is mode 0640.
The default bind in /etc/default/influxdb3-explorer is loopback. An operator
may replace it with a specific host IP, but Explorer does not provide a second
user-authentication boundary around a configured InfluxDB connection: anyone
who can reach Explorer inherits that connection token's permissions. Remote
access therefore needs an explicit access-control design, normally an
authenticating TLS reverse proxy.
The golden image deliberately starts with no InfluxDB connection. After clone,
service.influxdb.1.token_create mints and vaults the Core token and returns
only an nms-secret:<opaque-id> reference. Provision-time automation resolves
that reference only in memory, writes root:1500 mode-0640
/etc/influxdb3-explorer/config.json, and restarts
influxdb3-explorer.service. Neither the reference nor its resolved value is
baked into cloud-init. The Ubuntu/amd64-only Zabbix and NMS agent injections
remain off; QEMU guest-agent injection remains on, and the Explorer installer
stays the last command in the fully injected runcmd list.
This is also a runtime bake boundary. For a template marked
provisions_service = "influxdb3-explorer", netbox-packer parses and validates
the fully injected YAML immediately before calling proxbox-api. A Core
endpoint/config file, credential-bearing key or value, private key, encoded
write_files content that cannot be inspected, or any non-placeholder
nms-secret: reference aborts the build with a log entry; proxbox-api is not
called. The pristine seed checks remain useful, but are not a substitute for
validating the editable payload that is actually baked.
Base Image Pinning (reproducible, verifiable OS bases)¶
A cloud-init bake downloads a vendor base image that becomes the guest's entire
operating system. By default netbox-packer derives that URL from the template's
os_family/os_version and points at the vendor's mutable latest directory,
sending no content digest — so rebuilding the same profile can silently produce a
different root filesystem, and the artifact is accepted with no integrity check.
PackerTemplate therefore carries two optional fields:
| Field | Purpose |
|---|---|
base_image_url |
An exact (normally dated) vendor artifact, replacing the derived release default. |
base_image_sha256 |
The reviewed digest, forwarded to proxbox-api as sha256 and verified after download. |
Currently pinned: one profile¶
Migration 0029 pins influxdb-core-3.11.0-debian-13 (VMID 9052) and nothing else:
base_image_url = https://cloud.debian.org/images/cloud/trixie/20260509-2473/debian-13-genericcloud-amd64-20260509-2473.qcow2
base_image_sha256 = 34f5481f320aef28408720a861582dcfe3a81781ee69f3910a64c29ad5395b89
Migration 0029 fails closed if that exact template row is missing and requires its
compare-and-set to update exactly one row, so a rename, deletion, or concurrent edit
cannot be recorded as a successful migration that pinned zero profiles.
Every other seeded profile is still unpinned and still resolves the vendor's mutable
latest directory with no digest. That is a known, accepted gap, not an oversight —
it closes per profile, by an operator who has verified a digest. Pinning is not free:
changing the pin on a template that is already ready marks it pending for rebake, so a
blanket pin across the catalog demands estate-wide rebakes.
Obtaining a digest you can defend¶
Debian publishes only SHA512SUMS for cloud images. There is no SHA256SUMS, and a
SHA-256 cannot be derived from a SHA-512, so the digest must come from hashing the
artifact yourself:
SNAP=20260509-2473 # a DATED directory, never latest/
FILE=debian-13-genericcloud-amd64-$SNAP.qcow2
BASE=https://cloud.debian.org/images/cloud/trixie/$SNAP
# 1. the vendor's published checksum for this exact file
curl -sSf "$BASE/SHA512SUMS" | grep " $FILE$"
# 2. the artifact itself
curl -sSfL --proto '=https' --tlsv1.2 -o "$FILE" "$BASE/$FILE"
# 3. THE STEP THAT MATTERS: prove the bytes you hashed are the bytes Debian published
# a checksum for. If this does not match, stop — do not pin.
sha512sum "$FILE"
# 4. only now derive the value for base_image_sha256
sha256sum "$FILE"
Never skip step 3, and never copy a checksum out of a listing into the field: that proves only that the listing and the field agree, while looking exactly like provenance.
Limit worth stating plainly: these snapshot directories carry no SHA512SUMS.sign, so
the trust chain is TLS to cloud.debian.org plus the published checksum. That is
meaningfully stronger than an unpinned mutable URL and meaningfully weaker than an
offline-verifiable Debian signature. Do not call a pinned image signature-verified.
To refresh a pin, repeat the whole procedure against the new snapshot — do not edit the
constants alone.
| base_image_url_at_build | Machine-managed resolved URL used by the last successful cloud-image build. |
| base_image_sha256_at_build | Machine-managed resolved digest used by the last successful cloud-image build. |
Both may also be supplied per build via variable_overrides['image_url'] and
variable_overrides['image_sha256'], which take precedence over the template fields.
Image URLs may not contain URL userinfo, a query string, or a fragment. The UI, API/model, and build-job boundaries parse and reject those components so inline tokens (including signed-download query credentials) cannot be persisted or sent to proxbox-api. Build logs, proxbox output, and provenance snapshots additionally use a URL with those components removed as defense in depth. A future authenticated-download flow must use an opaque secret reference, never an inline URL credential.
Migration 0028 adds the two *_at_build snapshots to both PackerBuild and
PackerTemplate. On success, the build's resolved source and the template's
last-successful source are committed atomically with the timestamp. The transaction
locks and reloads the template before comparing its current declared pin with the
resolved source, so a mismatched override or a pin edit during the build writes stale
instead of ready. Readiness requires both ready status and no computed staleness. Pin
changes are therefore visible immediately: changing or clearing a declared pin makes
the old artifact stale, and a per-build pin that differs from the template also produces
a stale template. Pin drift is checked even when max_age_days is unset; automatic
remediation sets the template to building and dispatches the queued build after any
configured branch merge. A queued row left by the former create-only path is recovered
rather than blocking remediation forever.
An unpinned build records its derived URL with an empty digest and does not become stale
merely because historical unpinned snapshots are empty.
The rule that matters: a pinned URL must carry a digest. If an explicit URL comes
from either source and no digest resolves, jobs._resolve_cloud_image_source() raises
and the build fails closed. A pin without verification is the worst of both worlds — it
looks like provenance while guaranteeing nothing about the bytes, and it does not even
survive the vendor replacing the artifact at that URL. A malformed digest is refused
rather than forwarded; an uppercase digest is normalised, since hex is case-insensitive.
An unpinned release build still runs without a digest. Requiring one everywhere would break every existing template at once, so that gap is closed per profile, by pinning it.
What this does NOT do¶
No profile is pinned by this change. Pinning requires two judgements that must not be guessed: which dated vendor image a profile should target, and a digest that has actually been verified. An invented or unverified digest is worse than none.
To pin a profile:
- Choose a dated vendor directory instead of
latest(for example a dated Debian cloud-image build, or an Ubuntu release-dated image). - Obtain the digest from the vendor's own published checksum file for that exact artifact — not from a mirror, a search result, or this repository's history.
- Verify it: download the image, compute
sha256sum, and confirm it matches the vendor's published value. - Set
base_image_urlandbase_image_sha256on the template, and record in the template description (or the pinning PR) where the digest came from and how it was verified. - Rebake the profile so the recorded artifact is the one in use.
Pinning trades automatic upstream fixes for reproducibility, so a pinned profile needs a periodic refresh to pick up base-image security updates.
PowerDNS Authoritative + Recursor Template¶
Migration 0013_seed_powerdns_auth_recursor_cloud_init.py seeds a co-hosted
PowerDNS image for internal DNS service VMs.
| Field | Value |
|---|---|
| Template name | powerdns-auth-recursor-ubuntu |
| Installer config | powerdns-auth-recursor-ubuntu |
| OS | Ubuntu 24.04 |
| Template VMID | 9019 |
| Proxmox endpoint | https://10.0.30.71:8006 |
| Proxmox node / SSH host | 10.0.30.71 |
| Storage | local |
| Authoritative listener | 127.0.0.1:5300 |
| Recursor listener | VM primary IPv4 address on port 53 |
The default bake target is the CLUSTER01-DC01 PVE cluster host
10.0.30.71. Operators may override the node or VMID at build dispatch when a
different target is needed.
The cloud-init payload installs pdns-server, pdns-backend-sqlite3,
pdns-recursor, qemu-guest-agent, sqlite3, and iproute2. The
authoritative service uses the bundled SQLite3 backend and initializes the
gsqlite3 schema at /var/lib/powerdns/pdns.sqlite3.
Authoritative is loopback-only:
- DNS:
local-address=127.0.0.1,local-port=5300 - API:
webserver=yes,webserver-address=127.0.0.1,webserver-port=8081 - API key placeholder:
PDNS_AUTH_API_KEY
Recursor is the resolver surface:
- DNS: VM primary IPv4 on port 53
- API:
webserver=yes,webserver-address=127.0.0.1,webserver-port=8082 - Local-zone forwarding:
PDNS_LOCAL_FORWARD_ZONES, defaultnmulti.cloud=127.0.0.1:5300 - Optional recursive forwarding:
PDNS_LOCAL_FORWARD_ZONES_RECURSEappends aforward-zones-recurseentry when provided - API key placeholder:
PDNS_RECURSOR_API_KEY
The recursor allow-from list is restricted to 127.0.0.1/8, 10.0.0.0/8,
172.16.0.0/12, 192.168.0.0/16, and ::1/128. Do not set it to
0.0.0.0/0; this template must not become an open resolver.
Passbolt CE Template¶
Migration 0015_seed_passbolt_cloud_init.py seeds the Passbolt CE secret-manager
image that hosts https://credential.nmulti.cloud.
| Field | Value |
|---|---|
| Template name | passbolt-ce-ubuntu-2404 |
| Installer config | passbolt-ce-ubuntu-2404 |
| Cloud-config source | netbox_packer/seeds/passbolt-ce-ubuntu-2404.cloud-config.yaml |
| OS | Ubuntu 24.04 |
| Template VMID | 9060 |
| Proxmox endpoint | https://10.0.30.71:8006 |
| Proxmox node / SSH host | 10.0.30.71 |
| Storage | local |
| App base URL | https://credential.nmulti.cloud |
The cloud-config installs the native passbolt-ce-server package via the
official checksum-verified repo setup (nginx + php-fpm + a local MariaDB), makes
PASSBOLT_PLUGINS_JWT_AUTHENTICATION_ENABLED=true effective via php-fpm pool
env[] entries, and configures nginx WITHOUT
SSL (passbolt/nginx-configuration-three-choices select none) because nginx-nms
terminates TLS upstream — the guest serves plain HTTP on :80. The QEMU guest
agent and Zabbix Agent 2 (pointed at zabbix.nmulti.cloud) are injected at bake
time, so they are intentionally absent from the seed. No secret is baked: the
local DB password is generated on first boot into /etc/passbolt/.db_password,
and the production server OpenPGP key, JWT keys, and database are supplied by the
data migration from the existing Passbolt instance. SMTP is intentionally left
unconfigured until an operator wires a relay.
File Server All-in-One Template¶
Migration 0014_seed_fileserver_allinone_cloud_init.py originally seeded the
combined file server image used by File Server auto-provisioning. Migration
0017_update_fileserver_agent_package_index.py updates the current record.
| Field | Value |
|---|---|
| Template name | tpl-fileserver-allinone-ubuntu-2404 |
| Installer config | fileserver-allinone-cloud-config version 1.0.1 |
| Cloud-config source | netbox_packer/seeds/tpl-fileserver-allinone.cloud-config.yaml |
| OS | Ubuntu 24.04 |
| Template VMID | 9300 |
| Proxmox endpoint | https://10.0.30.71:8006 |
| Proxmox node / SSH host | 10.0.30.71 |
| Storage | local |
| NMS backend URL | https://backend.nms.nmulti.cloud |
| NetBox URL | https://netbox.nmulti.cloud |
The default bake target is CLUSTER01-DC01. Operators may override the node or VMID at build dispatch when a different target is needed, but the seeded row is the production convention for this image.
The cloud-config installs Samba AD/DC packages (samba, samba-dsdb-modules,
samba-vfs-modules, winbind, libnss-winbind, libpam-winbind,
krb5-user, acl, attr, chrony), Nextcloud web/PHP prerequisites
(nginx, php-fpm, php-ldap, php-smbclient, php-pgsql, php-gd,
php-curl, php-zip, php-xml, php-mbstring, php-intl, php-bcmath,
php-gmp, php-imagick, smbclient, cifs-utils, postgresql-client),
qemu-guest-agent, zabbix-agent2, and python3-venv.
nms-fileserver-agent is installed into /opt/nms-fileserver-agent/venv from
NMS_FILESERVER_AGENT_PIP_SPEC (default nms-fileserver-agent==0.1.0), not
through apt. Store the package-index identity on the singleton
PackerPluginSettings row: the username is plaintext metadata in
fileserver_package_read_user, while the token is written only through
set_fileserver_package_read_token() and stored in
fileserver_package_read_token_encrypted with the same Fernet cipher derived
from Django's SECRET_KEY that is used by the proxbox-api key.
Use the Django/NetBox shell (manage.py nbshell or manage.py shell) to set or
rotate the credential; there is no plugin REST endpoint for this settings row,
and the encrypted field is intentionally not directly editable in Django admin.
The raw token therefore stays out of request/API parameters and is supplied only
to the model's encrypted setter in an operator shell:
from netbox_packer.models import PackerPluginSettings
settings_row = PackerPluginSettings.get_solo()
settings_row.fileserver_package_read_user = "nms-pkg-reader"
settings_row.set_fileserver_package_read_token("<gitea-package-read-token>")
settings_row.save()
The credentials must belong to a dedicated non-human Gitea identity, and the
token must have package-Read permission only—never use a personal token or
PACKAGE_WRITE_TOKEN. Dispatch fails closed if either setting is empty,
URL-encodes both values, and redacts the raw or encoded token from persisted
build output. The rendered golden image stores the authenticated N-MultiCloud
PyPI index in root-only /etc/nms-fileserver-agent/pip.conf. Public httpx is
installed from PyPI before the agent is installed from the sole private index
with --no-deps.
Because every clone inherits that root-only credential, rotate the token on the
PackerPluginSettings row and rebake VMID 9300 whenever the read token is
replaced; retire prior images and clones according to the credential-rotation
policy.
Migration 0017_update_fileserver_agent_package_index.py repoints installations
that already ran migration 0014 at this v1.0.1 config and marks the template
pending for a replacement bake.
This image is software-only. The bake does not create a Samba domain, does not
run a Nextcloud tenant install, and does not include any tenant secret. nginx
is disabled, smbd/nmbd/winbind are masked,
nms-fileserver-agent-enroll.service is installed but disabled/not run on the
golden template, and nms-fileserver-agent-heartbeat.timer is disabled until
clone-time user-data provides the one-time enrollment token and starts the agent
lifecycle.
Base Ubuntu LTS Cloud-init Templates¶
Migration 0016_seed_ubuntu_lts_base_cloud_init.py seeds three minimal base
images that serve as the starting templates for the customer VM catalog.
| Field | Value |
|---|---|
| Template names | ubuntu-2204-cloudinit-base (VMID 9040), ubuntu-2404-cloudinit-base (VMID 9041), ubuntu-2604-cloudinit-base (VMID 9042) |
| Installer config | ubuntu-lts-base-cloud-config (shared by all three) |
| OS | Ubuntu 22.04, 24.04, 26.04 respectively |
| Proxmox endpoint | https://10.0.30.71:8006 |
| Proxmox node / SSH host | 10.0.30.71 |
| Storage | local |
This is the only seed migration with a fully reversible reverse function — rolling it back deletes the three seeded rows.
The cloud-config content is intentionally minimal. qemu-guest-agent,
zabbix-agent2, and ssh_pwauth: true are all added by the build-time
monitoring-agent injection in jobs.py rather than baked into the seed
content directly. No secret is baked into the image: per-VM username,
password (cipassword), and SSH keys are supplied by Proxmox cloud-init at
clone time.
Akvorado 2.4.0 Template¶
Migrations 0023_packertemplate_nms_agent_and_service_marker.py and
0024_seed_akvorado_cloud_init.py add the optional NMS host-agent injection
fields, durable service marker, and Akvorado golden template.
| Field | Value |
|---|---|
| Template name | akvorado-2.4.0-ubuntu-2404 |
| Installer config | akvorado-2.4.0-ubuntu-2404-cloud-config version 2.4.0 |
| Cloud-config source | netbox_packer/seeds/akvorado-2.4.0-ubuntu-2404.cloud-config.yaml |
| OS | Ubuntu 24.04 |
| Template VMID | 9070 |
| Proxmox endpoint | https://10.0.30.71:8006 |
| Proxmox node / SSH host | 10.0.30.71 |
| Storage | local |
| Service marker | akvorado |
| Lifecycle unit | akvorado.service |
On first boot, the cloud-config checksum-verifies the Docker APT signing key,
installs Docker Engine and the Compose plugin, validates and pulls
/opt/akvorado/docker-compose.yml, then enables akvorado.service. That single
oneshot unit owns the complete Compose lifecycle with up -d --wait on start
and down on stop. The Compose stack pins:
- Kafka
apache/kafka:4.2.0in single-node KRaft mode; - Valkey
valkey/valkey:9.0(there is no Redis server or Redis container); - ClickHouse
26.3(clickhouse/clickhouse-server:26.3); - Akvorado console, inlet, outlet, and orchestrator at
quay.io/akvorado/akvorado:2.4.0.
The console uses Akvorado's cache driver named redis to speak the
Redis-compatible protocol to valkey:6379; the backing service is Valkey.
Flow receivers listen on UDP 2055, 4739, and 6343, the BMP receiver on
TCP 10179, and the console only on loopback at 127.0.0.1:8081. Akvorado's
console trusts an identity header from a fronting authenticating proxy and does
not authenticate users itself, so the template does not publish it on a
wildcard host address. Reach it through an SSH tunnel to loopback port 8081,
or provision a separate authenticating reverse proxy explicitly. The shipped
akvorado.yaml plus inlet/outlet/console includes is a working credential-free
default, so no configuration RPC is required before the stack starts.
The seed sets install_nms_agent=True only on this template. The build-time
injector compiles the static agent from one pinned public commit using a
SHA256-verified Go toolchain, writes a root-only config pointing at
https://backend.nms.nmulti.cloud, and enables its systemd unit. It reuses the
agent's secure-prefix self-registration flow and contains no enrollment token,
backend signing key, or other trust material. Zabbix management inside that
agent is disabled because the existing Zabbix Agent 2 injection remains the
owner of Zabbix configuration. Its local service allowlist contains exactly
akvorado.service.
Created VMs already store the source template primary key in the
source_packer_template custom field. Downstream integration code follows
that existing lineage to the read-only
PackerTemplate.provisions_service="akvorado" marker instead of guessing from
a hostname or introducing another VM tag.
Build Verification¶
After either current InfluxDB build completes, the template row should be
ready with result VMID 9050 or 9051; the selected target cluster should
contain a template with a cicustom user-data snippet. Clone verification must
confirm the exact package version, held package state, systemd state, and local
health/readiness endpoint before typed RPC onboarding begins.
For InfluxDB 3 Explorer, VMID 9053 should be a template on the selected
endpoint. On a fresh clone, wait for cloud-init status --wait, confirm
systemctl is-active influxdb3-explorer.service, and inspect the running image
reference for the reviewed digest. curl http://127.0.0.1:8080/ should answer,
while /etc/influxdb3-explorer/config.json should not exist before provision-time
automation resolves the clone's nms-secret:<opaque-id> reference. After
provisioning, confirm that file is root:1500 mode 0640 and the unit restarted.
For the PowerDNS co-hosted template, VMID 9019 should be marked as a template
on 10.0.30.71. On first boot from a clone, pdns should listen on
127.0.0.1:5300, pdns-recursor should listen on the primary IPv4 address on
port 53, both PowerDNS API webservers should bind to localhost, and no
configuration should expose recursion to 0.0.0.0/0.
For the File Server all-in-one template, VMID 9300 should be marked as a
template on 10.0.30.71. On a clone before tenant provisioning, Samba and
nginx should remain inactive, zabbix-agent2 should point at
zabbix.nmulti.cloud, and /etc/nms-fileserver-agent/config.env should contain
only the production NMS_BACKEND_URL and NETBOX_URL values.
For Akvorado, VMID 9070 should be marked as a template on 10.0.30.71 and
retain the cloud-init snippet. On a fresh clone, wait for
cloud-init status --wait, then verify systemctl is-active akvorado.service
and systemctl is-active nms-agent.service. docker compose -f
/opt/akvorado/docker-compose.yml ps should show all seven services running and
the Kafka, Valkey, ClickHouse, orchestrator, and console health checks should
settle healthy. The agent journal should show a bootstrap attempt against
backend.nms.nmulti.cloud; authorization still follows the existing
secure-prefix policy.
Regression Coverage¶
tests/test_cloud_config_build_static.py locks the cloud-init build contract:
- the cloud-config branch delegates to
proxbox-api /cloud/templates/images; - the client performs plan → signed preflight → execute in order, keeps all build fields stable, and forwards the returned plan token;
- unavailable/not-ready/incompatible preflight, missing or expired tokens, execute-time plan rejection, and unverified execution all fail the build;
- unset target nodes are sent as
None, not an empty string; - historical migration
0007remains byte-for-byte unchanged while additive migration0020safely retires its database row; - OSS 2.9.1 and Core 3.11.0 cloud-configs parse as YAML, pin and hold exact package versions, verify the InfluxData signing key, and contain no setup request or credential material;
- Explorer 1.9.0 uses the exact
influxdata/influxdb3-uimanifest digest, Debiandocker.io, a loopback-default systemd lifecycle, bounded first boot, collision-safe VMID9053seeding, and no baked Core connection or credential; - build requests validate explicit endpoint, node, VMID, and storage selectors and suppress legacy SSH-host metadata when an endpoint ID is selected;
- project docs and LLM files cover both profiles, typed RPC onboarding, and the
nms-secret:boundary. - the PowerDNS co-hosted seed keeps
pdns-server,pdns-recursor,qemu-guest-agent,127.0.0.1:5300, privateallow-fromranges, and reversible seeded-row cleanup stable. - the File Server all-in-one seed keeps
tpl-fileserver-allinone-ubuntu-2404,fileserver-allinone-cloud-config, VMID9300, CLUSTER01-DC01 endpointhttps://10.0.30.71:8006, production NMS URLs, service-disabled defaults, root-only package-index configuration, package-Read credential placeholders, YAML parseability, and reversible seeded-row cleanup stable. - the Akvorado seed keeps VMID
9070, Kafka4.2.0, Valkey9.0, ClickHouse26.3, Akvorado2.4.0, the exactakvorado.servicelifecycle contract, loopback-only console, working default config, HTTPS-only agent backend, agent opt-in/default-off behavior, source-template marker, and structural agent deduplication stable.