Table of Contents
The opnsense: extension
A single extra top-level key inside user-data, alongside the standard
#cloud-config fields. Real cloud-init ignores unknown top-level keys, so a
user-data document carrying this key is still a fully valid
#cloud-config — nothing about the standard seed changes.
You only need this when the default behavior doesn't fit:
- Default role assignment: the first NIC in
network-configbecomes WAN, the second LAN, any further NICs becomeopt1,opt2, ... - Default addressing: whatever each NIC's
network-configsubnet says (DHCP or static v4/v6). - Default users:
ssh_authorized_keys/users:entries namedrootare applied to therootaccount; nothing else changes.
If that's all you need — e.g. examples/wan-lan-dhcp/ — skip this
extension entirely.
Schema
#cloud-config
# ... standard fields (hostname, ssh_authorized_keys, users, ...) ...
opnsense:
roles:
# MAC (any case) -> role, overriding the default net0/net1/netN
# positional assignment for that interface.
"bc:24:11:00:10:01": wan
"bc:24:11:00:10:02": lan
vlans:
# Each entry creates a VLAN sub-interface. A NIC referenced only as a
# VLAN parent (no `roles` entry of its own) does not get a standalone
# top-level role slot.
- parent_mac: "bc:24:11:00:10:03"
tag: 10
role: opt1
descr: "Guest Wi-Fi VLAN"
- parent_mac: "bc:24:11:00:10:03"
tag: 20
role: opt2
descr: "IoT VLAN"
api_keys:
# Generates an OPNsense API key/secret pair for the root user on first
# boot. The secret is written once to /root/.opnseed-api-keys.txt
# (mode 600) on the guest -- config.xml only ever gets the hash.
- description: "ansible-provisioning"
ipv6_track:
# MAC -> role to track for IPv6 (e.g. LAN tracking WAN's DHCPv6-PD
# delegation). Standard cloud-init network-config has no "track6"
# subnet type, so this can only come from here.
"bc:24:11:00:10:02": wan
See opnseed/src/opnseed/extension.py for the exact parsing rules and
examples/wan-lan-static-vlans/ for a complete worked example (also
exercised by the test suite, opnseed/tests/test_extension.py and
test_merge.py::test_merge_static_vlans_example).
Delivering it
Proxmox's native Cloud-Init fields (GUI, qm set, or this repo's OpenTofu
module's ip_config/user_account/dns) can't express an extension block
directly — there's no field for it. Instead, author a user-data file with
the standard fields plus this block, upload it as a Proxmox snippet, and
point the VM's Cloud-Init config at it:
qm set <vmid> --cicustom "user=local:snippets/my-user-data.yml"
Or, in OpenTofu, set user_data_file_id on the opnsense-vm module (see
terraform/examples/branch-with-vlans/) — this supersedes the module's own
ip_config/user_account-derived user-data entirely; network-config (the
NIC list and its own addressing) still comes from the module's
network_interfaces, since the extension only ever lives in user-data.