From e74e04955a4677d08478206ed0c419f84dbef38a Mon Sep 17 00:00:00 2001 From: automation-bot Date: Sat, 19 Sep 2026 09:35:10 +0300 Subject: [PATCH] docs: fix iac-pipeline.md drift (provisioning/compliance role counts, templates) Partial fix - diagram update still pending (Nextcloud unavailable). Live-verified against roles_metadata.yml: provisioning stage now has 2 roles (clamav, netbird_agent - added for NetBird mesh enrollment), compliance stage now has 16 (added ssh_ca_client). Technitium's shown tag-file example was stale (21->22 tags, missing netbird_agent). containers.tf gained an 18-line header comment that pushed its actual resource block past line 20, so the doc's 'head -20' example no longer reproduced the shown output - fixed to a targeted sed range. Also found and documented a genuine live inconsistency: host_tags.yml.tpl gained an inventory_group field with the 2026-08 LXC rollout, but no existing VM host has been re-applied since, so none of their rendered files have it yet (only new LXCs do) - called out explicitly rather than papered over. --- docs/iac-pipeline.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/iac-pipeline.md b/docs/iac-pipeline.md index 7033d6f..64ff56f 100644 --- a/docs/iac-pipeline.md +++ b/docs/iac-pipeline.md @@ -53,7 +53,7 @@ Terraform manages both VMs and LXCs now — two separate resource types, one per The LXC path is genuinely newer and mirrors the VM flow step-for-step, but has to close a gap the VM path never has: the stock container template has no `sshd` and no fleet users at all, since it never went through Packer. A `pct exec` bootstrap step runs first — installing `sshd` and creating both fleet identities with their keys, the same baseline Packer bakes into the VM image at build time instead of at provision time — before the same wait-for-SSH and tag-file-render steps every VM already gets. This is the real resource definition, not a paraphrase: ``` -$ head -20 containers.tf +$ sed -n '20,39p' containers.tf resource "proxmox_virtual_environment_container" "ct" { provider = proxmox-bpg for_each = var.containers @@ -64,6 +64,8 @@ resource "proxmox_virtual_environment_container" "ct" { start_on_boot = each.value.onboot started = true + # PVE-native tags — cosmetic/searchable in the PVE UI, mirrors main.tf's + # `tags = join(",", each.value.tags)` on the VM resource. tags = each.value.tags cpu { @@ -82,10 +84,12 @@ This is the actual template that renders into every host's tag file — the lite ``` $ cat host_tags.yml.tpl +# Host metadata from Terraform provisioning vmid: ${vmid} vm_name: ${name} target_node: ${target_node} template: ${template} +inventory_group: ${inventory_group} cores: ${cores} sockets: ${sockets} memory: ${memory} @@ -102,7 +106,12 @@ vm_tags: %{ endfor ~} ``` -Every field in this file traces back to one map entry in Terraform's own VM definitions — nothing here is hand-maintained separately from the VM it describes. And this is a real rendered instance of it, not a hypothetical fill-in — one fleet host's actual generated tag file, `ip`/`macaddr` redacted: +Every field in this file traces back to one map entry in Terraform's own VM definitions — nothing here is hand-maintained separately from the VM it describes. + +> [!NOTE] +> **A field that exists in the template but not yet in any VM's real file.** `inventory_group` was added to this template alongside the 2026-08 LXC rollout — new LXCs render it correctly (`rocky_lxc`, confirmed live on netbird), but no existing VM has been re-applied since, so `inventory_group: rocky_vm` isn't actually in any of their rendered files yet, technitium's included below. A real, current gap, not a hypothetical one — worth naming rather than quietly patching the example to look more finished than the fleet actually is. + +And this is a real rendered instance of it, not a hypothetical fill-in — one fleet host's actual generated tag file, `ip`/`macaddr` redacted: ``` $ cat host_vars/technitium/host_tags.yml @@ -142,9 +151,10 @@ vm_tags: - observability_agent - mqtt_client - wazuh_agent_tuning + - netbird_agent ``` -21 tags on this one host alone, spanning both the initialization roles (`packages`, `firewall_ssh`, …) and every one of the 9 compliance sub-roles — the same file Terraform generated is the exact input every Ansible stage after it reads. +22 tags on this one host alone, spanning the initialization roles (`packages`, `firewall_ssh`, …), the provisioning-stage `netbird_agent` role, and every one of the 9 compliance sub-roles — the same file Terraform generated is the exact input every Ansible stage after it reads. ## Ansible: tag-driven, re-applied continuously @@ -159,13 +169,13 @@ d = yaml.safe_load(open('roles_metadata.yml'))['roles_metadata'] print('provisioning:', [k for k,v in d.items() if v.get('provisioning') is True]) print('compliance:', sorted(k for k,v in d.items() if v.get('compliance') is True)) " -provisioning: ['clamav'] -compliance: ['auditd_hardening', 'banners', 'chrony_hardening', 'disk_resize', 'docker_bench', 'file_permissions', 'filesystem_hardening', 'kernel_hardening', 'mqtt_client', 'observability_agent', 'password_policy', 'service_disable', 'ssh_hardening', 'wazuh_agent_tuning', 'zsh_custom'] +provisioning: ['clamav', 'netbird_agent'] +compliance: ['auditd_hardening', 'banners', 'chrony_hardening', 'disk_resize', 'docker_bench', 'file_permissions', 'filesystem_hardening', 'kernel_hardening', 'mqtt_client', 'observability_agent', 'password_policy', 'service_disable', 'ssh_ca_client', 'ssh_hardening', 'wazuh_agent_tuning', 'zsh_custom'] ``` -`clamav` is the only provisioning-stage role, and only one host carries it in its `vm_tags`. An under-used pipeline stage isn't the same as an unused one — it exists on purpose for exactly this shape of role, not as leftover scaffolding. +Two roles are tagged for the provisioning stage: `clamav` (rocky-docker only) and `netbird_agent` (enrols a host into the NetBird mesh overlay covered in [`docs/netbird-zero-trust.md`](netbird-zero-trust.md) — monitoring was the first host onboarded through it). Neither fits "once at init" or "every compliance run" cleanly, which is exactly the shape this stage exists for — an under-used pipeline stage isn't the same as an unused one. -Compliance hardening runs as a set of independently-tagged sub-roles, selected per host via that same tag list — which controls apply to a given host is explicit and auditable in that host's own config, not an implicit "all or nothing." All 9 current sub-roles (of the 15 total compliance-stage roles above — the other 6 handle observability, notifications, and disk/shell setup, not CIS hardening specifically) are active fleet-wide; none are disabled or pending. This same tag-driven selection mechanism is what makes the deliberate CIS deviations in [`docs/security-monitoring.md`](security-monitoring.md) auditable rather than hidden — a reader can see exactly which controls a given host runs by reading its tags, not by reverse-engineering behavior. +Compliance hardening runs as a set of independently-tagged sub-roles, selected per host via that same tag list — which controls apply to a given host is explicit and auditable in that host's own config, not an implicit "all or nothing." All 9 current sub-roles (of the 16 total compliance-stage roles above — the other 7 handle observability, notifications, SSH certificate refresh, and disk/shell setup, not CIS hardening specifically) are active fleet-wide; none are disabled or pending. This same tag-driven selection mechanism is what makes the deliberate CIS deviations in [`docs/security-monitoring.md`](security-monitoring.md) auditable rather than hidden — a reader can see exactly which controls a given host runs by reading its tags, not by reverse-engineering behavior. ## The Honest Exception: not everything fits the clean model