From 44c4978b5ff79264aadc28449c13ac59a2e0da12 Mon Sep 17 00:00:00 2001 From: sjat Date: Wed, 17 Jun 2026 16:04:46 +0200 Subject: [PATCH] feat(base): NetBird agent enrollment concern (mesh) Co-Authored-By: Claude Opus 4.8 (1M context) --- roles/base/README.md | 27 +++++++++++++++ roles/base/defaults/main.yml | 10 ++++++ roles/base/tasks/main.yml | 8 +++++ roles/base/tasks/mesh.yml | 66 ++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 roles/base/tasks/mesh.yml diff --git a/roles/base/README.md b/roles/base/README.md index 20bd053..9bf46d4 100644 --- a/roles/base/README.md +++ b/roles/base/README.md @@ -27,3 +27,30 @@ render + validate without applying (used by Molecule). - `make test ROLE=base` — Molecule renders + `nft -c` syntax-checks (never applies; it shares the host kernel). Enforcement + the apply/rollback path are verified at ADR-008 Level 2 on staging VMs. + +## Mesh enrollment (NetBird agent) + +Enrols the host as a NetBird *agent* on the self-hosted mesh (ADR-016): installs the +pinned `netbird` daemon from the upstream APT repo (keyring in `/etc/apt/keyrings`, +mirroring the `docker_host` repo idiom) and runs `netbird up` against the coordinator +with a setup key. Tagged `mesh`. + +**Additive only — this concern makes no firewall change.** SSH is already gated to the +NetBird overlay interface by the `firewall` concern (`base__firewall_mgmt_interface`, +default `wt0`); enrolling a host simply brings that interface up. No port is opened here. + +Enrolment is **opt-in**: `base__mesh_enabled` defaults to `false`, so applying `base` to +a host not on the mesh is a no-op for this concern. Re-enrolment is guarded on +`netbird status` reporting `Management: Connected`, so re-runs are idempotent. The setup +key is sourced from `vault.netbird.setup_key` and passed with `no_log` (it lands on the +process argv). + +### Variables + +| Variable | Default | Purpose | +|------------------------------|--------------------------------------|---------| +| `base__mesh_enabled` | `false` | Opt-in switch — include the concern at all. Set per-host/group to enrol. | +| `base__mesh_manage` | `true` | Test gate — when `false`, skips the live network/daemon tasks (apt install, status check, `netbird up`) so Molecule can exercise the wiring without a coordinator. | +| `base__mesh_management_url` | `https://netbird.askari.wingu.me` | Coordinator (management) URL. | +| `base__mesh_setup_key` | `{{ vault.netbird.setup_key }}` | Enrolment setup key, from vault. | +| `base__mesh_version` | `"0.72.4"` | Pinned agent version (matches the coordinator). The exact apt version string is confirmed on-host at deploy. | diff --git a/roles/base/defaults/main.yml b/roles/base/defaults/main.yml index cbdf34d..681f0c2 100644 --- a/roles/base/defaults/main.yml +++ b/roles/base/defaults/main.yml @@ -20,3 +20,13 @@ base__fail2ban_bantime: 1h base__fail2ban_findtime: 10m # base__ssh_authorised_keys lives in group_vars/all/vars.yml (per-person control keys). base__ssh_authorised_keys: [] + +# NetBird mesh agent enrollment (ADR-016). Opt-in: default off so applying `base` to a +# host not on the mesh is a no-op for this concern. The live actions (apt install over +# the network, `netbird up` against the coordinator) are additionally gated by +# base__mesh_manage so Molecule can exercise the wiring without a coordinator. +base__mesh_enabled: false +base__mesh_manage: true +base__mesh_management_url: "https://netbird.askari.wingu.me" +base__mesh_setup_key: "{{ vault.netbird.setup_key }}" +base__mesh_version: "0.72.4" # match the coordinator; exact apt pin confirmed on-host at deploy diff --git a/roles/base/tasks/main.yml b/roles/base/tasks/main.yml index 1b585ba..b022586 100644 --- a/roles/base/tasks/main.yml +++ b/roles/base/tasks/main.yml @@ -22,3 +22,11 @@ apply: tags: [hardening] tags: [hardening] + +- name: NetBird mesh enrollment + ansible.builtin.include_tasks: + file: mesh.yml + apply: + tags: [mesh] + when: base__mesh_enabled | bool + tags: [mesh] diff --git a/roles/base/tasks/mesh.yml b/roles/base/tasks/mesh.yml new file mode 100644 index 0000000..5226043 --- /dev/null +++ b/roles/base/tasks/mesh.yml @@ -0,0 +1,66 @@ +--- +# NetBird agent enrollment (ADR-016). Additive only — no firewall change here. +- name: Install NetBird apt prerequisites + ansible.builtin.apt: + name: [ca-certificates, curl, gnupg] + state: present + update_cache: true + when: base__mesh_manage | bool + tags: [mesh] + +- name: Ensure /etc/apt/keyrings exists + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: "0755" + when: base__mesh_manage | bool + tags: [mesh] + +- name: Add the NetBird APT GPG key + ansible.builtin.get_url: + url: https://pkgs.netbird.io/debian/public.key + dest: /etc/apt/keyrings/netbird.asc + mode: "0644" + when: base__mesh_manage | bool + tags: [mesh] + +- name: Add the NetBird APT repository + ansible.builtin.apt_repository: + repo: >- + deb [signed-by=/etc/apt/keyrings/netbird.asc] + https://pkgs.netbird.io/debian stable main + filename: netbird + state: present + when: base__mesh_manage | bool + tags: [mesh] + +# The apt pin string can't be confirmed from docs — it might be a bare "0.72.4" or +# carry a packaging suffix. The live deploy task confirms the exact on-host string. +- name: Install the NetBird agent (pinned) + ansible.builtin.apt: + name: "netbird={{ base__mesh_version }}" + state: present + update_cache: true + when: base__mesh_manage | bool + tags: [mesh] + +- name: Check current NetBird connection status + ansible.builtin.command: netbird status + register: _netbird_status + changed_when: false + failed_when: false + when: base__mesh_manage | bool + tags: [mesh] + +- name: Enrol this host in the mesh + ansible.builtin.command: >- + netbird up + --management-url {{ base__mesh_management_url }} + --setup-key {{ base__mesh_setup_key }} + register: _netbird_up + changed_when: _netbird_up.rc == 0 + when: + - base__mesh_manage | bool + - "'Management: Connected' not in (_netbird_status.stdout | default(''))" + no_log: true # setup key is on the argv + tags: [mesh]