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.
This commit is contained in:
automation-bot committed 2026-09-19 09:35:10 +03:00
1 parent a5254a95ac
commit e74e04955a
1 file changed
+17 -7
+17 -7
View File
@@ -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: 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" { resource "proxmox_virtual_environment_container" "ct" {
provider = proxmox-bpg provider = proxmox-bpg
for_each = var.containers for_each = var.containers
@@ -64,6 +64,8 @@ resource "proxmox_virtual_environment_container" "ct" {
start_on_boot = each.value.onboot start_on_boot = each.value.onboot
started = true 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 tags = each.value.tags
cpu { 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 $ cat host_tags.yml.tpl
# Host metadata from Terraform provisioning
vmid: ${vmid} vmid: ${vmid}
vm_name: ${name} vm_name: ${name}
target_node: ${target_node} target_node: ${target_node}
template: ${template} template: ${template}
inventory_group: ${inventory_group}
cores: ${cores} cores: ${cores}
sockets: ${sockets} sockets: ${sockets}
memory: ${memory} memory: ${memory}
@@ -102,7 +106,12 @@ vm_tags:
%{ endfor ~} %{ 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 $ cat host_vars/technitium/host_tags.yml
@@ -142,9 +151,10 @@ vm_tags:
- observability_agent - observability_agent
- mqtt_client - mqtt_client
- wazuh_agent_tuning - 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 ## 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('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)) print('compliance:', sorted(k for k,v in d.items() if v.get('compliance') is True))
" "
provisioning: ['clamav'] 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_hardening', 'wazuh_agent_tuning', 'zsh_custom'] 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 ## The Honest Exception: not everything fits the clean model